2050 lines
90 KiB
Objective-C
2050 lines
90 KiB
Objective-C
//
|
||
// SettingsViewController.m
|
||
// Moonlight
|
||
//
|
||
// Created by Diego Waxemberg on 10/27/14.
|
||
// Copyright (c) 2014 Moonlight Stream. All rights reserved.
|
||
//
|
||
// Modified by True砖家 since 2024.6.1
|
||
// Copyright © 2024 True砖家 @ Bilibili. All rights reserved.
|
||
//
|
||
|
||
#import "SettingsViewController.h"
|
||
#import "TemporarySettings.h"
|
||
#import "DataManager.h"
|
||
#import "ThemeManager.h"
|
||
|
||
#import <UIKit/UIGestureRecognizerSubclass.h>
|
||
#import <VideoToolbox/VideoToolbox.h>
|
||
#import <AVFoundation/AVFoundation.h>
|
||
|
||
#import "LocalizationHelper.h"
|
||
|
||
@implementation SettingsViewController {
|
||
NSInteger _bitrate;
|
||
NSInteger _lastSelectedResolutionIndex;
|
||
bool justEnteredSettingsViewDoNotOpenOscLayoutTool;
|
||
uint16_t oswLayoutFingers;
|
||
CustomEdgeSlideGestureRecognizer *slideToCloseSettingsViewRecognizer;
|
||
NSMutableDictionary *_settingStackDict;
|
||
NSMutableArray *_favoriteSettingStackIdentifiers;
|
||
bool settingStackWillBeRelocatedToLowestPosition;
|
||
uint8_t currentSettingsMenuMode;
|
||
UIView *snapshot;
|
||
UIStackView* capturedStack;
|
||
CADisplayLink *_autoScrollDisplayLink;
|
||
CGFloat _scrollSpeed;
|
||
CGFloat _currentRefreshRate;
|
||
MenuSectionView *touchControlSection;
|
||
NSMutableSet* hiddenStacks;
|
||
}
|
||
|
||
@dynamic overrideUserInterfaceStyle;
|
||
|
||
|
||
//static NSString* bitrateFormat;
|
||
static const int bitrateTable[] = {
|
||
500,
|
||
1000,
|
||
1500,
|
||
2000,
|
||
2500,
|
||
3000,
|
||
4000,
|
||
5000,
|
||
6000,
|
||
7000,
|
||
8000,
|
||
9000,
|
||
10000,
|
||
11000,
|
||
12000,
|
||
13000,
|
||
14000,
|
||
15000,
|
||
16000,
|
||
17000,
|
||
18000,
|
||
19000,
|
||
20000,
|
||
21000,
|
||
22000,
|
||
23000,
|
||
24000,
|
||
25000,
|
||
26000,
|
||
27000,
|
||
28000,
|
||
29000,
|
||
30000,
|
||
31000,
|
||
32000,
|
||
33000,
|
||
34000,
|
||
35000,
|
||
36000,
|
||
37000,
|
||
38000,
|
||
39000,
|
||
40000,
|
||
41000,
|
||
42000,
|
||
43000,
|
||
44000,
|
||
45000,
|
||
46000,
|
||
47000,
|
||
48000,
|
||
49000,
|
||
50000,
|
||
50000,
|
||
51000,
|
||
52000,
|
||
53000,
|
||
54000,
|
||
55000,
|
||
56000,
|
||
57000,
|
||
58000,
|
||
59000,
|
||
60000,
|
||
61000,
|
||
62000,
|
||
63000,
|
||
64000,
|
||
65000,
|
||
66000,
|
||
67000,
|
||
68000,
|
||
69000,
|
||
70000,
|
||
80000,
|
||
90000,
|
||
100000,
|
||
110000,
|
||
120000,
|
||
130000,
|
||
140000,
|
||
150000,
|
||
160000,
|
||
170000,
|
||
180000,
|
||
200000,
|
||
220000,
|
||
240000,
|
||
260000,
|
||
280000,
|
||
300000,
|
||
320000,
|
||
340000,
|
||
360000,
|
||
380000,
|
||
400000,
|
||
420000,
|
||
440000,
|
||
460000,
|
||
480000,
|
||
500000,
|
||
};
|
||
|
||
const int RESOLUTION_TABLE_SIZE = 6;
|
||
const int RESOLUTION_TABLE_CUSTOM_INDEX = RESOLUTION_TABLE_SIZE - 1;
|
||
CGSize resolutionTable[RESOLUTION_TABLE_SIZE];
|
||
|
||
-(int)getSliderValueForBitrate:(NSInteger)bitrate {
|
||
int i;
|
||
|
||
for (i = 0; i < (sizeof(bitrateTable) / sizeof(*bitrateTable)); i++) {
|
||
if (bitrate <= bitrateTable[i]) {
|
||
return i;
|
||
}
|
||
}
|
||
|
||
// Return the last entry in the table
|
||
return i - 1;
|
||
}
|
||
|
||
// This view is rooted at a ScrollView. To make it scrollable,
|
||
// we'll update content size here.
|
||
-(void)viewDidLayoutSubviews {
|
||
CGFloat highestViewY = 0;
|
||
|
||
// Enumerate the scroll view's subviews looking for the
|
||
// highest view Y value to set our scroll view's content
|
||
// size.
|
||
|
||
for (UIView* view in self.scrollView.subviews) {
|
||
// UIScrollViews have 2 default child views
|
||
// which represent the horizontal and vertical scrolling
|
||
// indicators. Ignore any views we don't recognize.
|
||
if (![view isKindOfClass:[UILabel class]] &&
|
||
![view isKindOfClass:[UISegmentedControl class]] &&
|
||
![view isKindOfClass:[UISlider class]]) {
|
||
continue;
|
||
}
|
||
|
||
CGFloat currentViewY = view.frame.origin.y + view.frame.size.height;
|
||
if (currentViewY > highestViewY) {
|
||
highestViewY = currentViewY;
|
||
}
|
||
}
|
||
|
||
// Add a bit of padding so the view doesn't end right at the button of the display
|
||
self.scrollView.contentSize = CGSizeMake(self.scrollView.contentSize.width, _parentStack.frame.size.height + [self getStandardNavBarHeight] + 20);
|
||
double delayInSeconds = 3;
|
||
// Convert the delay into a dispatch_time_t value
|
||
dispatch_time_t delayTime = dispatch_time(DISPATCH_TIME_NOW, (int64_t)(delayInSeconds * NSEC_PER_SEC));
|
||
// Perform some task after the delay
|
||
dispatch_after(delayTime, dispatch_get_main_queue(), ^{// Code to execute after the delay
|
||
// [self updateResolutionAccordingly];
|
||
});
|
||
|
||
}
|
||
|
||
// Adjust the subviews for the safe area on the iPhone X.
|
||
- (void)viewSafeAreaInsetsDidChange {
|
||
[super viewSafeAreaInsetsDidChange];
|
||
|
||
//if (@available(iOS 11.0, *)) {
|
||
if (false) { // cancel this for portrait mode
|
||
for (UIView* view in self.view.subviews) {
|
||
// HACK: The official safe area is much too large for our purposes
|
||
// so we'll just use the presence of any safe area to indicate we should
|
||
// pad by 20.
|
||
if (self.view.safeAreaInsets.left >= 20 || self.view.safeAreaInsets.right >= 20) {
|
||
view.frame = CGRectMake(view.frame.origin.x + 20, view.frame.origin.y, view.frame.size.width, view.frame.size.height);
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
BOOL isCustomResolution(CGSize res) {
|
||
if (res.width == 0 && res.height == 0) {
|
||
return NO;
|
||
}
|
||
|
||
for (int i = 0; i < RESOLUTION_TABLE_CUSTOM_INDEX; i++) {
|
||
|
||
if ((res.width == resolutionTable[i].width && res.height == resolutionTable[i].height) || (res.height == resolutionTable[i].width && res.width == resolutionTable[i].height)) {
|
||
return NO;
|
||
}
|
||
}
|
||
|
||
return YES;
|
||
}
|
||
|
||
+ (bool)isLandscapeNow {
|
||
return CGRectGetWidth([[UIScreen mainScreen]bounds]) > CGRectGetHeight([[UIScreen mainScreen]bounds]);
|
||
}
|
||
|
||
- (bool)isFullScreenRequired {
|
||
NSDictionary *infoDictionary = [[NSBundle mainBundle] infoDictionary];
|
||
NSNumber *requiresFullScreen = infoDictionary[@"UIRequiresFullScreen"];
|
||
|
||
if (requiresFullScreen != nil) {
|
||
return [requiresFullScreen boolValue];
|
||
}
|
||
// Default behavior if the key is not set
|
||
return true;
|
||
}
|
||
|
||
- (bool)isAirPlayEnabled{
|
||
return [self.externalDisplayModeSelector selectedSegmentIndex] == 1;
|
||
}
|
||
|
||
- (void)updateResolutionTable{
|
||
UIWindow *window = UIApplication.sharedApplication.windows.firstObject;
|
||
CGFloat screenScale = window.screen.scale;
|
||
CGFloat safeAreaWidth = (window.frame.size.width - window.safeAreaInsets.left - window.safeAreaInsets.right) * screenScale;
|
||
CGFloat appWindowWidth = window.frame.size.width * screenScale;
|
||
CGFloat appWindowHeight = window.frame.size.height * screenScale;
|
||
if([self isAirPlayEnabled] && UIScreen.screens.count > 1){
|
||
CGRect bounds = [UIScreen.screens.lastObject bounds];
|
||
screenScale = [UIScreen.screens.lastObject scale];
|
||
appWindowWidth = bounds.size.width * screenScale;
|
||
appWindowHeight = bounds.size.height * screenScale;
|
||
}
|
||
bool needSwapWidthAndHeight = appWindowWidth > appWindowHeight;
|
||
|
||
resolutionTable[3] = CGSizeMake(safeAreaWidth, appWindowHeight);
|
||
|
||
for(uint8_t i=0;i<RESOLUTION_TABLE_SIZE;i++){
|
||
CGFloat longSideLen = resolutionTable[i].height > resolutionTable[i].width ? resolutionTable[i].height : resolutionTable[i].width;
|
||
CGFloat shortSideLen = resolutionTable[i].height < resolutionTable[i].width ? resolutionTable[i].height : resolutionTable[i].width;
|
||
if(needSwapWidthAndHeight) resolutionTable[i] = CGSizeMake(longSideLen, shortSideLen);
|
||
else resolutionTable[i] = CGSizeMake(shortSideLen, longSideLen);
|
||
}
|
||
|
||
resolutionTable[4] = CGSizeMake(appWindowWidth, appWindowHeight);
|
||
|
||
[self updateResolutionDisplayLabel];
|
||
}
|
||
|
||
// this will also be called back when device orientation changes
|
||
- (void)viewWillTransitionToSize:(CGSize)size withTransitionCoordinator:(id<UIViewControllerTransitionCoordinator>)coordinator {
|
||
[super viewWillTransitionToSize:size withTransitionCoordinator:coordinator];
|
||
double delayInSeconds = 0.2;
|
||
// Convert the delay into a dispatch_time_t value
|
||
dispatch_time_t delayTime = dispatch_time(DISPATCH_TIME_NOW, (int64_t)(delayInSeconds * NSEC_PER_SEC));
|
||
// Perform some task after the delay
|
||
dispatch_after(delayTime, dispatch_get_main_queue(), ^{// Code to execute after the delay
|
||
[self updateResolutionTable];
|
||
});
|
||
}
|
||
|
||
- (void)viewWillAppear:(BOOL)animated{
|
||
|
||
[self.resolutionSelector setEnabled:!self.customResolutionSwitch.isOn];
|
||
}
|
||
|
||
- (void)viewDidAppear:(BOOL)animated{
|
||
[super viewDidAppear:NO];
|
||
[self updateResolutionTable];
|
||
NSLog(@"Resolution table updated");
|
||
[self.resolutionSelector setEnabled:!self.customResolutionSwitch.isOn];
|
||
}
|
||
|
||
|
||
- (void)viewDidDisappear:(BOOL)animated{
|
||
[super viewDidDisappear:NO];
|
||
[[NSNotificationCenter defaultCenter] postNotificationName:@"SettingsViewClosedNotification" object:self]; // notify other view that settings view just closed
|
||
}
|
||
|
||
/*
|
||
- (void)addExitButtonOnTop{
|
||
UIButton *exitButton = [UIButton buttonWithType:UIButtonTypeSystem];
|
||
[exitButton setTitle:@"Exit" forState:UIControlStateNormal];
|
||
[exitButton setTitleColor:[UIColor whiteColor] forState:UIControlStateNormal]; // Set font color
|
||
exitButton.backgroundColor = [UIColor clearColor]; // Set background color
|
||
exitButton.titleLabel.font = [UIFont boldSystemFontOfSize:16.0]; // Set font size and style
|
||
exitButton.layer.cornerRadius = 8.0; // Optional: Round corners if desired
|
||
exitButton.clipsToBounds = YES; // Ensure rounded corners are applied properly
|
||
[exitButton addTarget:self action:@selector(exitButtonTapped) forControlEvents:UIControlEventTouchUpInside];
|
||
[self.view addSubview:exitButton];
|
||
exitButton.frame = CGRectMake(0, 20, 200, 50); // Adjust Y and height as needed
|
||
}*/
|
||
|
||
- (SettingsMenuMode)getSettingsMenuMode{
|
||
return currentSettingsMenuMode;
|
||
}
|
||
|
||
- (void)edgeSwiped {
|
||
[self.mainFrameViewController closeSettingViewAnimated:YES];
|
||
}
|
||
|
||
- (BOOL)isIPhone {
|
||
return [UIDevice currentDevice].userInterfaceIdiom == UIUserInterfaceIdiomPhone;
|
||
}
|
||
|
||
- (CGFloat)getStandardNavBarHeight{
|
||
return [self isIPhone] ? UINavigationBarHeightIPhone : UINavigationBarHeightIPad;
|
||
}
|
||
|
||
|
||
- (void)initParentStack{
|
||
self.scrollView.contentInsetAdjustmentBehavior = UIScrollViewContentInsetAdjustmentAutomatic;
|
||
// 可选:确保 scrollView 开启垂直滚动
|
||
self.scrollView.alwaysBounceVertical = YES;
|
||
self.scrollView.showsVerticalScrollIndicator = NO;
|
||
|
||
/*
|
||
self.scrollView.translatesAutoresizingMaskIntoConstraints = NO;
|
||
[NSLayoutConstraint activateConstraints:@[
|
||
// [self.scrollView.topAnchor constraintEqualToAnchor:],
|
||
[self.scrollView.leadingAnchor constraintEqualToAnchor:self.view.leadingAnchor],
|
||
[self.scrollView.trailingAnchor constraintEqualToAnchor:self.view.trailingAnchor],
|
||
[self.scrollView.topAnchor constraintEqualToAnchor:self.view.topAnchor],
|
||
[self.scrollView.bottomAnchor constraintEqualToAnchor:self.view.bottomAnchor],
|
||
]];
|
||
*/
|
||
|
||
_parentStack = [[UIStackView alloc] init];
|
||
_parentStack.axis = UILayoutConstraintAxisVertical;
|
||
_parentStack.spacing = 0;
|
||
_parentStack.translatesAutoresizingMaskIntoConstraints = NO;
|
||
[self.scrollView addSubview:_parentStack];
|
||
[NSLayoutConstraint activateConstraints:@[
|
||
[_parentStack.topAnchor constraintEqualToAnchor:self.scrollView.contentLayoutGuide.topAnchor constant: currentSettingsMenuMode == AllSettings ? [self getStandardNavBarHeight] : [self getStandardNavBarHeight]+20],
|
||
[_parentStack.bottomAnchor constraintEqualToAnchor:self.scrollView.contentLayoutGuide.bottomAnchor constant:-20],
|
||
[_parentStack.centerXAnchor constraintEqualToAnchor:self.view.centerXAnchor constant: 0], //mark: settingMenuLayout
|
||
[_parentStack.widthAnchor constraintEqualToAnchor:self.view.widthAnchor constant:-20] // section width adjusted here //mark: settingMenuLayout
|
||
]];
|
||
}
|
||
|
||
- (UIButton* )findInfoButtonFromStack:(UIStackView* )stack{
|
||
UIButton* button;
|
||
for(UIView *view in stack.subviews){
|
||
if([view isKindOfClass:[UIButton class]] && [view.accessibilityIdentifier isEqualToString:@"infoButton"]) button = (UIButton* )view;
|
||
}
|
||
return button;
|
||
}
|
||
|
||
- (UILabel* )findDynamicLabelFromStack:(UIStackView* )stack{
|
||
UILabel* label;
|
||
for(UIView *view in stack.subviews){
|
||
if([view isKindOfClass:[UILabel class]] && [view.accessibilityIdentifier isEqualToString:@"dynamicLabel"]) label = (UILabel* )view;
|
||
}
|
||
return label;
|
||
}
|
||
|
||
- (void)hideOverlappedDynamicLabelWithinStack:(UIStackView* )stack{
|
||
UILabel* label1 = [self findDynamicLabelFromStack:stack];
|
||
for(UIView* view in stack.subviews){
|
||
if([view isKindOfClass:[UILabel class]] && view != label1){
|
||
UILabel* label2 = (UILabel* )view;
|
||
CGRect textRect1 = [label1 textRectForBounds:label1.bounds limitedToNumberOfLines:label1.numberOfLines];
|
||
CGRect textRect2 = [label2 textRectForBounds:label2.bounds limitedToNumberOfLines:label2.numberOfLines];
|
||
CGRect textRect1InLabel2 = [label2 convertRect:textRect1 fromView:label1];
|
||
if(CGRectIntersectsRect(textRect1InLabel2, textRect2)){
|
||
label1.hidden = YES;return;
|
||
}
|
||
}
|
||
}
|
||
label1.hidden = NO;
|
||
}
|
||
|
||
- (UIView *)findCommonSuperViewFor:(UIView *)view1 andView:(UIView *)view2 {
|
||
if (!view1 || !view2) return nil;
|
||
// 把 view1 的所有 superview 放入集合
|
||
NSMutableSet<UIView *> *superviewsOfView1 = [NSMutableSet set];
|
||
UIView *currentView = view1;
|
||
while (currentView) {
|
||
[superviewsOfView1 addObject:currentView];
|
||
currentView = currentView.superview;
|
||
}
|
||
// 向上遍历 view2,找到第一个也在 view1 superview 集合里的视图
|
||
currentView = view2;
|
||
while (currentView) {
|
||
if ([superviewsOfView1 containsObject:currentView]) {
|
||
return currentView;
|
||
}
|
||
currentView = currentView.superview;
|
||
}
|
||
return nil; // 没有共同父视图(一般不会发生,除非来自不同的视图层次结构)
|
||
}
|
||
|
||
- (void)hideOverlappedDynamicLabels{
|
||
[self hideDynamicLabelsWhenOverlapped:self.parentStack];
|
||
}
|
||
|
||
- (void)hideDynamicLabelsWhenOverlapped:(UIView* )view{
|
||
for (UIView *subview in view.subviews) {
|
||
if ([subview isKindOfClass:[UIStackView class]]) {
|
||
UIStackView *stack = (UIStackView *)subview;
|
||
if(stack.accessibilityIdentifier != nil) {
|
||
// NSLog(@"found stack: %@", stack.accessibilityIdentifier);
|
||
[self hideOverlappedDynamicLabelWithinStack:stack];
|
||
}
|
||
}
|
||
[self hideDynamicLabelsWhenOverlapped:subview];
|
||
}
|
||
}
|
||
|
||
- (void)addDynamicLabelForStack:(UIStackView* )stack{
|
||
UILabel* label = [self findDynamicLabelFromStack:stack];
|
||
if(label) return;
|
||
|
||
UIButton* button = [self findInfoButtonFromStack:stack];
|
||
label = [[UILabel alloc] initWithFrame:CGRectMake(0, 0, 20, 50)];
|
||
label.textAlignment = NSTextAlignmentCenter;
|
||
// label.adjustsFontSizeToFitWidth = YES;
|
||
label.accessibilityIdentifier = @"dynamicLabel";
|
||
label.textColor = [ThemeManager appPrimaryColor];
|
||
label.font = [UIFont systemFontOfSize:16 weight:UIFontWeightMedium];
|
||
label.translatesAutoresizingMaskIntoConstraints = NO;
|
||
[stack addSubview:label];
|
||
if(button){
|
||
[NSLayoutConstraint activateConstraints:@[
|
||
[label.trailingAnchor constraintEqualToAnchor:button.leadingAnchor constant:-5],
|
||
//[label.centerYAnchor constraintEqualToAnchor:stack.arrangedSubviews[0].centerYAnchor],
|
||
[label.bottomAnchor constraintEqualToAnchor:stack.arrangedSubviews[0].bottomAnchor],
|
||
[label.heightAnchor constraintEqualToAnchor:stack.arrangedSubviews[0].heightAnchor],
|
||
[label.widthAnchor constraintEqualToConstant:150],
|
||
]];
|
||
}
|
||
else{
|
||
[NSLayoutConstraint activateConstraints:@[
|
||
[label.trailingAnchor constraintEqualToAnchor:stack.trailingAnchor constant:-5],
|
||
[label.heightAnchor constraintEqualToAnchor:stack.arrangedSubviews[0].heightAnchor],
|
||
[label.widthAnchor constraintEqualToConstant:150],
|
||
]];
|
||
|
||
}
|
||
}
|
||
|
||
- (void)addSetting:(UIStackView *)stack ofId:(NSString* )identifier withInfoTag:(BOOL)attched withDynamicLabel:(BOOL)added to:(MenuSectionView* )menuSection{
|
||
stack.accessibilityIdentifier = identifier;
|
||
[_settingStackDict setObject:stack forKey:identifier];
|
||
if(attched) [self attachInfoTagForStack:stack];
|
||
if(added) [self addDynamicLabelForStack:stack];
|
||
[menuSection addSubStackView:stack];
|
||
}
|
||
|
||
- (void)layoutSections{
|
||
|
||
|
||
MenuSectionView *videoSection = [[MenuSectionView alloc] init];
|
||
videoSection.delegate = self;
|
||
videoSection.sectionTitle = [LocalizationHelper localizedStringForKey:@"Video"];
|
||
if (@available(iOS 13.0, *)) {
|
||
[videoSection setSectionWithIcon:[UIImage systemImageNamed:@"waveform"] andSize:20];
|
||
}
|
||
[self addSetting:self.resolutionStack ofId:@"resolutionStack" withInfoTag:YES withDynamicLabel:YES to:videoSection];
|
||
[self addSetting:self.fpsStack ofId:@"fpsStack" withInfoTag:NO withDynamicLabel:NO to:videoSection];
|
||
[self addSetting:self.bitrateStack ofId:@"bitrateStack" withInfoTag:YES withDynamicLabel:YES to:videoSection];
|
||
[self addSetting:self.framepacingStack ofId:@"framepacingStack" withInfoTag:NO withDynamicLabel:NO to:videoSection];
|
||
[self addSetting:self.codecStack ofId:@"codecStack" withInfoTag:NO withDynamicLabel:NO to:videoSection];
|
||
[self addSetting:self.HdrStack ofId:@"HdrStack" withInfoTag:NO withDynamicLabel:NO to:videoSection];
|
||
[self addSetting:self.yuv444Stack ofId:@"yuv444Stack" withInfoTag:YES withDynamicLabel:NO to:videoSection];
|
||
[videoSection addToParentStack:_parentStack];
|
||
[videoSection setExpanded:YES];
|
||
|
||
touchControlSection = [[MenuSectionView alloc] init];
|
||
touchControlSection.delegate = self;
|
||
touchControlSection.sectionTitle = [LocalizationHelper localizedStringForKey:@"Touch Control"];
|
||
if (@available(iOS 13.0, *)) {
|
||
[touchControlSection setSectionWithIcon:[UIImage imageNamed:@"arcade.stick.console"] andSize:20.5];
|
||
}
|
||
[self addSetting:self.touchModeStack ofId:@"touchModeStack" withInfoTag:YES withDynamicLabel:NO to:touchControlSection];
|
||
[self addSetting:self.pointerVelocityDividerStack ofId:@"pointerVelocityDividerStack" withInfoTag:YES withDynamicLabel:YES to:touchControlSection];
|
||
[self addSetting:self.pointerVelocityFactorStack ofId:@"pointerVelocityFactorStack" withInfoTag:YES withDynamicLabel:YES to:touchControlSection];
|
||
[self addSetting:self.mousePointerVelocityStack ofId:@"mousePointerVelocityStack" withInfoTag:NO withDynamicLabel:YES to:touchControlSection];
|
||
[self addSetting:self.onScreenWidgetStack ofId:@"onScreenWidgetStack" withInfoTag:YES withDynamicLabel:YES to:touchControlSection];
|
||
[self addSetting:self.swapAbaxyStack ofId:@"swapAbaxyStack" withInfoTag:NO withDynamicLabel:NO to:touchControlSection];
|
||
[touchControlSection addToParentStack:_parentStack];
|
||
[touchControlSection setExpanded:YES];
|
||
|
||
|
||
MenuSectionView *gesturesSection = [[MenuSectionView alloc] init];
|
||
gesturesSection.delegate = self;
|
||
gesturesSection.sectionTitle = [LocalizationHelper localizedStringForKey:@"Gestures"];
|
||
if (@available(iOS 13.0, *)) {
|
||
[gesturesSection setSectionWithIcon:[UIImage systemImageNamed:@"hand.draw"] andSize:23];
|
||
}
|
||
|
||
[self addSetting:self.softKeyboardGestureStack ofId:@"softKeyboardGestureStack" withInfoTag:YES withDynamicLabel:NO to:gesturesSection];
|
||
[self addSetting:self.slideToSettingsScreenEdgeStack ofId:@"slideToSettingsScreenEdgeStack" withInfoTag:NO withDynamicLabel:NO to:gesturesSection];
|
||
[self addSetting:self.slideToToolboxScreenEdgeStack ofId:@"slideToToolboxScreenEdgeStack" withInfoTag:NO withDynamicLabel:NO to:gesturesSection];
|
||
[self addSetting:self.slideToSettingsDistanceStack ofId:@"slideToSettingsDistanceStack" withInfoTag:YES withDynamicLabel:YES to:gesturesSection];
|
||
[gesturesSection addToParentStack:_parentStack];
|
||
[gesturesSection setExpanded:YES];
|
||
|
||
MenuSectionView *peripheralSection = [[MenuSectionView alloc] init];
|
||
peripheralSection.delegate = self;
|
||
peripheralSection.sectionTitle = [LocalizationHelper localizedStringForKey:@"Peripherals"];
|
||
if (@available(iOS 13.0, *)) {
|
||
[peripheralSection setSectionWithIcon:[UIImage imageNamed:@"cable.connector.video"] andSize:20];
|
||
}
|
||
[self addSetting:self.externalDisplayModeStack ofId:@"externalDisplayModeStack" withInfoTag:YES withDynamicLabel:NO to:peripheralSection];
|
||
[self addSetting:self.localMousePointerModeStack ofId:@"localMousePointerModeStack" withInfoTag:YES withDynamicLabel:NO to:peripheralSection];
|
||
[self addSetting:self.reverseMouseWheelDirectionStack ofId:@"reverseMouseWheelDirectionStack" withInfoTag:NO withDynamicLabel:NO to:peripheralSection];
|
||
[self addSetting:self.citrixX1MouseStack ofId:@"citrixX1MouseStack" withInfoTag:NO withDynamicLabel:NO to:peripheralSection];
|
||
[peripheralSection addToParentStack:_parentStack];
|
||
[peripheralSection setExpanded:YES];
|
||
|
||
|
||
|
||
MenuSectionView *audioSection = [[MenuSectionView alloc] init];
|
||
audioSection.delegate = self;
|
||
audioSection.sectionTitle = [LocalizationHelper localizedStringForKey:@"Audio"];
|
||
if (@available(iOS 13.0, *)) {
|
||
[audioSection setSectionWithIcon:[UIImage systemImageNamed:@"speaker.wave.2"] andSize:23];
|
||
}
|
||
|
||
[self addSetting:self.audioOnPcStack ofId:@"audioOnPcStack" withInfoTag:NO withDynamicLabel:NO to:audioSection];
|
||
[audioSection addToParentStack:_parentStack];
|
||
[audioSection setExpanded:YES];
|
||
|
||
|
||
MenuSectionView *otherSection = [[MenuSectionView alloc] init];
|
||
otherSection.delegate = self;
|
||
otherSection.sectionTitle = [LocalizationHelper localizedStringForKey:@"Others"];
|
||
if (@available(iOS 13.0, *)) {
|
||
[otherSection setSectionWithIcon:[UIImage systemImageNamed:@"cube"] andSize:20.5];
|
||
}
|
||
[self addSetting:self.statsOverlayStack ofId:@"statsOverlayStack" withInfoTag:NO withDynamicLabel:NO to:otherSection];
|
||
[self addSetting:self.unlockDisplayOrientationStack ofId:@"unlockDisplayOrientationStack" withInfoTag:YES withDynamicLabel:NO to:otherSection];
|
||
[self addSetting:self.optimizeGamesStack ofId:@"optimizeGamesStack" withInfoTag:YES withDynamicLabel:NO to:otherSection];
|
||
[self addSetting:self.multiControllerStack ofId:@"multiControllerStack" withInfoTag:YES withDynamicLabel:NO to:otherSection];
|
||
[self addSetting:self.softKeyboardToolbarStack ofId:@"softKeyboardToolbarStack" withInfoTag:YES withDynamicLabel:NO to:otherSection];
|
||
[otherSection addToParentStack:_parentStack];
|
||
[otherSection setExpanded:YES];
|
||
|
||
|
||
MenuSectionView *experimentalSection = [[MenuSectionView alloc] init];
|
||
experimentalSection.delegate = self;
|
||
experimentalSection.sectionTitle = [LocalizationHelper localizedStringForKey:@"Experimental"];
|
||
if (@available(iOS 13.0, *)) {
|
||
[experimentalSection setSectionWithIcon:[UIImage imageNamed:@"flask"] andSize:20];
|
||
}
|
||
// [self addSetting:self.asyncTouchStack ofId:@"asyncTouchStack" withInfoTag:YES withDynamicLabel:NO to:experimentalSection];
|
||
[self addSetting:self.touchMoveEventIntervalStack ofId:@"touchMoveEventIntervalStack" withInfoTag:YES withDynamicLabel:YES to:experimentalSection];
|
||
[experimentalSection addToParentStack:_parentStack];
|
||
[experimentalSection setExpanded:YES];
|
||
}
|
||
|
||
|
||
- (void)layoutWidgetes {
|
||
|
||
// [self.view addSubview:self.navigationBar];
|
||
|
||
self.scrollView.contentInsetAdjustmentBehavior = UIScrollViewContentInsetAdjustmentAutomatic;
|
||
|
||
// 可选:确保 scrollView 开启垂直滚动
|
||
self.scrollView.alwaysBounceVertical = YES;
|
||
|
||
UIStackView* parentStackTmp = [[UIStackView alloc] init];
|
||
parentStackTmp.axis = UILayoutConstraintAxisVertical;
|
||
parentStackTmp.spacing = 13;
|
||
parentStackTmp.translatesAutoresizingMaskIntoConstraints = NO;
|
||
[self.scrollView addSubview:parentStackTmp];
|
||
[NSLayoutConstraint activateConstraints:@[
|
||
[parentStackTmp.topAnchor constraintEqualToAnchor:self.scrollView.contentLayoutGuide.topAnchor constant: [self getStandardNavBarHeight] + 300],
|
||
[parentStackTmp.bottomAnchor constraintEqualToAnchor:self.scrollView.contentLayoutGuide.bottomAnchor constant:-20],
|
||
[parentStackTmp.leadingAnchor constraintEqualToAnchor:self.scrollView.contentLayoutGuide.leadingAnchor constant: 14],
|
||
[parentStackTmp.trailingAnchor constraintEqualToAnchor:self.scrollView.contentLayoutGuide.trailingAnchor constant: -15],
|
||
[parentStackTmp.widthAnchor constraintEqualToAnchor:self.scrollView.contentLayoutGuide.widthAnchor]
|
||
]];
|
||
|
||
|
||
[parentStackTmp addArrangedSubview:self.resolutionStack];
|
||
// self.resolutionStack.hidden = YES;
|
||
[parentStackTmp addArrangedSubview:self.fpsStack];
|
||
// self.fpsStack.hidden = YES;
|
||
[parentStackTmp addArrangedSubview:self.bitrateStack];
|
||
// self.bitrateStack.hidden = YES;
|
||
[parentStackTmp addArrangedSubview:self.touchModeStack];
|
||
// [parentStackTmp addArrangedSubview:self.asyncTouchStack];
|
||
[parentStackTmp addArrangedSubview:self.pointerVelocityDividerStack];
|
||
[parentStackTmp addArrangedSubview:self.pointerVelocityFactorStack];
|
||
[parentStackTmp addArrangedSubview:self.touchMoveEventIntervalStack];
|
||
[parentStackTmp addArrangedSubview:self.mousePointerVelocityStack];
|
||
[parentStackTmp addArrangedSubview:self.onScreenWidgetStack];
|
||
[parentStackTmp addArrangedSubview:self.softKeyboardGestureStack];
|
||
[parentStackTmp addArrangedSubview:self.liftStreamViewForKeyboardStack];
|
||
[parentStackTmp addArrangedSubview:self.softKeyboardToolbarStack];
|
||
[parentStackTmp addArrangedSubview:self.slideToSettingsScreenEdgeStack];
|
||
[parentStackTmp addArrangedSubview:self.slideToToolboxScreenEdgeStack];
|
||
[parentStackTmp addArrangedSubview:self.slideToSettingsDistanceStack];
|
||
[parentStackTmp addArrangedSubview:self.optimizeGamesStack];
|
||
[parentStackTmp addArrangedSubview:self.multiControllerStack];
|
||
[parentStackTmp addArrangedSubview:self.swapAbaxyStack];
|
||
[parentStackTmp addArrangedSubview:self.audioOnPcStack];
|
||
[parentStackTmp addArrangedSubview:self.codecStack];
|
||
[parentStackTmp addArrangedSubview:self.yuv444Stack];
|
||
[parentStackTmp addArrangedSubview:self.HdrStack];
|
||
[parentStackTmp addArrangedSubview:self.framepacingStack];
|
||
[parentStackTmp addArrangedSubview:self.reverseMouseWheelDirectionStack];
|
||
[parentStackTmp addArrangedSubview:self.citrixX1MouseStack];
|
||
[parentStackTmp addArrangedSubview:self.statsOverlayStack];
|
||
[parentStackTmp addArrangedSubview:self.unlockDisplayOrientationStack];
|
||
[parentStackTmp addArrangedSubview:self.externalDisplayModeStack];
|
||
[parentStackTmp addArrangedSubview:self.localMousePointerModeStack];
|
||
[parentStackTmp removeFromSuperview];
|
||
}
|
||
|
||
- (void)handleAutoScroll:(CGPoint)location{
|
||
bool scrollDown = location.y > self.view.bounds.size.height - 100;
|
||
bool scrollUp = location.y < 150;
|
||
|
||
//NSLog(@"%f flag: %d, %d, obj: %@, locY: %f", CACurrentMediaTime(), scrollUp, scrollDown, _autoScrollDisplayLink, location.y);
|
||
|
||
if(!(scrollUp||scrollDown)) [self stopAutoScroll];
|
||
|
||
if((scrollUp||scrollDown) && _autoScrollDisplayLink == nil ){
|
||
|
||
// NSLog(@"_autoScrollDisplayLink: %@", _autoScrollDisplayLink);
|
||
//if (!_autoScrollDisplayLink) {
|
||
//[_autoScrollDisplayLink ]
|
||
_scrollSpeed = fabs(120/_currentRefreshRate);
|
||
// _scrollSpeed = 2;
|
||
CGFloat scrollDirection = 0;
|
||
if(scrollDown) scrollDirection = 1;
|
||
if(scrollUp) scrollDirection = -1;
|
||
_scrollSpeed = _scrollSpeed * scrollDirection;
|
||
//NSLog(@"%f, scrollSpeed: %f", CACurrentMediaTime(), _scrollSpeed);
|
||
|
||
_autoScrollDisplayLink = [CADisplayLink displayLinkWithTarget:self selector:@selector(startScroll)];
|
||
[_autoScrollDisplayLink addToRunLoop:[NSRunLoop mainRunLoop] forMode:NSRunLoopCommonModes];
|
||
}
|
||
|
||
//}
|
||
}
|
||
|
||
- (void)stopAutoScroll {
|
||
[_autoScrollDisplayLink invalidate];
|
||
_autoScrollDisplayLink = nil;
|
||
}
|
||
|
||
- (BOOL)scrolledToTop {
|
||
return self.scrollView.contentOffset.y <= 0;
|
||
}
|
||
|
||
- (BOOL)scrolledToBottom {
|
||
CGFloat maxOffsetY = self.scrollView.contentSize.height - self.scrollView.bounds.size.height;
|
||
return self.scrollView.contentOffset.y >= maxOffsetY + snapshot.bounds.size.height+50;
|
||
}
|
||
|
||
|
||
- (void)startScroll {
|
||
|
||
if(![self scrolledToTop] && ![self scrolledToBottom]){
|
||
CGPoint snapshotLocation = snapshot.center;
|
||
snapshotLocation = CGPointMake(snapshotLocation.x, snapshotLocation.y+_scrollSpeed);
|
||
snapshot.center = snapshotLocation;
|
||
}
|
||
|
||
CGPoint offset = self.scrollView.contentOffset;
|
||
CGFloat newY = offset.y + _scrollSpeed;
|
||
|
||
// 限制滚动范围
|
||
newY = MAX(0, MIN(newY, self.scrollView.contentSize.height - self.scrollView.bounds.size.height+snapshot.bounds.size.height+50));
|
||
|
||
[self.scrollView setContentOffset:CGPointMake(offset.x, newY) animated:NO];
|
||
[self updateRelocationIndicatorFor:snapshot.center];
|
||
}
|
||
|
||
- (void)estimateFPSWithCompletion:(void (^)(CGFloat fps))completion {
|
||
CADisplayLink *link = [CADisplayLink displayLinkWithTarget:self selector:@selector(handleTick:)];
|
||
|
||
// 让系统决定刷新率
|
||
link.preferredFramesPerSecond = 0;
|
||
|
||
// 关联block和时间戳
|
||
objc_setAssociatedObject(link, @"fpsCompletion", completion, OBJC_ASSOCIATION_COPY_NONATOMIC);
|
||
objc_setAssociatedObject(link, @"lastTimestamp", @(0), OBJC_ASSOCIATION_RETAIN_NONATOMIC);
|
||
|
||
[link addToRunLoop:[NSRunLoop mainRunLoop] forMode:NSRunLoopCommonModes];
|
||
}
|
||
|
||
- (void)handleTick:(CADisplayLink *)link {
|
||
NSTimeInterval lastTimestamp = [objc_getAssociatedObject(link, @"lastTimestamp") doubleValue];
|
||
void (^completion)(CGFloat fps) = objc_getAssociatedObject(link, @"fpsCompletion");
|
||
|
||
if (lastTimestamp > 0) {
|
||
NSTimeInterval delta = link.timestamp - lastTimestamp;
|
||
CGFloat fps = 1.0 / delta;
|
||
|
||
// 先停止CADisplayLink,避免继续调用
|
||
[link invalidate];
|
||
link = nil;
|
||
|
||
if (completion) {
|
||
completion(fps);
|
||
}
|
||
|
||
// 清理关联,防止内存泄漏
|
||
objc_setAssociatedObject(link, @"fpsCompletion", nil, OBJC_ASSOCIATION_COPY_NONATOMIC);
|
||
objc_setAssociatedObject(link, @"lastTimestamp", nil, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
|
||
} else {
|
||
objc_setAssociatedObject(link, @"lastTimestamp", @(link.timestamp), OBJC_ASSOCIATION_RETAIN_NONATOMIC);
|
||
}
|
||
}
|
||
|
||
|
||
|
||
|
||
- (NSInteger)parentStackIndexForLocation:(CGPoint)location {
|
||
for (NSInteger i = _parentStack.arrangedSubviews.count-1; i >=0; i--) {
|
||
UIView *subview = _parentStack.arrangedSubviews[i];
|
||
// CGRect frame = [subview convertRect:subview.bounds toView:parentStack];
|
||
CGFloat stackMinY = CGRectGetMinY(subview.frame);
|
||
// NSLog(@" index: %ld, stackY: %f, touchY: %f", (long)i, CGRectGetMidY(subview.frame), location.y);
|
||
if(stackMinY < location.y) return i;
|
||
}
|
||
return 0;
|
||
}
|
||
|
||
- (void)highlightedPurpleBackgroundForView:(UIView* )view{
|
||
view.layer.cornerRadius = 6;
|
||
view.layer.masksToBounds = YES;
|
||
view.clipsToBounds = YES;
|
||
view.backgroundColor = [UIColor colorWithRed:0.5 green:0.5 blue:1 alpha:0.35];
|
||
}
|
||
|
||
- (void)highlightedBackgroundForView:(UIView* )view{
|
||
view.layer.cornerRadius = 6;
|
||
view.layer.masksToBounds = YES;
|
||
view.clipsToBounds = YES;
|
||
view.backgroundColor = [ThemeManager appPrimaryColorWithAlpha];
|
||
}
|
||
|
||
|
||
- (void)updateRelocationIndicatorFor:(CGPoint)locationInParentStack{
|
||
uint16_t currentIndex = [self parentStackIndexForLocation:locationInParentStack];
|
||
UIStackView* currentStack;
|
||
for (NSInteger i = _parentStack.arrangedSubviews.count-1; i >=0; i--) {
|
||
currentStack = _parentStack.arrangedSubviews[i];
|
||
|
||
if(currentIndex == i){
|
||
settingStackWillBeRelocatedToLowestPosition = false;
|
||
if(i == _parentStack.arrangedSubviews.count-1 && locationInParentStack.y > CGRectGetMaxY(currentStack.frame)){
|
||
[self highlightedPurpleBackgroundForView:snapshot];
|
||
currentStack.backgroundColor = [UIColor clearColor];
|
||
settingStackWillBeRelocatedToLowestPosition = true;
|
||
}
|
||
else{
|
||
[self highlightedBackgroundForView:currentStack];
|
||
snapshot.backgroundColor = [UIColor clearColor];
|
||
}
|
||
}
|
||
else{
|
||
currentStack.layer.cornerRadius = 0;
|
||
currentStack.backgroundColor = [UIColor clearColor];
|
||
}
|
||
}
|
||
}
|
||
|
||
- (void)clearRelocationIndicator{
|
||
for (UIView* view in _parentStack.arrangedSubviews) {
|
||
view.layer.cornerRadius = 0;
|
||
view.backgroundColor = [UIColor clearColor];
|
||
}
|
||
}
|
||
|
||
- (void)findCapturedStackByTouchLocation:(CGPoint)point{
|
||
UIView *touchedView = [_parentStack hitTest:point withEvent:nil];
|
||
while(touchedView){
|
||
NSLog(@"view captured: %@, %@", touchedView, touchedView.accessibilityIdentifier);
|
||
if([touchedView isKindOfClass:[UIStackView class]] && touchedView.accessibilityIdentifier != nil){
|
||
capturedStack = (UIStackView *)touchedView;
|
||
break;
|
||
}
|
||
touchedView = touchedView.superview;
|
||
}
|
||
}
|
||
|
||
- (UIAlertController* )prepareAddToFavoriteActionSheet{
|
||
// actionsheet
|
||
UIAlertController *actionSheet = [UIAlertController alertControllerWithTitle:nil
|
||
message:nil
|
||
preferredStyle:UIAlertControllerStyleActionSheet];
|
||
UIAlertAction *addFavoriteAction = [UIAlertAction actionWithTitle:[LocalizationHelper localizedStringForKey:@"Add to favorite"]
|
||
style:UIAlertActionStyleDefault
|
||
handler:^(UIAlertAction * _Nonnull action) {
|
||
[self addSettingToFavorite:self->capturedStack];
|
||
self->capturedStack.backgroundColor = [UIColor clearColor];
|
||
}];
|
||
UIAlertAction *cancelAction = [UIAlertAction actionWithTitle:[self isIPhone] ? [LocalizationHelper localizedStringForKey:@"Cancel"] : @""
|
||
style:UIAlertActionStyleCancel
|
||
handler:^(UIAlertAction * _Nonnull action) {
|
||
self->capturedStack.backgroundColor = [UIColor clearColor];
|
||
}];
|
||
|
||
[actionSheet addAction:addFavoriteAction];
|
||
[actionSheet addAction:cancelAction];
|
||
return actionSheet;
|
||
}
|
||
|
||
- (void)handleLongPress:(UILongPressGestureRecognizer *)gesture {
|
||
CGPoint locationInParentStack = [gesture locationInView:_parentStack];
|
||
CGPoint locationInRootView = [gesture locationInView:self.view.superview];
|
||
if(currentSettingsMenuMode == AllSettings &&gesture.state == UIGestureRecognizerStateBegan) {
|
||
[self findCapturedStackByTouchLocation:locationInParentStack];
|
||
if(capturedStack == nil) return;
|
||
[self highlightedBackgroundForView:capturedStack];
|
||
UIAlertController* actionSheet = [self prepareAddToFavoriteActionSheet];
|
||
actionSheet.popoverPresentationController.sourceView = capturedStack;
|
||
[self presentViewController:actionSheet animated:YES completion:nil];
|
||
}
|
||
|
||
|
||
static CGPoint originalCenter;
|
||
static NSInteger originalIndex;
|
||
if(gesture.state == UIGestureRecognizerStateBegan){
|
||
|
||
[self estimateFPSWithCompletion:^(CGFloat fps) {
|
||
self->_currentRefreshRate = fps;
|
||
}];
|
||
_autoScrollDisplayLink = nil;
|
||
}
|
||
|
||
if(currentSettingsMenuMode == FavoriteSettings){
|
||
switch (gesture.state) {
|
||
case UIGestureRecognizerStateBegan:
|
||
// 创建快照视图
|
||
[self findCapturedStackByTouchLocation:locationInParentStack];
|
||
if(capturedStack == nil) return;
|
||
|
||
snapshot = [capturedStack snapshotViewAfterScreenUpdates:YES];
|
||
//snapshot.center = capturedStack.center;
|
||
snapshot.center = locationInParentStack;
|
||
[_parentStack addSubview:snapshot];
|
||
capturedStack.hidden = YES;
|
||
originalCenter = capturedStack.center;
|
||
originalIndex = [_parentStack.arrangedSubviews indexOfObject:capturedStack];
|
||
break;
|
||
|
||
case UIGestureRecognizerStateChanged:
|
||
snapshot.center = locationInParentStack;
|
||
// NSLog(@"coordY in rootView: %f", locationInRootView.y);
|
||
[self handleAutoScroll:locationInRootView];
|
||
[self updateRelocationIndicatorFor:locationInParentStack];
|
||
|
||
|
||
break;
|
||
case UIGestureRecognizerStateCancelled:
|
||
case UIGestureRecognizerStateEnded:
|
||
// 更新快照视图位置
|
||
[self stopAutoScroll];
|
||
[snapshot removeFromSuperview];
|
||
snapshot = nil;
|
||
// 计算新的插入位置
|
||
NSInteger newIndex = [self parentStackIndexForLocation:locationInParentStack];
|
||
[self clearRelocationIndicator];
|
||
NSInteger oldIndex = [_parentStack.arrangedSubviews indexOfObject:capturedStack];
|
||
newIndex = newIndex > oldIndex ? newIndex - 1 : newIndex;
|
||
//NSLog(@"newidx %ld, oldidx %ld", newIndex, oldIndex);
|
||
if(settingStackWillBeRelocatedToLowestPosition){
|
||
newIndex = newIndex + 1;
|
||
settingStackWillBeRelocatedToLowestPosition = false;
|
||
}
|
||
|
||
if (newIndex != NSNotFound) {
|
||
if(newIndex >= _parentStack.arrangedSubviews.count) newIndex = _parentStack.arrangedSubviews.count-1;
|
||
[_parentStack removeArrangedSubview:capturedStack];
|
||
[_parentStack insertArrangedSubview:capturedStack atIndex:newIndex];
|
||
// [parentStack addSubview:capturedStack];
|
||
originalIndex = newIndex;
|
||
capturedStack.hidden = NO;
|
||
[self saveFavoriteSettingStackIdentifiers];
|
||
}
|
||
// 移除快照视图,显示原始视图
|
||
break;
|
||
|
||
default:break;
|
||
}
|
||
}
|
||
}
|
||
|
||
|
||
- (void)addSettingToFavorite:(UIStackView* )settingStack{
|
||
|
||
if([_favoriteSettingStackIdentifiers containsObject:settingStack.accessibilityIdentifier]) return;
|
||
|
||
[_favoriteSettingStackIdentifiers addObject:settingStack.accessibilityIdentifier];
|
||
for(NSString *identifier in _favoriteSettingStackIdentifiers){
|
||
NSLog(@"favorite setting: %@", identifier);
|
||
}
|
||
|
||
[self saveFavoriteSettingStackIdentifiers];
|
||
}
|
||
|
||
- (void)attachRemoveButtonForStack:(UIStackView* )stack{
|
||
UIButton* button = [UIButton buttonWithType:UIButtonTypeSystem];
|
||
if (@available(iOS 13.0, *)) {
|
||
UIImageSymbolConfiguration *config = [UIImageSymbolConfiguration configurationWithPointSize:16 weight:UIImageSymbolWeightMedium];
|
||
[button setImage:[UIImage systemImageNamed:@"minus.circle" withConfiguration:config] forState:UIControlStateNormal];
|
||
} else {
|
||
[button setTitle:[LocalizationHelper localizedStringForKey:@"Remove"] forState:UIControlStateNormal];
|
||
}
|
||
button.accessibilityIdentifier = @"removeButton";
|
||
button.translatesAutoresizingMaskIntoConstraints = NO;
|
||
button.tintColor = [UIColor redColor];
|
||
|
||
[button addTarget:self action:@selector(removeSettingStackFromFavorites:) forControlEvents:UIControlEventTouchUpInside];
|
||
|
||
[stack addSubview:button];
|
||
[NSLayoutConstraint activateConstraints:@[
|
||
[button.trailingAnchor constraintEqualToAnchor:stack.trailingAnchor constant:-8],
|
||
[button.topAnchor constraintEqualToAnchor:stack.topAnchor],
|
||
]];
|
||
}
|
||
|
||
- (void)attachInfoTagForStack:(UIStackView* )stack{
|
||
UIButton* button = [self findInfoButtonFromStack:stack];
|
||
if(button) return;
|
||
button = [UIButton buttonWithType:UIButtonTypeSystem];
|
||
if (@available(iOS 13.0, *)) {
|
||
UIImageSymbolConfiguration *config = [UIImageSymbolConfiguration configurationWithPointSize:13.5 weight:UIImageSymbolWeightBold];
|
||
[button setImage:[UIImage systemImageNamed:@"info.circle" withConfiguration:config] forState:UIControlStateNormal];
|
||
} else {
|
||
[button setTitle:@"info" forState:UIControlStateNormal];
|
||
button.titleLabel.font = [UIFont systemFontOfSize:13 weight:UIFontWeightBold];
|
||
}
|
||
button.accessibilityIdentifier = @"infoButton";
|
||
button.translatesAutoresizingMaskIntoConstraints = NO;
|
||
button.tintColor = [ThemeManager appPrimaryColor];
|
||
|
||
[button addTarget:self action:@selector(infoButtonTapped:) forControlEvents:UIControlEventTouchUpInside];
|
||
|
||
[stack addSubview:button];
|
||
[NSLayoutConstraint activateConstraints:@[
|
||
[button.trailingAnchor constraintEqualToAnchor:stack.trailingAnchor constant:-4],
|
||
[button.bottomAnchor constraintEqualToAnchor: stack.arrangedSubviews[0].bottomAnchor constant:0],
|
||
]];
|
||
}
|
||
|
||
- (void)infoButtonTapped:(UIButton* )sender{
|
||
|
||
NSString* tipText = [NSString stringWithFormat:@"dummy text for setting stack: %@", sender.superview.accessibilityIdentifier];
|
||
|
||
UIAlertController *tipsAlertController = [UIAlertController alertControllerWithTitle: [LocalizationHelper localizedStringForKey:@"Tips"] message: [LocalizationHelper localizedStringForKey:@"%@", tipText] preferredStyle:UIAlertControllerStyleAlert];
|
||
|
||
UIAlertAction *readInstruction = [UIAlertAction actionWithTitle:[LocalizationHelper localizedStringForKey:@"Online Documentation"]
|
||
style:UIAlertActionStyleDefault
|
||
handler:^(UIAlertAction *action){
|
||
NSURL *url = [NSURL URLWithString:@"https://b23.tv/J8qEXOr"];
|
||
if ([[UIApplication sharedApplication] canOpenURL:url]) {
|
||
[[UIApplication sharedApplication] openURL:url options:@{} completionHandler:nil];
|
||
}
|
||
}];
|
||
|
||
UIAlertAction *okAction = [UIAlertAction actionWithTitle:[LocalizationHelper localizedStringForKey:@"OK"]
|
||
style:UIAlertActionStyleDefault
|
||
handler:nil];
|
||
[tipsAlertController addAction:readInstruction];
|
||
[tipsAlertController addAction:okAction];
|
||
[self presentViewController:tipsAlertController animated:YES completion:nil];
|
||
}
|
||
|
||
|
||
|
||
- (void)removeSettingStackFromFavorites:(UIButton* )sender{
|
||
[sender.superview removeFromSuperview];
|
||
[sender removeFromSuperview];
|
||
[self saveFavoriteSettingStackIdentifiers];
|
||
}
|
||
|
||
- (void)layoutSettingsView{
|
||
[self.scrollView layoutSubviews];
|
||
|
||
//switchToAll/Favorite 调用此方法时,这些hiddenStack已身处新的superView中, 可以正常执行hidden = YES
|
||
for(UIStackView* stack in hiddenStacks) stack.hidden = YES;
|
||
|
||
if(currentSettingsMenuMode == AllSettings){
|
||
for(MenuSectionView* section in _parentStack.arrangedSubviews) [section updateViewForFoldState];
|
||
}
|
||
[self hideDynamicLabelsWhenOverlapped:self.parentStack];
|
||
}
|
||
|
||
// 旧版本iOS兼容必要
|
||
- (void)forceRestoreHeightTemporarilyForSettingStackParentView{
|
||
for(UIStackView* stack in hiddenStacks) stack.hidden = NO;
|
||
if(currentSettingsMenuMode == AllSettings){
|
||
for(UIView* view in _parentStack.arrangedSubviews){
|
||
if([view isKindOfClass:[MenuSectionView class]]){
|
||
MenuSectionView* section = (MenuSectionView* )view;
|
||
[section updateViewForFoldState];
|
||
}
|
||
}
|
||
}
|
||
else{
|
||
NSLayoutConstraint* heightConstraint = [_parentStack.heightAnchor constraintEqualToConstant:666];
|
||
heightConstraint.active = YES;
|
||
CGSize fittingSize = [_parentStack systemLayoutSizeFittingSize:UILayoutFittingCompressedSize];
|
||
heightConstraint.constant = fittingSize.height;
|
||
}
|
||
}
|
||
|
||
- (void)switchToFavoriteSettings{
|
||
[self forceRestoreHeightTemporarilyForSettingStackParentView];
|
||
[_parentStack removeFromSuperview];
|
||
currentSettingsMenuMode = FavoriteSettings;
|
||
[self initParentStack];
|
||
[self updateTheme];
|
||
DataManager* dataMan = [[DataManager alloc] init];
|
||
Settings *currentSettings = [dataMan retrieveSettings];
|
||
currentSettings.settingsMenuMode = [NSNumber numberWithInteger:currentSettingsMenuMode];
|
||
[dataMan saveData];
|
||
|
||
_parentStack.spacing = [self isIPhone] ? 10 : 15;
|
||
|
||
[self loadFavoriteSettingStackIdentifiers];
|
||
for(NSString* settingIdentifier in _favoriteSettingStackIdentifiers){
|
||
[_parentStack addArrangedSubview:_settingStackDict[settingIdentifier]];
|
||
}
|
||
[self hideDynamicLabelsWhenOverlapped:self.parentStack];
|
||
[self layoutSettingsView];
|
||
}
|
||
|
||
- (void)switchToAllSettings{
|
||
[self forceRestoreHeightTemporarilyForSettingStackParentView];
|
||
currentSettingsMenuMode = AllSettings;
|
||
[_parentStack removeFromSuperview];
|
||
[self initParentStack];
|
||
[self layoutSections];
|
||
[self updateTheme];
|
||
//[self doneRemoveSettingItem];
|
||
DataManager* dataMan = [[DataManager alloc] init];
|
||
Settings *currentSettings = [dataMan retrieveSettings];
|
||
currentSettings.settingsMenuMode = [NSNumber numberWithInteger:currentSettingsMenuMode];
|
||
[dataMan saveData];
|
||
[self layoutSettingsView];
|
||
}
|
||
|
||
- (void)enterRemoveSettingItemMode{
|
||
currentSettingsMenuMode = RemoveSettingItem;
|
||
for(UIStackView* stack in _parentStack.arrangedSubviews){
|
||
for(UIView* view in stack.subviews){
|
||
if([view.accessibilityIdentifier isEqualToString:@"infoButton"]) view.hidden = YES;
|
||
// view.userInteractionEnabled = false;
|
||
}
|
||
[self attachRemoveButtonForStack:stack];
|
||
// stack.userInteractionEnabled = false;
|
||
}
|
||
}
|
||
|
||
- (void)doneRemoveSettingItem{
|
||
currentSettingsMenuMode = FavoriteSettings;
|
||
for(UIStackView* stack in _parentStack.arrangedSubviews){
|
||
//stack.userInteractionEnabled = true;
|
||
for(UIView* view in stack.subviews){
|
||
//view.
|
||
if([view.accessibilityIdentifier isEqualToString:@"infoButton"]) view.hidden = NO;
|
||
if([view.accessibilityIdentifier isEqualToString:@"removeButton"]) [view removeFromSuperview];
|
||
}
|
||
}
|
||
}
|
||
|
||
- (void)saveFavoriteSettingStackIdentifiers {
|
||
|
||
if(currentSettingsMenuMode != AllSettings){
|
||
[_favoriteSettingStackIdentifiers removeAllObjects];
|
||
//for(NSInteger i = 0; i < parentStack.arrangedSubviews.count; i++){
|
||
for(NSInteger i = 0; i < _parentStack.arrangedSubviews.count; i++){
|
||
[_favoriteSettingStackIdentifiers addObject:_parentStack.arrangedSubviews[i].accessibilityIdentifier];
|
||
}
|
||
}
|
||
|
||
[[NSUserDefaults standardUserDefaults] setObject:_favoriteSettingStackIdentifiers forKey:@"FavoriteSettingStackIdentifiers"];
|
||
[[NSUserDefaults standardUserDefaults] synchronize];
|
||
}
|
||
|
||
- (void)loadFavoriteSettingStackIdentifiers {
|
||
NSArray *savedArray = [[NSUserDefaults standardUserDefaults] objectForKey:@"FavoriteSettingStackIdentifiers"];
|
||
if ([savedArray isKindOfClass:[NSArray class]]) {
|
||
_favoriteSettingStackIdentifiers = [savedArray mutableCopy];
|
||
} else {
|
||
_favoriteSettingStackIdentifiers = [NSMutableArray array];
|
||
}
|
||
/*
|
||
for(NSString* str in _favoriteSettingStackIdentifiers){
|
||
NSLog(@"favarite setting loaded: %@", str);
|
||
}
|
||
*/
|
||
}
|
||
|
||
- (void)viewDidLoad {
|
||
//[self updateTheme];
|
||
settingStackWillBeRelocatedToLowestPosition = false;
|
||
hiddenStacks = [[NSMutableSet alloc] init];
|
||
|
||
[[NSUserDefaults standardUserDefaults] setBool:NO forKey:@"_UIConstraintBasedLayoutLogUnsatisfiable"];
|
||
|
||
UILongPressGestureRecognizer *longPress = [[UILongPressGestureRecognizer alloc] initWithTarget:self action:@selector(handleLongPress:)];
|
||
[self.view addGestureRecognizer:longPress];
|
||
|
||
_settingStackDict = [[NSMutableDictionary alloc] init];
|
||
|
||
// return;
|
||
BOOL isIPad = ([UIDevice currentDevice].userInterfaceIdiom == UIUserInterfaceIdiomPad);
|
||
if(true){
|
||
//[self layoutWidgetes]; // layout for ipad tmply
|
||
// [self.view ]
|
||
for(UIView* view in self.view.subviews){
|
||
[view removeFromSuperview];
|
||
}
|
||
[self initParentStack];
|
||
[self layoutSections];
|
||
}
|
||
|
||
|
||
// [self swi];
|
||
|
||
self->slideToCloseSettingsViewRecognizer = [[CustomEdgeSlideGestureRecognizer alloc] initWithTarget:self action:@selector(edgeSwiped)];
|
||
slideToCloseSettingsViewRecognizer.edges = UIRectEdgeLeft;
|
||
slideToCloseSettingsViewRecognizer.normalizedThresholdDistance = 0.0;
|
||
slideToCloseSettingsViewRecognizer.EDGE_TOLERANCE = 10;
|
||
slideToCloseSettingsViewRecognizer.immediateTriggering = true;
|
||
slideToCloseSettingsViewRecognizer.delaysTouchesBegan = NO;
|
||
slideToCloseSettingsViewRecognizer.delaysTouchesEnded = NO;
|
||
[self.view addGestureRecognizer:slideToCloseSettingsViewRecognizer];
|
||
|
||
justEnteredSettingsViewDoNotOpenOscLayoutTool = true;
|
||
|
||
// Always run settings in dark mode because we want the light fonts
|
||
if (@available(iOS 13.0, tvOS 13.0, *)) {
|
||
self.overrideUserInterfaceStyle = UIUserInterfaceStyleDark;
|
||
}
|
||
|
||
DataManager* dataMan = [[DataManager alloc] init];
|
||
TemporarySettings* currentSettings = [dataMan getSettings];
|
||
|
||
currentSettingsMenuMode = currentSettings.settingsMenuMode.intValue;
|
||
[self loadFavoriteSettingStackIdentifiers];
|
||
if(currentSettings.settingsMenuMode.intValue == FavoriteSettings) [self switchToFavoriteSettings];
|
||
/*
|
||
switch (currentSettingsMenuMode) {
|
||
case FavoriteSettings:
|
||
[self switchToFavoriteSettings];
|
||
break;
|
||
case AllSettings:
|
||
//[self switchToAllSettings];
|
||
break;
|
||
default:
|
||
break;
|
||
}
|
||
*/
|
||
|
||
|
||
// Ensure we pick a bitrate that falls exactly onto a slider notch
|
||
_bitrate = bitrateTable[[self getSliderValueForBitrate:[currentSettings.bitrate intValue]]];
|
||
|
||
// Get the size of the screen with and without safe area insets
|
||
UIWindow *window = UIApplication.sharedApplication.windows.firstObject;
|
||
CGFloat screenScale = window.screen.scale;
|
||
CGFloat safeAreaWidth = (window.frame.size.width - window.safeAreaInsets.left - window.safeAreaInsets.right) * screenScale;
|
||
CGFloat fullScreenWidth = window.frame.size.width * screenScale;
|
||
CGFloat fullScreenHeight = window.frame.size.height * screenScale;
|
||
|
||
[self.resolutionSelector removeSegmentAtIndex:0 animated:NO]; // remove 360p
|
||
[self.resolutionSelector removeSegmentAtIndex:5 animated:NO]; // remove custom segment
|
||
|
||
resolutionTable[0] = CGSizeMake(1280, 720);
|
||
resolutionTable[1] = CGSizeMake(1920, 1080);
|
||
resolutionTable[2] = CGSizeMake(3840, 2160);
|
||
resolutionTable[3] = CGSizeMake(safeAreaWidth, fullScreenHeight);
|
||
resolutionTable[4] = CGSizeMake(fullScreenWidth, fullScreenHeight);
|
||
resolutionTable[5] = CGSizeMake([currentSettings.width integerValue], [currentSettings.height integerValue]); // custom initial value
|
||
[self updateResolutionTable];
|
||
|
||
// Don't populate the custom entry unless we have a custom resolution
|
||
//self.customResolutionSwitch
|
||
if (!isCustomResolution(resolutionTable[5])) {
|
||
resolutionTable[5] = CGSizeMake(0, 0);
|
||
}
|
||
|
||
NSInteger framerate;
|
||
switch ([currentSettings.framerate integerValue]) {
|
||
case 30:
|
||
framerate = 0;
|
||
break;
|
||
default:
|
||
case 60:
|
||
framerate = 1;
|
||
break;
|
||
case 120:
|
||
framerate = 2;
|
||
break;
|
||
}
|
||
|
||
NSInteger resolution = currentSettings.resolutionSelected.integerValue;
|
||
if(resolution >= RESOLUTION_TABLE_SIZE){
|
||
resolution = 0;
|
||
}
|
||
|
||
// Only show the 120 FPS option if we have a > 60-ish Hz display
|
||
bool enable120Fps = false;
|
||
if (@available(iOS 10.3, tvOS 10.3, *)) {
|
||
if ([UIScreen mainScreen].maximumFramesPerSecond > 62) {
|
||
enable120Fps = true;
|
||
}
|
||
}
|
||
if (!enable120Fps) {
|
||
[self.framerateSelector removeSegmentAtIndex:2 animated:NO];
|
||
}
|
||
|
||
// Disable codec selector segments for unsupported codecs
|
||
#if defined(__IPHONE_16_0) || defined(__TVOS_16_0)
|
||
if (!VTIsHardwareDecodeSupported(kCMVideoCodecType_AV1))
|
||
#endif
|
||
{
|
||
[self.codecSelector removeSegmentAtIndex:2 animated:NO];
|
||
}
|
||
if (!VTIsHardwareDecodeSupported(kCMVideoCodecType_HEVC)) {
|
||
[self.codecSelector removeSegmentAtIndex:1 animated:NO];
|
||
|
||
// Only enable the 4K option for "recent" devices. We'll judge that by whether
|
||
// they support HEVC decoding (A9 or later).
|
||
[self.resolutionSelector setEnabled:NO forSegmentAtIndex:3];
|
||
}
|
||
switch (currentSettings.preferredCodec) {
|
||
case CODEC_PREF_AUTO:
|
||
[self.codecSelector setSelectedSegmentIndex:self.codecSelector.numberOfSegments - 1];
|
||
break;
|
||
|
||
case CODEC_PREF_AV1:
|
||
[self.codecSelector setSelectedSegmentIndex:2];
|
||
break;
|
||
|
||
case CODEC_PREF_HEVC:
|
||
[self.codecSelector setSelectedSegmentIndex:1];
|
||
break;
|
||
|
||
case CODEC_PREF_H264:
|
||
[self.codecSelector setSelectedSegmentIndex:0];
|
||
break;
|
||
}
|
||
|
||
if (!VTIsHardwareDecodeSupported(kCMVideoCodecType_HEVC) || !(AVPlayer.availableHDRModes & AVPlayerHDRModeHDR10)) {
|
||
[self.hdrSwitch setOn:NO];
|
||
[self.hdrSwitch setEnabled:NO];
|
||
}
|
||
else {
|
||
[self.hdrSwitch setOn:currentSettings.enableHdr];
|
||
}
|
||
|
||
[self.yuv444Switch setOn:currentSettings.enableYUV444];
|
||
[self.statsOverlaySelector setSelectedSegmentIndex:currentSettings.statsOverlayLevel.intValue];
|
||
[self.citrixX1MouseSwitch setOn:currentSettings.btMouseSupport];
|
||
[self.optimizeGamesSwitch setOn: currentSettings.optimizeGames];
|
||
[self.framePacingSelector setSelectedSegmentIndex:currentSettings.useFramePacing ? 1 : 0];
|
||
[self.multiControllerSwitch setOn:currentSettings.multiController];
|
||
[self.swapABXYButtonsSelector setSelectedSegmentIndex:currentSettings.swapABXYButtons ? 1 : 0];
|
||
[self.audioOnPcSwitch setOn:currentSettings.playAudioOnPC];
|
||
_lastSelectedResolutionIndex = resolution;
|
||
[self.resolutionSelector setSelectedSegmentIndex:resolution];
|
||
[self.resolutionSelector addTarget:self action:@selector(newResolutionChosen) forControlEvents:UIControlEventValueChanged];
|
||
[self.customResolutionSwitch addTarget:self action:@selector(customResolutionSwitched:) forControlEvents:UIControlEventValueChanged];
|
||
CGSize currentResolution = CGSizeMake(currentSettings.width.intValue, currentSettings.height.intValue);
|
||
[self.customResolutionSwitch setOn: isCustomResolution(currentResolution)];
|
||
|
||
[self.framerateSelector setSelectedSegmentIndex:framerate];
|
||
[self.framerateSelector addTarget:self action:@selector(updateBitrate) forControlEvents:UIControlEventValueChanged];
|
||
[self.bitrateSlider setMinimumValue:0];
|
||
[self.bitrateSlider setMaximumValue:(sizeof(bitrateTable) / sizeof(*bitrateTable)) - 1];
|
||
[self.bitrateSlider setValue:[self getSliderValueForBitrate:_bitrate] animated:YES];
|
||
[self.bitrateSlider addTarget:self action:@selector(bitrateSliderMoved) forControlEvents:UIControlEventValueChanged];
|
||
[self updateBitrateText];
|
||
[self updateResolutionDisplayLabel];
|
||
|
||
// Unlock Display Orientation setting
|
||
// bool unlockDisplayOrientationSelectorEnabled = [self isFullScreenRequired];//need "requires fullscreen" enabled in the app bunddle to make runtime orientation limitation woring
|
||
bool unlockDisplayOrientationSelectorEnabled = true;
|
||
if(unlockDisplayOrientationSelectorEnabled) [self.unlockDisplayOrientationSelector setSelectedSegmentIndex:currentSettings.unlockDisplayOrientation ? 1 : 0];
|
||
else [self.unlockDisplayOrientationSelector setSelectedSegmentIndex:1]; // can't lock screen orientation in this mode = Display Orientation always unlocked
|
||
[self.unlockDisplayOrientationSelector setEnabled:unlockDisplayOrientationSelectorEnabled];
|
||
|
||
|
||
// lift streamview setting
|
||
[self.liftStreamViewForKeyboardSelector setSelectedSegmentIndex:currentSettings.liftStreamViewForKeyboard ? 1 : 0];// Load old setting
|
||
|
||
// showkeyboard toolbar setting
|
||
[self.softKeyboardToolbarSwitch setOn:currentSettings.showKeyboardToolbar];// Load old setting
|
||
|
||
// reverse mouse wheel direction setting
|
||
[self.reverseMouseWheelDirectionSelector setSelectedSegmentIndex:currentSettings.reverseMouseWheelDirection ? 1 : 0];// Load old setting
|
||
|
||
// slide to menu settings
|
||
[self.slideToSettingsScreenEdgeSelector setSelectedSegmentIndex:[self getSelectorIndexFromScreenEdge:(uint32_t)currentSettings.slideToSettingsScreenEdge.integerValue]];
|
||
// Load old setting
|
||
[self.slideToToolboxScreenEdgeSelector setEnabled:false];
|
||
[self.slideToSettingsScreenEdgeSelector addTarget:self action:@selector(slideToSettingsScreenEdgeChanged) forControlEvents:UIControlEventValueChanged];
|
||
[self slideToSettingsScreenEdgeChanged];
|
||
|
||
[self.slideToMenuDistanceSlider setValue:currentSettings.slideToSettingsDistance.floatValue];
|
||
[self.slideToMenuDistanceSlider addTarget:self action:@selector(slideToMenuDistanceSliderMoved:) forControlEvents:(UIControlEventValueChanged)]; // Update label display when slider is being moved.
|
||
[self slideToMenuDistanceSliderMoved:self.slideToMenuDistanceSlider];
|
||
|
||
|
||
|
||
//TouchMode & OSC Related Settings:
|
||
|
||
// pointer veloc setting, will be enable/disabled by touchMode
|
||
[self.pointerVelocityModeDividerSlider setValue: (uint8_t)(currentSettings.pointerVelocityModeDivider.floatValue * 100) animated:YES]; // Load old setting.
|
||
[self.pointerVelocityModeDividerSlider addTarget:self action:@selector(pointerVelocityModeDividerSliderMoved:) forControlEvents:(UIControlEventValueChanged)]; // Update label display when slider is being moved.
|
||
[self pointerVelocityModeDividerSliderMoved:self.pointerVelocityModeDividerSlider];
|
||
|
||
// init pointer veloc setting, will be enable/disabled by touchMode
|
||
[self.touchPointerVelocityFactorSlider setValue: [self map_SliderValue_fromVelocFactor: currentSettings.touchPointerVelocityFactor.floatValue] animated:YES]; // Load old setting.
|
||
[self.touchPointerVelocityFactorSlider addTarget:self action:@selector(touchPointerVelocityFactorSliderMoved:) forControlEvents:(UIControlEventValueChanged)]; // Update label display when slider is being moved.
|
||
[self touchPointerVelocityFactorSliderMoved:self.touchPointerVelocityFactorSlider];
|
||
|
||
// async native touch event
|
||
// [self.asyncNativeTouchPrioritySelector setSelectedSegmentIndex:currentSettings.asyncNativeTouchPriority.intValue]; // load old setting of asyncNativeTouchPriority
|
||
// [self.asyncNativeTouchPrioritySelector addTarget:self action:@selector(asyncNativeTouchPriorityChanged) forControlEvents:UIControlEventValueChanged];
|
||
|
||
// init relative touch mouse pointer veloc setting, will be enable/disabled by touchMode
|
||
[self.mousePointerVelocityFactorSlider setValue:[self map_SliderValue_fromVelocFactor: currentSettings.mousePointerVelocityFactor.floatValue] animated:YES]; // Load old setting.
|
||
[self.mousePointerVelocityFactorSlider addTarget:self action:@selector(mousePointerVelocityFactorSliderMoved:) forControlEvents:(UIControlEventValueChanged)]; // Update label display when slider is being moved.
|
||
[self mousePointerVelocityFactorSliderMoved:self.mousePointerVelocityFactorSlider];
|
||
|
||
|
||
// these settings will be affected by onscreenControl & touchMode, must be loaded before them.
|
||
// NSLog(@"osc tool fingers setting test: %d", currentSettings.oscLayoutToolFingers.intValue);
|
||
self->oswLayoutFingers = (uint16_t)currentSettings.oscLayoutToolFingers.intValue; // load old setting of oscLayoutFingers
|
||
uint8_t keyboardToggleFingers = currentSettings.keyboardToggleFingers.intValue;
|
||
|
||
[self.softKeyboardGestureSelector setSelectedSegmentIndex:keyboardToggleFingers>=5 ? 3 : keyboardToggleFingers-3];
|
||
|
||
|
||
|
||
// this setting will be affected by touchMode, must be loaded before them.
|
||
NSInteger onscreenControlsLevel = [currentSettings.onscreenControls integerValue];
|
||
[self.onScreenWidgetSelector setSelectedSegmentIndex:onscreenControlsLevel];
|
||
[self.onScreenWidgetSelector addTarget:self action:@selector(onScreenWidgetChanged) forControlEvents:UIControlEventValueChanged];
|
||
[self onScreenWidgetChanged];
|
||
|
||
// touch move event interval for native-touch.
|
||
[self.touchMoveEventIntervalSlider setValue:currentSettings.touchMoveEventInterval.intValue animated:YES]; // Load old setting.
|
||
[self.touchMoveEventIntervalSlider addTarget:self action:@selector(touchMoveEventIntervalSliderMoved:) forControlEvents:(UIControlEventValueChanged)]; // Update label display when slider is being moved.
|
||
[self touchMoveEventIntervalSliderMoved:self.touchMoveEventIntervalSlider];
|
||
|
||
|
||
// this part will enable/disable oscSelector & the asyncNativeTouchPriority selector
|
||
uint8_t touchModeSelectorIndex = currentSettings.touchMode.intValue == NativeTouchOnly ? NativeTouch : currentSettings.touchMode.intValue;
|
||
[self.touchModeSelector setSelectedSegmentIndex:touchModeSelectorIndex]; //Load old touchMode setting
|
||
[self.touchModeSelector addTarget:self action:@selector(touchModeChanged:) forControlEvents:UIControlEventValueChanged];
|
||
[self touchModeChanged:self.touchModeSelector];
|
||
|
||
self.enableOswSwitchStack.hidden = !(currentSettings.touchMode.intValue == NativeTouch || currentSettings.touchMode.intValue == NativeTouchOnly);
|
||
[self.enableOswForNativeTouchSwitch setOn:currentSettings.touchMode.intValue != NativeTouchOnly];
|
||
[self.enableOswForNativeTouchSwitch addTarget:self action:@selector(enableOswForNativeTouchSwitchFlipped:) forControlEvents:UIControlEventValueChanged];
|
||
[self enableOswForNativeTouchSwitchFlipped:self.enableOswForNativeTouchSwitch];
|
||
|
||
|
||
// init CustomOSC stuff
|
||
/* sets a reference to the correct 'LayoutOnScreenControlsViewController' depending on whether the user is on an iPhone or iPad */
|
||
// self.layoutOnScreenControlsVC = [[LayoutOnScreenControlsViewController alloc] init];
|
||
BOOL isIPhone = ([UIDevice currentDevice].userInterfaceIdiom == UIUserInterfaceIdiomPhone);
|
||
if (isIPhone) {
|
||
UIStoryboard *storyboard = [UIStoryboard storyboardWithName:@"iPhone" bundle:nil];
|
||
self.layoutOnScreenControlsVC = [storyboard instantiateViewControllerWithIdentifier:@"LayoutOnScreenControlsViewController"];
|
||
}
|
||
else {
|
||
UIStoryboard *storyboard = [UIStoryboard storyboardWithName:@"iPad" bundle:nil];
|
||
self.layoutOnScreenControlsVC = [storyboard instantiateViewControllerWithIdentifier:@"LayoutOnScreenControlsViewController"];
|
||
self.layoutOnScreenControlsVC.modalPresentationStyle = UIModalPresentationFullScreen;
|
||
}
|
||
|
||
[self.externalDisplayModeSelector setSelectedSegmentIndex:currentSettings.externalDisplayMode.integerValue];
|
||
[self.localMousePointerModeSelector setSelectedSegmentIndex:currentSettings.localMousePointerMode.integerValue];
|
||
}
|
||
|
||
- (void)slideToSettingsScreenEdgeChanged{
|
||
if([self.slideToSettingsScreenEdgeSelector selectedSegmentIndex] == 0) [self.slideToToolboxScreenEdgeSelector setSelectedSegmentIndex:1];
|
||
else [self.slideToToolboxScreenEdgeSelector setSelectedSegmentIndex:0];
|
||
}
|
||
|
||
|
||
- (void)updateTouchModeLabel{
|
||
NSString* labelText;
|
||
switch([self.touchModeSelector selectedSegmentIndex]){
|
||
case RelativeTouch:
|
||
labelText = [LocalizationHelper localizedStringForKey:@"Touch Mode - Double Tap to Drag, OSC Available"];break;
|
||
case NativeTouch:
|
||
labelText = [LocalizationHelper localizedStringForKey:@"Touch Mode - With OSC & Mouse Support"];break;
|
||
case NativeTouchOnly:
|
||
labelText = [LocalizationHelper localizedStringForKey:@"Touch Mode - No OSC & Mouse Support"];break;
|
||
case AbsoluteTouch:
|
||
labelText = [LocalizationHelper localizedStringForKey:@"Touch Mode - For MacOS Direct Touch"];break;
|
||
}
|
||
[self.touchModeLabel setText:labelText];
|
||
}
|
||
|
||
- (void)showCustomOswTip {
|
||
NSString* edgeSide = self.slideToSettingsScreenEdgeSelector.selectedSegmentIndex == 1 ? [LocalizationHelper localizedStringForKey:@"left"] : [LocalizationHelper localizedStringForKey:@"right"];
|
||
UIAlertController *alertController = [UIAlertController alertControllerWithTitle:[LocalizationHelper localizedStringForKey:@"Rebase in Streaming"]
|
||
message:[LocalizationHelper localizedStringForKey:@"Open widget tool in streaming by:\nSliding from %@ screen edge to open cmd tool.\nOr tap %d fingers on stream view, number of fingers required:", edgeSide, self->oswLayoutFingers]
|
||
preferredStyle:UIAlertControllerStyleAlert];
|
||
|
||
[alertController addTextFieldWithConfigurationHandler:^(UITextField *textField) {
|
||
textField.placeholder = [LocalizationHelper localizedStringForKey:@"%d", self->oswLayoutFingers];
|
||
textField.keyboardType = UIKeyboardTypeNumberPad;
|
||
}];
|
||
|
||
UIAlertAction *cancelAction = [UIAlertAction actionWithTitle:[LocalizationHelper localizedStringForKey:@"Cancel"]
|
||
style:UIAlertActionStyleDefault
|
||
handler:nil];
|
||
|
||
UIAlertAction *okAction = [UIAlertAction actionWithTitle:[LocalizationHelper localizedStringForKey:@"OK"]
|
||
style:UIAlertActionStyleDefault
|
||
handler:^(UIAlertAction *action) {
|
||
UITextField *textField = alertController.textFields.firstObject;
|
||
NSString *inputText = textField.text;
|
||
NSInteger fingers = [inputText integerValue];
|
||
if (inputText.length > 0 && fingers >= 4) {
|
||
self->oswLayoutFingers = (uint16_t) fingers;
|
||
NSLog(@"OK button tapped with %d fingers", (uint16_t)fingers);
|
||
} else {
|
||
NSLog(@"OK button tapped with no change");
|
||
}
|
||
|
||
// Continue execution after the alert is dismissed
|
||
if (!self->_mainFrameViewController.settingsExpandedInStreamView) {
|
||
[self invokeOscLayout]; // Don't open osc layout tool immediately during streaming
|
||
}
|
||
|
||
[self findDynamicLabelFromStack:self.onScreenWidgetStack].text = [LocalizationHelper localizedStringForKey:@"%d finger tap", self->oswLayoutFingers];
|
||
//markmark
|
||
[self handleOswGestureChange];
|
||
}];
|
||
|
||
[alertController addAction:cancelAction];
|
||
[alertController addAction:okAction];
|
||
[self presentViewController:alertController animated:YES completion:nil];
|
||
}
|
||
|
||
|
||
|
||
- (bool)isOswEnabled{
|
||
return [self isNotNativeTouchOnly] && self.onScreenWidgetSelector.selectedSegmentIndex != OnScreenControlsLevelOff;
|
||
}
|
||
|
||
- (bool)isCustomOswEnabled{
|
||
return [self isOswEnabled] && self.onScreenWidgetSelector.selectedSegmentIndex == OnScreenControlsLevelCustom;
|
||
}
|
||
|
||
- (bool)isNotNativeTouchOnly{
|
||
return (self.enableOswForNativeTouchSwitch.isOn && self.touchModeSelector.selectedSegmentIndex == NativeTouch) || self.touchModeSelector.selectedSegmentIndex == RelativeTouch || self.touchModeSelector.selectedSegmentIndex == AbsoluteTouch;
|
||
}
|
||
|
||
- (void)handleOswGestureChange{
|
||
if(oswLayoutFingers == self.softKeyboardGestureSelector.selectedSegmentIndex + 3 && oswLayoutFingers < 6){
|
||
[_softKeyboardGestureSelector setSelectedSegmentIndex:_softKeyboardGestureSelector.selectedSegmentIndex-1];
|
||
}
|
||
for (NSInteger i = 0; i < _softKeyboardGestureSelector.numberOfSegments; i++) {
|
||
[_softKeyboardGestureSelector setEnabled:![self isCustomOswEnabled] ? true : i+3 != oswLayoutFingers forSegmentAtIndex:i]; // 或 NO 来禁用
|
||
}
|
||
}
|
||
|
||
- (void)onScreenWidgetChanged{
|
||
|
||
BOOL isIPhone = ([UIDevice currentDevice].userInterfaceIdiom == UIUserInterfaceIdiomPhone);
|
||
if (isIPhone) {
|
||
UIStoryboard *storyboard = [UIStoryboard storyboardWithName:@"iPhone" bundle:nil];
|
||
self.layoutOnScreenControlsVC = [storyboard instantiateViewControllerWithIdentifier:@"LayoutOnScreenControlsViewController"];
|
||
}
|
||
else {
|
||
UIStoryboard *storyboard = [UIStoryboard storyboardWithName:@"iPad" bundle:nil];
|
||
self.layoutOnScreenControlsVC = [storyboard instantiateViewControllerWithIdentifier:@"LayoutOnScreenControlsViewController"];
|
||
self.layoutOnScreenControlsVC.modalPresentationStyle = UIModalPresentationFullScreen;
|
||
}
|
||
|
||
bool customOscEnabled = [self isCustomOswEnabled];
|
||
UILabel* oswDynamicLabel = [self findDynamicLabelFromStack:self.onScreenWidgetStack];
|
||
NSString* labelText = [LocalizationHelper localizedStringForKey:@"%d finger tap", self->oswLayoutFingers];
|
||
oswDynamicLabel.text = [NSString stringWithFormat:@" %@ ", labelText];
|
||
oswDynamicLabel.hidden = !customOscEnabled;
|
||
[self handleOswGestureChange];
|
||
if(customOscEnabled && !justEnteredSettingsViewDoNotOpenOscLayoutTool) {
|
||
// [self.keyboardToggleFingerNumSlider setValue:3.0];
|
||
// [self keyboardToggleFingerNumSliderMoved];
|
||
//markmark
|
||
[self showCustomOswTip];
|
||
justEnteredSettingsViewDoNotOpenOscLayoutTool = false;
|
||
}
|
||
justEnteredSettingsViewDoNotOpenOscLayoutTool = false;
|
||
}
|
||
|
||
- (void)invokeOscLayout{
|
||
self.layoutOnScreenControlsVC.view.backgroundColor = [UIColor colorWithWhite:0.55 alpha:1.0];
|
||
[self presentViewController:self.layoutOnScreenControlsVC animated:YES completion:nil];
|
||
}
|
||
|
||
|
||
- (void) pointerVelocityModeDividerSliderMoved:(UISlider* )sender {
|
||
[self findDynamicLabelFromStack:self.pointerVelocityDividerStack].text = [NSString stringWithFormat:@" | %d%% | %d%% | ", (uint8_t)sender.value, 100-(uint8_t)sender.value];
|
||
}
|
||
|
||
- (void) touchPointerVelocityFactorSliderMoved:(UISlider* )sender {
|
||
[self findDynamicLabelFromStack:self.pointerVelocityFactorStack].text = [NSString stringWithFormat:@" %d%% ", [self map_velocFactorDisplay_fromSliderValue: self.touchPointerVelocityFactorSlider.value]]; // Update label display
|
||
}
|
||
|
||
|
||
// veloc factor upto 700%
|
||
- (uint16_t) map_velocFactorDisplay_fromSliderValue:(CGFloat)sliderValue{
|
||
uint16_t velocFactorDisplay = 0;
|
||
if(sliderValue > 200) velocFactorDisplay = 200 + ((uint16_t)sliderValue % 200) * 5;
|
||
else velocFactorDisplay = (uint16_t) sliderValue;
|
||
return velocFactorDisplay;
|
||
}
|
||
|
||
// veloc factor upto 700%
|
||
|
||
- (CGFloat) map_SliderValue_fromVelocFactor:(CGFloat)velocFactor{
|
||
CGFloat sliderValue = 0.0f;
|
||
if(velocFactor < 2.0f) sliderValue = velocFactor * 100;
|
||
else sliderValue = (velocFactor - 2.0) * 100 / 5 + 200;
|
||
return sliderValue;
|
||
}
|
||
|
||
- (void) mousePointerVelocityFactorSliderMoved:(UISlider* )sender {
|
||
[self findDynamicLabelFromStack:_mousePointerVelocityStack].text = [NSString stringWithFormat:@" %d%% ",[self map_velocFactorDisplay_fromSliderValue:sender.value]];
|
||
}
|
||
|
||
- (uint32_t) getScreenEdgeFromSelector {
|
||
switch (self.slideToSettingsScreenEdgeSelector.selectedSegmentIndex) {
|
||
case 0: return UIRectEdgeLeft;
|
||
case 1: return UIRectEdgeRight;
|
||
case 2: return UIRectEdgeLeft|UIRectEdgeRight;
|
||
default: return UIRectEdgeLeft;
|
||
}
|
||
}
|
||
|
||
- (uint32_t) getSelectorIndexFromScreenEdge: (uint32_t)edge {
|
||
switch (edge) {
|
||
case UIRectEdgeLeft: return 0;
|
||
case UIRectEdgeRight: return 1;
|
||
case UIRectEdgeLeft|UIRectEdgeRight: return 2;
|
||
default: return 0;
|
||
}
|
||
return 0;
|
||
}
|
||
|
||
- (void) widget:(UISlider*)widget setEnabled:(bool)enabled{
|
||
[widget setEnabled:enabled];
|
||
if(enabled){
|
||
widget.alpha = 1.0;
|
||
[widget setValue:widget.value + 0.0001]; // this is for low iOS version (like iOS14), only setting this minor value change is able to make widget visibility clear
|
||
}
|
||
else widget.alpha = 0.5; // this is for updating widget visibility on low iOS version like mini5 ios14
|
||
}
|
||
|
||
/*
|
||
- (void) asyncNativeTouchPriorityChanged {
|
||
bool isNativeTouch = [self.touchModeSelector selectedSegmentIndex] == NativeTouchOnly || [self.touchModeSelector selectedSegmentIndex] == NativeTouch;
|
||
bool asyncNativeTouchEnabled = [self.asyncNativeTouchPrioritySelector selectedSegmentIndex] != AsyncNativeTouchOff;
|
||
[self widget:self.touchMoveEventIntervalSlider setEnabled:isNativeTouch && asyncNativeTouchEnabled];
|
||
}
|
||
*/
|
||
|
||
- (void)touchModeChanged:(UISegmentedControl* )sender {
|
||
// Disable On-Screen Controls & Widgets in non-relative touch mode
|
||
// bool customOscEnabled = [self isOswEnabled] && [self.onScreenWidgetSelector selectedSegmentIndex] == OnScreenControlsLevelCustom;
|
||
bool isNativeTouch = sender.selectedSegmentIndex == NativeTouch;
|
||
//bool asyncNativeTouchEnabled = [self.asyncNativeTouchPrioritySelector selectedSegmentIndex] != AsyncNativeTouchOff;
|
||
self.enableOswSwitchStack.hidden = !isNativeTouch;
|
||
[self setHidden:!isNativeTouch forStack:self.pointerVelocityDividerStack];
|
||
|
||
// [self.asyncNativeTouchPrioritySelector setEnabled:![self showOswSelector]]; // this selector stay aligned with oscSelector
|
||
[self touchMoveEventIntervalSliderMoved:self.touchMoveEventIntervalSlider];
|
||
[self setHidden:!isNativeTouch forStack:self.pointerVelocityDividerStack];
|
||
[self setHidden:!isNativeTouch forStack:self.pointerVelocityFactorStack];
|
||
|
||
[self setHidden:!(sender.selectedSegmentIndex == RelativeTouch) forStack:self.mousePointerVelocityStack];
|
||
[self setHidden:![self isNotNativeTouchOnly] forStack:self.onScreenWidgetStack];
|
||
[self setHidden:![self isNotNativeTouchOnly] forStack:self.swapAbaxyStack];
|
||
|
||
|
||
[touchControlSection updateViewForFoldState];
|
||
}
|
||
|
||
- (void)setHidden:(BOOL)hidden forStack:(UIStackView* )stack{
|
||
// CGFloat previousSpacing = stack.spacing;
|
||
if(hidden){
|
||
stack.hidden = YES;
|
||
[hiddenStacks addObject:stack];
|
||
}
|
||
else{
|
||
stack.hidden = NO;
|
||
[hiddenStacks removeObject:stack];
|
||
}
|
||
}
|
||
|
||
- (void)enableOswForNativeTouchSwitchFlipped:(UISwitch *)sender{
|
||
//self.onScreenWidgetStack.hidden = !sender.isOn;
|
||
[self setHidden:!sender.isOn forStack:_onScreenWidgetStack];
|
||
[self setHidden:!sender.isOn forStack:_swapAbaxyStack];
|
||
//self.swapAbaxyStack.hidden = !sender.isOn;
|
||
[touchControlSection updateViewForFoldState];
|
||
}
|
||
|
||
- (void) updateBitrate {
|
||
NSInteger fps = [self getChosenFrameRate];
|
||
NSInteger width = [self getChosenStreamWidth];
|
||
NSInteger height = [self getChosenStreamHeight];
|
||
NSInteger defaultBitrate;
|
||
|
||
// This logic is shamelessly stolen from Moonlight Qt:
|
||
// https://github.com/moonlight-stream/moonlight-qt/blob/master/app/settings/streamingpreferences.cpp
|
||
|
||
// Don't scale bitrate linearly beyond 60 FPS. It's definitely not a linear
|
||
// bitrate increase for frame rate once we get to values that high.
|
||
float frameRateFactor = (fps <= 60 ? fps : (sqrtf(fps / 60.f) * 60.f)) / 30.f;
|
||
|
||
// TODO: Collect some empirical data to see if these defaults make sense.
|
||
// We're just using the values that the Shield used, as we have for years.
|
||
struct {
|
||
NSInteger pixels;
|
||
int factor;
|
||
} resTable[] = {
|
||
{ 640 * 360, 1 },
|
||
{ 854 * 480, 2 },
|
||
{ 1280 * 720, 5 },
|
||
{ 1920 * 1080, 10 },
|
||
{ 2560 * 1440, 20 },
|
||
{ 3840 * 2160, 40 },
|
||
{ -1, -1 }
|
||
};
|
||
|
||
// Calculate the resolution factor by linear interpolation of the resolution table
|
||
float resolutionFactor;
|
||
NSInteger pixels = width * height;
|
||
for (int i = 0;; i++) {
|
||
if (pixels == resTable[i].pixels) {
|
||
// We can bail immediately for exact matches
|
||
resolutionFactor = resTable[i].factor;
|
||
break;
|
||
}
|
||
else if (pixels < resTable[i].pixels) {
|
||
if (i == 0) {
|
||
// Never go below the lowest resolution entry
|
||
resolutionFactor = resTable[i].factor;
|
||
}
|
||
else {
|
||
// Interpolate between the entry greater than the chosen resolution (i) and the entry less than the chosen resolution (i-1)
|
||
resolutionFactor = ((float)(pixels - resTable[i-1].pixels) / (resTable[i].pixels - resTable[i-1].pixels)) * (resTable[i].factor - resTable[i-1].factor) + resTable[i-1].factor;
|
||
}
|
||
break;
|
||
}
|
||
else if (resTable[i].pixels == -1) {
|
||
// Never go above the highest resolution entry
|
||
resolutionFactor = resTable[i-1].factor;
|
||
break;
|
||
}
|
||
}
|
||
|
||
defaultBitrate = round(resolutionFactor * frameRateFactor) * 1000;
|
||
_bitrate = MIN(defaultBitrate, 100000);
|
||
[self.bitrateSlider setValue:[self getSliderValueForBitrate:_bitrate] animated:YES];
|
||
|
||
[self updateBitrateText];
|
||
}
|
||
|
||
- (void) newResolutionChosen {
|
||
[self updateBitrate];
|
||
[self updateResolutionDisplayLabel];
|
||
_lastSelectedResolutionIndex = [self.resolutionSelector selectedSegmentIndex];
|
||
[self updateResolutionTable];
|
||
}
|
||
|
||
- (void)customResolutionSwitched:(UISwitch* )sender{
|
||
if(sender.isOn) [self promptCustomResolutionDialog];
|
||
else [self newResolutionChosen];
|
||
[self.resolutionSelector setEnabled:!sender.isOn];
|
||
}
|
||
|
||
- (void) promptCustomResolutionDialog {
|
||
UIAlertController *alertController = [UIAlertController alertControllerWithTitle:[LocalizationHelper localizedStringForKey: @"Enter Custom Resolution"] message:nil preferredStyle:UIAlertControllerStyleAlert];
|
||
|
||
[alertController addTextFieldWithConfigurationHandler:^(UITextField *textField) {
|
||
textField.placeholder = [LocalizationHelper localizedStringForKey:@"Video Width"];
|
||
textField.clearButtonMode = UITextFieldViewModeAlways;
|
||
textField.borderStyle = UITextBorderStyleRoundedRect;
|
||
textField.keyboardType = UIKeyboardTypeNumberPad;
|
||
|
||
if (resolutionTable[RESOLUTION_TABLE_CUSTOM_INDEX].width == 0) {
|
||
textField.text = @"";
|
||
}
|
||
else {
|
||
textField.text = [NSString stringWithFormat:@"%d", (int) resolutionTable[RESOLUTION_TABLE_CUSTOM_INDEX].width];
|
||
}
|
||
}];
|
||
|
||
[alertController addTextFieldWithConfigurationHandler:^(UITextField *textField) {
|
||
textField.placeholder = [LocalizationHelper localizedStringForKey:@"Video Height"];
|
||
textField.clearButtonMode = UITextFieldViewModeAlways;
|
||
textField.borderStyle = UITextBorderStyleRoundedRect;
|
||
textField.keyboardType = UIKeyboardTypeNumberPad;
|
||
|
||
if (resolutionTable[RESOLUTION_TABLE_CUSTOM_INDEX].height == 0) {
|
||
textField.text = @"";
|
||
}
|
||
else {
|
||
textField.text = [NSString stringWithFormat:@"%d", (int) resolutionTable[RESOLUTION_TABLE_CUSTOM_INDEX].height];
|
||
}
|
||
}];
|
||
|
||
[alertController addAction:[UIAlertAction actionWithTitle:[LocalizationHelper localizedStringForKey:@"Ok"] style:UIAlertActionStyleDefault handler:^(UIAlertAction *action) {
|
||
NSArray * textfields = alertController.textFields;
|
||
UITextField *widthField = textfields[0];
|
||
UITextField *heightField = textfields[1];
|
||
|
||
long width = [widthField.text integerValue];
|
||
long height = [heightField.text integerValue];
|
||
if (width <= 0 || height <= 0) {
|
||
// Restore the previous selection
|
||
[self.resolutionSelector setSelectedSegmentIndex:self->_lastSelectedResolutionIndex];
|
||
return;
|
||
}
|
||
|
||
// H.264 maximum
|
||
int maxResolutionDimension = 4096;
|
||
if (@available(iOS 11.0, tvOS 11.0, *)) {
|
||
if (VTIsHardwareDecodeSupported(kCMVideoCodecType_HEVC)) {
|
||
// HEVC maximum
|
||
maxResolutionDimension = 8192;
|
||
}
|
||
}
|
||
|
||
// Cap to maximum valid dimensions
|
||
width = MIN(width, maxResolutionDimension);
|
||
height = MIN(height, maxResolutionDimension);
|
||
|
||
// Cap to minimum valid dimensions
|
||
width = MAX(width, 256);
|
||
height = MAX(height, 256);
|
||
|
||
resolutionTable[RESOLUTION_TABLE_CUSTOM_INDEX] = CGSizeMake(width, height);
|
||
[self updateBitrate];
|
||
[self updateResolutionDisplayLabel];
|
||
|
||
UIAlertController *alertController = [UIAlertController alertControllerWithTitle:[LocalizationHelper localizedStringForKey:@"Custom Resolution Selected"] message: [LocalizationHelper localizedStringForKey:@"Custom resolutions are not officially supported by GeForce Experience, so it will not set your host display resolution. You will need to set it manually while in game.\n\nResolutions that are not supported by your client or host PC may cause streaming errors."] preferredStyle:UIAlertControllerStyleAlert];
|
||
[alertController addAction:[UIAlertAction actionWithTitle:[LocalizationHelper localizedStringForKey:@"Ok"] style:UIAlertActionStyleDefault handler:nil]];
|
||
[self presentViewController:alertController animated:YES completion:nil];
|
||
}]];
|
||
|
||
[alertController addAction:[UIAlertAction actionWithTitle:[LocalizationHelper localizedStringForKey:@"Cancel"] style:UIAlertActionStyleCancel handler:^(UIAlertAction *action) {
|
||
// Restore the previous selection
|
||
[self.customResolutionSwitch setOn:false];
|
||
[self.resolutionSelector setEnabled:true];
|
||
}]];
|
||
|
||
[self presentViewController:alertController animated:YES completion:nil];
|
||
}
|
||
|
||
- (void)resolutionDisplayViewTapped:(UITapGestureRecognizer *)sender {
|
||
}
|
||
|
||
- (void) updateResolutionDisplayLabel {
|
||
NSInteger width = [self getChosenStreamWidth];
|
||
NSInteger height = [self getChosenStreamHeight];
|
||
|
||
[self findDynamicLabelFromStack:_resolutionStack].text = [NSString stringWithFormat:@"%ld × %ld", (long)width, (long)height];
|
||
/*
|
||
[self.resolutionDisplayLabel sizeToFit];
|
||
self.resolutionDisplayLabel.textAlignment = NSTextAlignmentCenter;
|
||
self.resolutionDisplayLabel.adjustsFontSizeToFitWidth = YES;
|
||
self.resolutionDisplayLabel.textColor = [ThemeManager appPrimaryColor];
|
||
self.resolutionDisplayLabel.font = [UIFont systemFontOfSize:17 weight:UIFontWeightMedium]; */
|
||
}
|
||
|
||
- (void) touchMoveEventIntervalSliderMoved:(UISlider* )sender{
|
||
[self findDynamicLabelFromStack:self.touchMoveEventIntervalStack].text = sender.enabled ?
|
||
[NSString stringWithFormat:@" %d μs ", (uint16_t)self.touchMoveEventIntervalSlider.value] : @"";
|
||
}
|
||
|
||
|
||
- (void) slideToMenuDistanceSliderMoved:(UISlider* )sender{
|
||
UILabel* displayLabel = [self findDynamicLabelFromStack:_slideToSettingsDistanceStack];
|
||
// displayLabel.font = [UIFont systemFontOfSize:12 weight:UIFontWeightMedium];
|
||
NSString* labelText = [LocalizationHelper localizedStringForKey:@"%d%% screen width", (uint8_t)(sender.value * 100)];
|
||
displayLabel.text = [NSString stringWithFormat:@" %@ ", labelText];
|
||
}
|
||
|
||
- (void) bitrateSliderMoved {
|
||
assert(self.bitrateSlider.value < (sizeof(bitrateTable) / sizeof(*bitrateTable)));
|
||
_bitrate = bitrateTable[(int)self.bitrateSlider.value];
|
||
[self updateBitrateText];
|
||
}
|
||
|
||
- (void) updateBitrateText {
|
||
// Display bitrate in Mbps
|
||
UILabel* label = [self findDynamicLabelFromStack:self.bitrateStack];
|
||
[label setText:[LocalizationHelper localizedStringForKey:@" %.1f Mbps ", _bitrate / 1000.]];
|
||
}
|
||
|
||
- (NSInteger) getChosenFrameRate {
|
||
switch ([self.framerateSelector selectedSegmentIndex]) {
|
||
case 0:
|
||
return 30;
|
||
case 1:
|
||
return 60;
|
||
case 2:
|
||
return 120;
|
||
default:
|
||
abort();
|
||
}
|
||
}
|
||
|
||
- (uint32_t) getChosenCodecPreference {
|
||
// Auto is always the last segment
|
||
if (self.codecSelector.selectedSegmentIndex == self.codecSelector.numberOfSegments - 1) {
|
||
return CODEC_PREF_AUTO;
|
||
}
|
||
else {
|
||
switch (self.codecSelector.selectedSegmentIndex) {
|
||
case 0:
|
||
return CODEC_PREF_H264;
|
||
|
||
case 1:
|
||
return CODEC_PREF_HEVC;
|
||
|
||
case 2:
|
||
return CODEC_PREF_AV1;
|
||
|
||
default:
|
||
abort();
|
||
}
|
||
}
|
||
}
|
||
|
||
- (NSInteger) getChosenStreamHeight {
|
||
// because the 4k resolution can be removed
|
||
if (self.customResolutionSwitch.isOn) {
|
||
return resolutionTable[RESOLUTION_TABLE_CUSTOM_INDEX].height;
|
||
}
|
||
|
||
return resolutionTable[[self.resolutionSelector selectedSegmentIndex]].height;
|
||
}
|
||
|
||
- (NSInteger) getChosenStreamWidth {
|
||
// because the 4k resolution can be removed
|
||
if (self.customResolutionSwitch.isOn) {
|
||
return resolutionTable[RESOLUTION_TABLE_CUSTOM_INDEX].width;
|
||
}
|
||
|
||
return resolutionTable[[self.resolutionSelector selectedSegmentIndex]].width;
|
||
}
|
||
|
||
- (UIStackView *)findFlatStackViewFrom:(UIView *)view {
|
||
while (view != nil) {
|
||
if ([view isKindOfClass:[UIStackView class]]) {
|
||
UIStackView *stack = (UIStackView *)view;
|
||
BOOL hasNestedStack = NO;
|
||
for (UIView *sub in stack.arrangedSubviews) {
|
||
if ([sub isKindOfClass:[UIStackView class]]) {
|
||
hasNestedStack = YES;
|
||
break;
|
||
}
|
||
}
|
||
if (!hasNestedStack) {
|
||
return stack;
|
||
}
|
||
}
|
||
view = view.superview;
|
||
}
|
||
return nil;
|
||
}
|
||
|
||
|
||
- (void)updateThemeForLabels:(UIView *)view {
|
||
for (UIView *subview in view.subviews) {
|
||
if ([subview isKindOfClass:[UILabel class]]) {
|
||
UILabel *label = (UILabel *)subview;
|
||
if(label.accessibilityIdentifier != nil) break;
|
||
if (@available(iOS 13.0, *)) {
|
||
label.layer.filters = nil;
|
||
label.textColor = [ThemeManager textColor];
|
||
} else {
|
||
// Fallback on earlier versions
|
||
}
|
||
}
|
||
[self updateThemeForLabels:subview];
|
||
}
|
||
}
|
||
|
||
- (void)updateThemeForSelectors:(UIView *)view {
|
||
for (UIView *subview in view.subviews) {
|
||
if ([subview isKindOfClass:[UISegmentedControl class]]) {
|
||
UISegmentedControl *selector = (UISegmentedControl *)subview;
|
||
if (@available(iOS 13.0, *)) {
|
||
selector.selectedSegmentTintColor = [ThemeManager appSecondaryColor];
|
||
} else {
|
||
// Fallback on earlier versions
|
||
}
|
||
}
|
||
[self updateThemeForSelectors:subview];
|
||
}
|
||
}
|
||
|
||
- (void)updateThemeForSliders:(UIView *)view {
|
||
for (UIView *subview in view.subviews) {
|
||
if ([subview isKindOfClass:[UISlider class]]) {
|
||
UISlider *slider = (UISlider *)subview;
|
||
slider.tintColor = [ThemeManager appSecondaryColor];
|
||
|
||
}
|
||
[self updateThemeForSliders:subview];
|
||
}
|
||
}
|
||
|
||
|
||
- (void)updateTheme{
|
||
self.view.backgroundColor = [ThemeManager appBackgroundColor];
|
||
if (@available(iOS 13.0, *)) {
|
||
[self updateThemeForLabels:self.view];
|
||
[self updateThemeForSelectors:self.view];
|
||
[self updateThemeForSliders:self.view];
|
||
}
|
||
}
|
||
|
||
- (void) saveSettings {
|
||
DataManager* dataMan = [[DataManager alloc] init];
|
||
NSInteger framerate = [self getChosenFrameRate];
|
||
NSInteger height = [self getChosenStreamHeight];
|
||
NSInteger width = [self getChosenStreamWidth];
|
||
NSInteger onscreenControls = [self.onScreenWidgetSelector selectedSegmentIndex];
|
||
NSInteger keyboardToggleFingers = self.softKeyboardGestureSelector.selectedSegmentIndex == 3 ? 20 : self.softKeyboardGestureSelector.selectedSegmentIndex+3;
|
||
NSInteger oscLayoutToolFingers = (uint16_t)self->oswLayoutFingers;
|
||
|
||
CGFloat slideToSettingsDistance = self.slideToMenuDistanceSlider.value;
|
||
uint32_t slideToSettingsScreenEdge = [self getScreenEdgeFromSelector];
|
||
CGFloat pointerVelocityModeDivider = (CGFloat)(uint8_t)self.pointerVelocityModeDividerSlider.value/100;
|
||
CGFloat touchPointerVelocityFactor = (CGFloat)(uint16_t)[self map_velocFactorDisplay_fromSliderValue:self.touchPointerVelocityFactorSlider.value]/100;
|
||
CGFloat mousePointerVelocityFactor = (CGFloat)(uint16_t)[self map_velocFactorDisplay_fromSliderValue:self.mousePointerVelocityFactorSlider.value]/100;
|
||
|
||
uint16_t touchMoveEventInterval = (uint16_t)self.touchMoveEventIntervalSlider.value;
|
||
|
||
BOOL reverseMouseWheelDirection = [self.reverseMouseWheelDirectionSelector selectedSegmentIndex] == 1;
|
||
NSInteger asyncNativeTouchPriority = 1;
|
||
//BOOL liftStreamViewForKeyboard = [self.liftStreamViewForKeyboardSelector selectedSegmentIndex] == 1;
|
||
BOOL liftStreamViewForKeyboard = YES; // enable and hide this option
|
||
BOOL showKeyboardToolbar = self.softKeyboardToolbarSwitch.isOn;
|
||
BOOL optimizeGames = self.optimizeGamesSwitch.isOn;
|
||
BOOL multiController = self.multiControllerSwitch.isOn;
|
||
BOOL swapABXYButtons = [self.swapABXYButtonsSelector selectedSegmentIndex] == 1;
|
||
BOOL audioOnPC = self.audioOnPcSwitch.isOn;
|
||
uint32_t preferredCodec = [self getChosenCodecPreference];
|
||
BOOL enableYUV444 = self.yuv444Switch.isOn;
|
||
BOOL btMouseSupport = self.citrixX1MouseSwitch.isOn;
|
||
BOOL useFramePacing = [self.framePacingSelector selectedSegmentIndex] == 1;
|
||
NSInteger touchMode = [self isNotNativeTouchOnly] ? self.touchModeSelector.selectedSegmentIndex : NativeTouchOnly;
|
||
NSInteger statsOverlayLevel = [self.statsOverlaySelector selectedSegmentIndex];
|
||
BOOL statsOverlayEnabled = statsOverlayLevel != 0;
|
||
BOOL enableHdr = self.hdrSwitch.isOn;
|
||
BOOL unlockDisplayOrientation = [self.unlockDisplayOrientationSelector selectedSegmentIndex] == 1;
|
||
NSInteger resolutionSelected = [self.resolutionSelector selectedSegmentIndex];
|
||
NSInteger externalDisplayMode = [self.externalDisplayModeSelector selectedSegmentIndex];
|
||
NSInteger localMousePointerMode = [self.localMousePointerModeSelector selectedSegmentIndex];
|
||
[dataMan saveSettingsWithBitrate:_bitrate
|
||
framerate:framerate
|
||
height:height
|
||
width:width
|
||
audioConfig:2 // Stereo
|
||
onscreenControls:onscreenControls
|
||
keyboardToggleFingers:keyboardToggleFingers
|
||
oscLayoutToolFingers:oscLayoutToolFingers
|
||
slideToSettingsScreenEdge:slideToSettingsScreenEdge
|
||
slideToSettingsDistance:slideToSettingsDistance
|
||
pointerVelocityModeDivider:pointerVelocityModeDivider
|
||
touchPointerVelocityFactor:touchPointerVelocityFactor
|
||
mousePointerVelocityFactor:mousePointerVelocityFactor
|
||
touchMoveEventInterval:touchMoveEventInterval
|
||
reverseMouseWheelDirection:reverseMouseWheelDirection
|
||
asyncNativeTouchPriority:asyncNativeTouchPriority
|
||
liftStreamViewForKeyboard:liftStreamViewForKeyboard
|
||
showKeyboardToolbar:showKeyboardToolbar
|
||
optimizeGames:optimizeGames
|
||
multiController:multiController
|
||
swapABXYButtons:swapABXYButtons
|
||
audioOnPC:audioOnPC
|
||
preferredCodec:preferredCodec
|
||
enableYUV444:enableYUV444
|
||
useFramePacing:useFramePacing
|
||
enableHdr:enableHdr
|
||
btMouseSupport:btMouseSupport
|
||
touchMode:touchMode
|
||
statsOverlayLevel:statsOverlayLevel
|
||
statsOverlayEnabled:statsOverlayEnabled
|
||
unlockDisplayOrientation:unlockDisplayOrientation
|
||
resolutionSelected:resolutionSelected
|
||
externalDisplayMode:externalDisplayMode
|
||
localMousePointerMode:localMousePointerMode];
|
||
}
|
||
|
||
- (void)didReceiveMemoryWarning {
|
||
[super didReceiveMemoryWarning];
|
||
// Dispose of any resources that can be recreated.
|
||
}
|
||
|
||
|
||
#pragma mark - Navigation
|
||
|
||
- (void)prepareForSegue:(UIStoryboardSegue *)segue sender:(id)sender {
|
||
}
|
||
|
||
@end
|