refactor: remove unnecessary changes

This commit is contained in:
Acaki
2025-08-16 04:09:02 +08:00
parent 34eb2ccd31
commit b3664d0e2d
14 changed files with 95 additions and 864 deletions
@@ -32,8 +32,7 @@
@property (strong, nonatomic) IBOutlet UISegmentedControl *yuv444Selector; @property (strong, nonatomic) IBOutlet UISegmentedControl *yuv444Selector;
@property (strong, nonatomic) IBOutlet UISegmentedControl *pipSelector; @property (strong, nonatomic) IBOutlet UISegmentedControl *pipSelector;
@property (strong, nonatomic) IBOutlet UISegmentedControl *hdrSelector; @property (strong, nonatomic) IBOutlet UISegmentedControl *hdrSelector;
@property (strong, nonatomic) IBOutlet UILabel *frameQueueSizeLabel; @property (strong, nonatomic) IBOutlet UISegmentedControl *framePacingSelector;
@property (strong, nonatomic) IBOutlet UISlider *frameQueueSizeSlider;
@property (strong, nonatomic) IBOutlet UISegmentedControl *btMouseSelector; @property (strong, nonatomic) IBOutlet UISegmentedControl *btMouseSelector;
@property (strong, nonatomic) IBOutlet UISegmentedControl *reverseMouseWheelDirectionSelector; @property (strong, nonatomic) IBOutlet UISegmentedControl *reverseMouseWheelDirectionSelector;
@property (strong, nonatomic) IBOutlet UISegmentedControl *statsOverlaySelector; @property (strong, nonatomic) IBOutlet UISegmentedControl *statsOverlaySelector;
@@ -63,10 +62,6 @@
@property (strong, nonatomic) IBOutlet UISegmentedControl *externalDisplayModeSelector; @property (strong, nonatomic) IBOutlet UISegmentedControl *externalDisplayModeSelector;
@property (strong, nonatomic) IBOutlet UISegmentedControl *localMousePointerModeSelector; @property (strong, nonatomic) IBOutlet UISegmentedControl *localMousePointerModeSelector;
@property (strong, nonatomic) IBOutlet UILabel *enableGraphsLabel;
@property (strong, nonatomic) IBOutlet UISegmentedControl *enableGraphsSelector;
@property (strong, nonatomic) IBOutlet UIStepper *graphOpacityStepper;
@property (strong, nonatomic) IBOutlet UISegmentedControl *renderingBackendSelector;
#pragma clang diagnostic push #pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wunguarded-availability" #pragma clang diagnostic ignored "-Wunguarded-availability"
@@ -22,8 +22,6 @@
bool justEnteredSettingsViewDoNotOpenOscLayoutTool; bool justEnteredSettingsViewDoNotOpenOscLayoutTool;
uint16_t oscLayoutFingers; uint16_t oscLayoutFingers;
CustomEdgeSlideGestureRecognizer *slideToCloseSettingsViewRecognizer; CustomEdgeSlideGestureRecognizer *slideToCloseSettingsViewRecognizer;
NSInteger _frameQueueSize;
NSInteger _graphOpacity;
} }
@dynamic overrideUserInterfaceStyle; @dynamic overrideUserInterfaceStyle;
@@ -201,7 +199,7 @@ BOOL isCustomResolution(CGSize res) {
} }
for (int i = 0; i < RESOLUTION_TABLE_CUSTOM_INDEX; i++) { 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)) { if ((res.width == resolutionTable[i].width && res.height == resolutionTable[i].height) || (res.height == resolutionTable[i].width && res.width == resolutionTable[i].height)) {
return NO; return NO;
} }
@@ -217,7 +215,7 @@ BOOL isCustomResolution(CGSize res) {
- (bool)isFullScreenRequired { - (bool)isFullScreenRequired {
NSDictionary *infoDictionary = [[NSBundle mainBundle] infoDictionary]; NSDictionary *infoDictionary = [[NSBundle mainBundle] infoDictionary];
NSNumber *requiresFullScreen = infoDictionary[@"UIRequiresFullScreen"]; NSNumber *requiresFullScreen = infoDictionary[@"UIRequiresFullScreen"];
if (requiresFullScreen != nil) { if (requiresFullScreen != nil) {
return [requiresFullScreen boolValue]; return [requiresFullScreen boolValue];
} }
@@ -242,7 +240,7 @@ BOOL isCustomResolution(CGSize res) {
appWindowHeight = bounds.size.height * screenScale; appWindowHeight = bounds.size.height * screenScale;
} }
bool needSwapWidthAndHeight = appWindowWidth > appWindowHeight; bool needSwapWidthAndHeight = appWindowWidth > appWindowHeight;
resolutionTable[4] = CGSizeMake(safeAreaWidth, appWindowHeight); resolutionTable[4] = CGSizeMake(safeAreaWidth, appWindowHeight);
for(uint8_t i=0;i<7;i++){ for(uint8_t i=0;i<7;i++){
@@ -342,7 +340,7 @@ BOOL isCustomResolution(CGSize res) {
- (void)viewDidLoad { - (void)viewDidLoad {
//[self pushDownExistingWidgets]; //[self pushDownExistingWidgets];
//[self addExitButtonOnTop]; //[self addExitButtonOnTop];
self->slideToCloseSettingsViewRecognizer = [[CustomEdgeSlideGestureRecognizer alloc] initWithTarget:self action:@selector(edgeSwiped)]; self->slideToCloseSettingsViewRecognizer = [[CustomEdgeSlideGestureRecognizer alloc] initWithTarget:self action:@selector(edgeSwiped)];
slideToCloseSettingsViewRecognizer.edges = UIRectEdgeLeft; slideToCloseSettingsViewRecognizer.edges = UIRectEdgeLeft;
slideToCloseSettingsViewRecognizer.normalizedThresholdDistance = 0.0; slideToCloseSettingsViewRecognizer.normalizedThresholdDistance = 0.0;
@@ -351,13 +349,13 @@ BOOL isCustomResolution(CGSize res) {
slideToCloseSettingsViewRecognizer.delaysTouchesBegan = NO; slideToCloseSettingsViewRecognizer.delaysTouchesBegan = NO;
slideToCloseSettingsViewRecognizer.delaysTouchesEnded = NO; slideToCloseSettingsViewRecognizer.delaysTouchesEnded = NO;
[self.view addGestureRecognizer:slideToCloseSettingsViewRecognizer]; [self.view addGestureRecognizer:slideToCloseSettingsViewRecognizer];
justEnteredSettingsViewDoNotOpenOscLayoutTool = true; justEnteredSettingsViewDoNotOpenOscLayoutTool = true;
[[NSNotificationCenter defaultCenter] addObserver:self [[NSNotificationCenter defaultCenter] addObserver:self
selector:@selector(deviceOrientationDidChange) // handle orientation change since i made portrait mode available selector:@selector(deviceOrientationDidChange) // handle orientation change since i made portrait mode available
name:UIDeviceOrientationDidChangeNotification name:UIDeviceOrientationDidChangeNotification
object:nil]; object:nil];
// Always run settings in dark mode because we want the light fonts // Always run settings in dark mode because we want the light fonts
if (@available(iOS 13.0, tvOS 13.0, *)) { if (@available(iOS 13.0, tvOS 13.0, *)) {
self.overrideUserInterfaceStyle = UIUserInterfaceStyleDark; self.overrideUserInterfaceStyle = UIUserInterfaceStyleDark;
@@ -368,8 +366,6 @@ BOOL isCustomResolution(CGSize res) {
// Ensure we pick a bitrate that falls exactly onto a slider notch // Ensure we pick a bitrate that falls exactly onto a slider notch
_bitrate = bitrateTable[[self getSliderValueForBitrate:[currentSettings.bitrate intValue]]]; _bitrate = bitrateTable[[self getSliderValueForBitrate:[currentSettings.bitrate intValue]]];
_frameQueueSize = [currentSettings.frameQueueSize intValue];
_graphOpacity = [currentSettings.graphOpacity intValue];
// Get the size of the screen with and without safe area insets // Get the size of the screen with and without safe area insets
UIWindow *window = UIApplication.sharedApplication.windows.firstObject; UIWindow *window = UIApplication.sharedApplication.windows.firstObject;
@@ -383,26 +379,20 @@ BOOL isCustomResolution(CGSize res) {
UITapGestureRecognizer *resolutionDisplayViewTap = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(resolutionDisplayViewTapped:)]; UITapGestureRecognizer *resolutionDisplayViewTap = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(resolutionDisplayViewTapped:)];
[self.resolutionDisplayView addGestureRecognizer:resolutionDisplayViewTap]; [self.resolutionDisplayView addGestureRecognizer:resolutionDisplayViewTap];
resolutionTable[0] = CGSizeMake(1280, 720); resolutionTable[0] = CGSizeMake(640, 360);
resolutionTable[1] = CGSizeMake(1920, 1080); resolutionTable[1] = CGSizeMake(1280, 720);
resolutionTable[2] = CGSizeMake(2560, 1440); resolutionTable[2] = CGSizeMake(1920, 1080);
resolutionTable[3] = CGSizeMake(3840, 2160); resolutionTable[3] = CGSizeMake(3840, 2160);
resolutionTable[4] = CGSizeMake(safeAreaWidth, fullScreenHeight); resolutionTable[4] = CGSizeMake(safeAreaWidth, fullScreenHeight);
resolutionTable[5] = CGSizeMake(fullScreenWidth, fullScreenHeight); resolutionTable[5] = CGSizeMake(fullScreenWidth, fullScreenHeight);
resolutionTable[6] = CGSizeMake([currentSettings.width integerValue], [currentSettings.height integerValue]); // custom initial value resolutionTable[6] = CGSizeMake([currentSettings.width integerValue], [currentSettings.height integerValue]); // custom initial value
[self updateResolutionTable]; [self updateResolutionTable];
// Don't populate the custom entry unless we have a custom resolution // Don't populate the custom entry unless we have a custom resolution
if (!isCustomResolution(resolutionTable[6])) { if (!isCustomResolution(resolutionTable[6])) {
resolutionTable[6] = CGSizeMake(0, 0); resolutionTable[6] = CGSizeMake(0, 0);
} }
// Customize framerate list for ProMotion devices
if ([[UIScreen mainScreen] maximumFramesPerSecond] >= 90) {
[self.framerateSelector setEnabled:YES forSegmentAtIndex:2]; // enable 90
[self.framerateSelector setEnabled:YES forSegmentAtIndex:3]; // enable 120
}
NSInteger framerate; NSInteger framerate;
switch ([currentSettings.framerate integerValue]) { switch ([currentSettings.framerate integerValue]) {
case 30: case 30:
@@ -412,11 +402,8 @@ BOOL isCustomResolution(CGSize res) {
case 60: case 60:
framerate = 1; framerate = 1;
break; break;
case 90:
framerate = 2;
break;
case 120: case 120:
framerate = 3; framerate = 2;
break; break;
} }
@@ -425,6 +412,17 @@ BOOL isCustomResolution(CGSize res) {
resolution = 0; 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 // Disable codec selector segments for unsupported codecs
#if defined(__IPHONE_16_0) || defined(__TVOS_16_0) #if defined(__IPHONE_16_0) || defined(__TVOS_16_0)
if (!VTIsHardwareDecodeSupported(kCMVideoCodecType_AV1)) if (!VTIsHardwareDecodeSupported(kCMVideoCodecType_AV1))
@@ -465,27 +463,13 @@ BOOL isCustomResolution(CGSize res) {
else { else {
[self.hdrSelector setSelectedSegmentIndex:currentSettings.enableHdr ? 1 : 0]; [self.hdrSelector setSelectedSegmentIndex:currentSettings.enableHdr ? 1 : 0];
} }
[self.yuv444Selector setSelectedSegmentIndex:currentSettings.enableYUV444 ? 1 : 0]; [self.yuv444Selector setSelectedSegmentIndex:currentSettings.enableYUV444 ? 1 : 0];
[self.pipSelector setSelectedSegmentIndex:currentSettings.enablePIP ? 1 : 0]; [self.pipSelector setSelectedSegmentIndex:currentSettings.enablePIP ? 1 : 0];
[self.statsOverlaySelector setSelectedSegmentIndex:currentSettings.statsOverlayLevel.intValue]; [self.statsOverlaySelector setSelectedSegmentIndex:currentSettings.statsOverlayLevel.intValue];
NSInteger renderingBackend = [currentSettings.renderingBackend integerValue];
[self.renderingBackendSelector setSelectedSegmentIndex:renderingBackend];
[self.enableGraphsSelector setSelectedSegmentIndex:currentSettings.enableGraphs ? 1 : 0];
[self.enableGraphsSelector addTarget:self action:@selector(enableGraphsChanged) forControlEvents:UIControlEventValueChanged];
[self enableGraphsChanged];
[self.graphOpacityStepper setMinimumValue:0];
[self.graphOpacityStepper setMaximumValue:100];
[self.graphOpacityStepper setValue:_graphOpacity];
[self.graphOpacityStepper addTarget:self action:@selector(graphOpacityStepperMoved) forControlEvents:UIControlEventValueChanged];
[self updateGraphOpacityText];
[self.btMouseSelector setSelectedSegmentIndex:currentSettings.btMouseSupport ? 1 : 0]; [self.btMouseSelector setSelectedSegmentIndex:currentSettings.btMouseSupport ? 1 : 0];
[self.optimizeSettingsSelector setSelectedSegmentIndex:currentSettings.optimizeGames ? 1 : 0]; [self.optimizeSettingsSelector setSelectedSegmentIndex:currentSettings.optimizeGames ? 1 : 0];
[self.frameQueueSizeSlider setMinimumValue:1]; [self.framePacingSelector setSelectedSegmentIndex:currentSettings.useFramePacing ? 1 : 0];
[self.frameQueueSizeSlider setMaximumValue:5];
[self.frameQueueSizeSlider setValue:_frameQueueSize];
[self.frameQueueSizeSlider addTarget:self action:@selector(frameQueueSizeSliderMoved) forControlEvents:UIControlEventValueChanged];
[self updateFrameQueueSizeText];
[self.multiControllerSelector setSelectedSegmentIndex:currentSettings.multiController ? 1 : 0]; [self.multiControllerSelector setSelectedSegmentIndex:currentSettings.multiController ? 1 : 0];
[self.swapABXYButtonsSelector setSelectedSegmentIndex:currentSettings.swapABXYButtons ? 1 : 0]; [self.swapABXYButtonsSelector setSelectedSegmentIndex:currentSettings.swapABXYButtons ? 1 : 0];
[self.audioOnPCSelector setSelectedSegmentIndex:currentSettings.playAudioOnPC ? 1 : 0]; [self.audioOnPCSelector setSelectedSegmentIndex:currentSettings.playAudioOnPC ? 1 : 0];
@@ -502,20 +486,20 @@ BOOL isCustomResolution(CGSize res) {
[self.bitrateSlider addTarget:self action:@selector(bitrateSliderMoved) forControlEvents:UIControlEventValueChanged]; [self.bitrateSlider addTarget:self action:@selector(bitrateSliderMoved) forControlEvents:UIControlEventValueChanged];
[self updateBitrateText]; [self updateBitrateText];
[self updateResolutionDisplayViewText]; [self updateResolutionDisplayViewText];
// Unlock Display Orientation setting // Unlock Display Orientation setting
bool unlockDisplayOrientationSelectorEnabled = [self isFullScreenRequired];//need "requires fullscreen" enabled in the app bunddle to make runtime orientation limitation woring bool unlockDisplayOrientationSelectorEnabled = [self isFullScreenRequired];//need "requires fullscreen" enabled in the app bunddle to make runtime orientation limitation woring
if(unlockDisplayOrientationSelectorEnabled) [self.unlockDisplayOrientationSelector setSelectedSegmentIndex:currentSettings.unlockDisplayOrientation ? 1 : 0]; 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 else [self.unlockDisplayOrientationSelector setSelectedSegmentIndex:1]; // can't lock screen orientation in this mode = Display Orientation always unlocked
[self.unlockDisplayOrientationSelector setEnabled:unlockDisplayOrientationSelectorEnabled]; [self.unlockDisplayOrientationSelector setEnabled:unlockDisplayOrientationSelectorEnabled];
// lift streamview setting // lift streamview setting
[self.liftStreamViewForKeyboardSelector setSelectedSegmentIndex:currentSettings.liftStreamViewForKeyboard ? 1 : 0];// Load old setting [self.liftStreamViewForKeyboardSelector setSelectedSegmentIndex:currentSettings.liftStreamViewForKeyboard ? 1 : 0];// Load old setting
// showkeyboard toolbar setting // showkeyboard toolbar setting
[self.showKeyboardToolbarSelector setSelectedSegmentIndex:currentSettings.showKeyboardToolbar ? 1 : 0];// Load old setting [self.showKeyboardToolbarSelector setSelectedSegmentIndex:currentSettings.showKeyboardToolbar ? 1 : 0];// Load old setting
// reverse mouse wheel direction setting // reverse mouse wheel direction setting
[self.reverseMouseWheelDirectionSelector setSelectedSegmentIndex:currentSettings.reverseMouseWheelDirection ? 1 : 0];// Load old setting [self.reverseMouseWheelDirectionSelector setSelectedSegmentIndex:currentSettings.reverseMouseWheelDirection ? 1 : 0];// Load old setting
@@ -529,11 +513,11 @@ BOOL isCustomResolution(CGSize res) {
[self.slideToMenuDistanceSlider setValue:currentSettings.slideToSettingsDistance.floatValue]; [self.slideToMenuDistanceSlider setValue:currentSettings.slideToSettingsDistance.floatValue];
[self.slideToMenuDistanceSlider addTarget:self action:@selector(slideToMenuDistanceSliderMoved) forControlEvents:(UIControlEventValueChanged)]; // Update label display when slider is being moved. [self.slideToMenuDistanceSlider addTarget:self action:@selector(slideToMenuDistanceSliderMoved) forControlEvents:(UIControlEventValueChanged)]; // Update label display when slider is being moved.
[self slideToMenuDistanceSliderMoved]; [self slideToMenuDistanceSliderMoved];
//TouchMode & OSC Related Settings: //TouchMode & OSC Related Settings:
// pointer veloc setting, will be enable/disabled by touchMode // 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 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.pointerVelocityModeDividerSlider addTarget:self action:@selector(pointerVelocityModeDividerSliderMoved) forControlEvents:(UIControlEventValueChanged)]; // Update label display when slider is being moved.
@@ -543,13 +527,13 @@ BOOL isCustomResolution(CGSize res) {
[self.touchPointerVelocityFactorSlider setValue: [self map_SliderValue_fromVelocFactor: currentSettings.touchPointerVelocityFactor.floatValue] animated:YES]; // Load old setting. [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.touchPointerVelocityFactorSlider addTarget:self action:@selector(touchPointerVelocityFactorSliderMoved) forControlEvents:(UIControlEventValueChanged)]; // Update label display when slider is being moved.
[self touchPointerVelocityFactorSliderMoved]; [self touchPointerVelocityFactorSliderMoved];
// init relative touch mouse pointer veloc setting, will be enable/disabled by touchMode // 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 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.mousePointerVelocityFactorSlider addTarget:self action:@selector(mousePointerVelocityFactorSliderMoved) forControlEvents:(UIControlEventValueChanged)]; // Update label display when slider is being moved.
[self mousePointerVelocityFactorSliderMoved]; [self mousePointerVelocityFactorSliderMoved];
// these settings will be affected by onscreenControl & touchMode, must be loaded before them. // these settings will be affected by onscreenControl & touchMode, must be loaded before them.
// NSLog(@"osc tool fingers setting test: %d", currentSettings.oscLayoutToolFingers.intValue); // NSLog(@"osc tool fingers setting test: %d", currentSettings.oscLayoutToolFingers.intValue);
self->oscLayoutFingers = (uint16_t)currentSettings.oscLayoutToolFingers.intValue; // load old setting of oscLayoutFingers self->oscLayoutFingers = (uint16_t)currentSettings.oscLayoutToolFingers.intValue; // load old setting of oscLayoutFingers
@@ -563,7 +547,7 @@ BOOL isCustomResolution(CGSize res) {
[self.onscreenControlSelector addTarget:self action:@selector(onscreenControlChanged) forControlEvents:UIControlEventValueChanged]; [self.onscreenControlSelector addTarget:self action:@selector(onscreenControlChanged) forControlEvents:UIControlEventValueChanged];
[self onscreenControlChanged]; [self onscreenControlChanged];
[self.largerStickLR1Selector setSelectedSegmentIndex:currentSettings.largerStickLR1 ? 1 : 0]; // load old setting of largerStickLR1 [self.largerStickLR1Selector setSelectedSegmentIndex:currentSettings.largerStickLR1 ? 1 : 0]; // load old setting of largerStickLR1
// tap exclusion area size for custom OSC, must be loaded before touchMode & osc selector. // tap exclusion area size for custom OSC, must be loaded before touchMode & osc selector.
[self.oscTapExlusionAreaSizeSlider setValue:currentSettings.oscTapExlusionAreaSize.floatValue * 100 animated:YES]; // Load old setting. [self.oscTapExlusionAreaSizeSlider setValue:currentSettings.oscTapExlusionAreaSize.floatValue * 100 animated:YES]; // Load old setting.
[self.oscTapExlusionAreaSizeSlider addTarget:self action:@selector(oscTapExlusionAreaSizeSliderMoved) forControlEvents:(UIControlEventValueChanged)]; // Update label display when slider is being moved. [self.oscTapExlusionAreaSizeSlider addTarget:self action:@selector(oscTapExlusionAreaSizeSliderMoved) forControlEvents:(UIControlEventValueChanged)]; // Update label display when slider is being moved.
@@ -576,7 +560,7 @@ BOOL isCustomResolution(CGSize res) {
[self.touchModeSelector addTarget:self action:@selector(touchModeChanged) forControlEvents:UIControlEventValueChanged]; [self.touchModeSelector addTarget:self action:@selector(touchModeChanged) forControlEvents:UIControlEventValueChanged];
[self touchModeChanged]; [self touchModeChanged];
// init CustomOSC stuff // init CustomOSC stuff
/* sets a reference to the correct 'LayoutOnScreenControlsViewController' depending on whether the user is on an iPhone or iPad */ /* sets a reference to the correct 'LayoutOnScreenControlsViewController' depending on whether the user is on an iPhone or iPad */
self.layoutOnScreenControlsVC = [[LayoutOnScreenControlsViewController alloc] init]; self.layoutOnScreenControlsVC = [[LayoutOnScreenControlsViewController alloc] init];
@@ -620,16 +604,16 @@ BOOL isCustomResolution(CGSize res) {
UIAlertController *alertController = [UIAlertController alertControllerWithTitle:[LocalizationHelper localizedStringForKey:@"Rebase in Stream View"] UIAlertController *alertController = [UIAlertController alertControllerWithTitle:[LocalizationHelper localizedStringForKey:@"Rebase in Stream View"]
message:[LocalizationHelper localizedStringForKey:@"Tap %d fingers to change layout in stream view, or change the number of fingers required to:", self->oscLayoutFingers] message:[LocalizationHelper localizedStringForKey:@"Tap %d fingers to change layout in stream view, or change the number of fingers required to:", self->oscLayoutFingers]
preferredStyle:UIAlertControllerStyleAlert]; preferredStyle:UIAlertControllerStyleAlert];
[alertController addTextFieldWithConfigurationHandler:^(UITextField *textField) { [alertController addTextFieldWithConfigurationHandler:^(UITextField *textField) {
textField.placeholder = [LocalizationHelper localizedStringForKey:@"%d", self->oscLayoutFingers]; textField.placeholder = [LocalizationHelper localizedStringForKey:@"%d", self->oscLayoutFingers];
textField.keyboardType = UIKeyboardTypeNumberPad; textField.keyboardType = UIKeyboardTypeNumberPad;
}]; }];
UIAlertAction *cancelAction = [UIAlertAction actionWithTitle:[LocalizationHelper localizedStringForKey:@"Cancel"] UIAlertAction *cancelAction = [UIAlertAction actionWithTitle:[LocalizationHelper localizedStringForKey:@"Cancel"]
style:UIAlertActionStyleCancel style:UIAlertActionStyleCancel
handler:nil]; handler:nil];
UIAlertAction *okAction = [UIAlertAction actionWithTitle:[LocalizationHelper localizedStringForKey:@"OK"] UIAlertAction *okAction = [UIAlertAction actionWithTitle:[LocalizationHelper localizedStringForKey:@"OK"]
style:UIAlertActionStyleDefault style:UIAlertActionStyleDefault
handler:^(UIAlertAction *action) { handler:^(UIAlertAction *action) {
@@ -642,16 +626,16 @@ BOOL isCustomResolution(CGSize res) {
} else { } else {
NSLog(@"OK button tapped with no change"); NSLog(@"OK button tapped with no change");
} }
// Continue execution after the alert is dismissed // Continue execution after the alert is dismissed
if (!self->_mainFrameViewController.settingsExpandedInStreamView) { if (!self->_mainFrameViewController.settingsExpandedInStreamView) {
[self invokeOscLayout]; // Don't open osc layout tool immediately during streaming [self invokeOscLayout]; // Don't open osc layout tool immediately during streaming
} }
[self.onscreenControllerLabel setText:[LocalizationHelper localizedStringForKey: @"Tap %d Fingers to Change OSC Layout in Stream View", self->oscLayoutFingers]]; //update the osc label [self.onscreenControllerLabel setText:[LocalizationHelper localizedStringForKey: @"Tap %d Fingers to Change OSC Layout in Stream View", self->oscLayoutFingers]]; //update the osc label
[self keyboardToggleFingerNumSliderMoved]; //update keyboard toggle number; [self keyboardToggleFingerNumSliderMoved]; //update keyboard toggle number;
}]; }];
[alertController addAction:cancelAction]; [alertController addAction:cancelAction];
[alertController addAction:okAction]; [alertController addAction:okAction];
[self presentViewController:alertController animated:YES completion:nil]; [self presentViewController:alertController animated:YES completion:nil];
@@ -666,7 +650,7 @@ BOOL isCustomResolution(CGSize res) {
- (void)onscreenControlChanged{ - (void)onscreenControlChanged{
BOOL isIPhone = ([UIDevice currentDevice].userInterfaceIdiom == UIUserInterfaceIdiomPhone); BOOL isIPhone = ([UIDevice currentDevice].userInterfaceIdiom == UIUserInterfaceIdiomPhone);
if (isIPhone) { if (isIPhone) {
UIStoryboard *storyboard = [UIStoryboard storyboardWithName:@"iPhone" bundle:nil]; UIStoryboard *storyboard = [UIStoryboard storyboardWithName:@"iPhone" bundle:nil];
@@ -677,7 +661,7 @@ BOOL isCustomResolution(CGSize res) {
self.layoutOnScreenControlsVC = [storyboard instantiateViewControllerWithIdentifier:@"LayoutOnScreenControlsViewController"]; self.layoutOnScreenControlsVC = [storyboard instantiateViewControllerWithIdentifier:@"LayoutOnScreenControlsViewController"];
self.layoutOnScreenControlsVC.modalPresentationStyle = UIModalPresentationFullScreen; self.layoutOnScreenControlsVC.modalPresentationStyle = UIModalPresentationFullScreen;
} }
bool customOscEnabled = [self isOnScreenControllerOrButtonEnabled] && [self.onscreenControlSelector selectedSegmentIndex] == OnScreenControlsLevelCustom; bool customOscEnabled = [self isOnScreenControllerOrButtonEnabled] && [self.onscreenControlSelector selectedSegmentIndex] == OnScreenControlsLevelCustom;
// [self widget:self.keyboardToggleFingerNumSlider setEnabled:!customOscEnabled]; // [self widget:self.keyboardToggleFingerNumSlider setEnabled:!customOscEnabled];
[self widget:self.oscTapExlusionAreaSizeSlider setEnabled:customOscEnabled]; [self widget:self.oscTapExlusionAreaSizeSlider setEnabled:customOscEnabled];
@@ -764,10 +748,10 @@ BOOL isCustomResolution(CGSize res) {
bool oscSelectorEnabled = [self.touchModeSelector selectedSegmentIndex] == RELATIVE_TOUCH || [self.touchModeSelector selectedSegmentIndex] == REGULAR_NATIVE_TOUCH || [self.touchModeSelector selectedSegmentIndex] == ABSOLUTE_TOUCH; bool oscSelectorEnabled = [self.touchModeSelector selectedSegmentIndex] == RELATIVE_TOUCH || [self.touchModeSelector selectedSegmentIndex] == REGULAR_NATIVE_TOUCH || [self.touchModeSelector selectedSegmentIndex] == ABSOLUTE_TOUCH;
bool customOscEnabled = [self isOnScreenControllerOrButtonEnabled] && [self.onscreenControlSelector selectedSegmentIndex] == OnScreenControlsLevelCustom; bool customOscEnabled = [self isOnScreenControllerOrButtonEnabled] && [self.onscreenControlSelector selectedSegmentIndex] == OnScreenControlsLevelCustom;
bool isNativeTouch = [self.touchModeSelector selectedSegmentIndex] == PURE_NATIVE_TOUCH || [self.touchModeSelector selectedSegmentIndex] == REGULAR_NATIVE_TOUCH; bool isNativeTouch = [self.touchModeSelector selectedSegmentIndex] == PURE_NATIVE_TOUCH || [self.touchModeSelector selectedSegmentIndex] == REGULAR_NATIVE_TOUCH;
[self.onscreenControlSelector setEnabled:oscSelectorEnabled]; [self.onscreenControlSelector setEnabled:oscSelectorEnabled];
[self.largerStickLR1Selector setEnabled:oscSelectorEnabled]; // this selector stay aligned with oscSelector [self.largerStickLR1Selector setEnabled:oscSelectorEnabled]; // this selector stay aligned with oscSelector
[self widget:self.pointerVelocityModeDividerSlider setEnabled:isNativeTouch]; // pointer velocity scaling works only in native touch mode. [self widget:self.pointerVelocityModeDividerSlider setEnabled:isNativeTouch]; // pointer velocity scaling works only in native touch mode.
[self widget:self.touchPointerVelocityFactorSlider setEnabled:isNativeTouch]; // pointer velocity scaling works only in native touch mode. [self widget:self.touchPointerVelocityFactorSlider setEnabled:isNativeTouch]; // pointer velocity scaling works only in native touch mode.
[self widget:self.mousePointerVelocityFactorSlider setEnabled:[self.touchModeSelector selectedSegmentIndex] == RELATIVE_TOUCH]; // mouse velocity scaling works only in relative touch mode. [self widget:self.mousePointerVelocityFactorSlider setEnabled:[self.touchModeSelector selectedSegmentIndex] == RELATIVE_TOUCH]; // mouse velocity scaling works only in relative touch mode.
@@ -845,7 +829,7 @@ BOOL isCustomResolution(CGSize res) {
} }
defaultBitrate = round(resolutionFactor * frameRateFactor) * 1000; defaultBitrate = round(resolutionFactor * frameRateFactor) * 1000;
_bitrate = MIN(defaultBitrate, 150000); _bitrate = MIN(defaultBitrate, 100000);
[self.bitrateSlider setValue:[self getSliderValueForBitrate:_bitrate] animated:YES]; [self.bitrateSlider setValue:[self getSliderValueForBitrate:_bitrate] animated:YES];
[self updateBitrateText]; [self updateBitrateText];
@@ -983,14 +967,14 @@ BOOL isCustomResolution(CGSize res) {
- (void) keyboardToggleFingerNumSliderMoved{ - (void) keyboardToggleFingerNumSliderMoved{
// bool oscEnabled = [self isOnScreenControllerOrButtonEnabled]; // bool oscEnabled = [self isOnScreenControllerOrButtonEnabled];
bool customOscEnabled = [self isOnScreenControllerOrButtonEnabled] && [self.onscreenControlSelector selectedSegmentIndex] == OnScreenControlsLevelCustom; bool customOscEnabled = [self isOnScreenControllerOrButtonEnabled] && [self.onscreenControlSelector selectedSegmentIndex] == OnScreenControlsLevelCustom;
CGFloat sliderValue = self.keyboardToggleFingerNumSlider.value; CGFloat sliderValue = self.keyboardToggleFingerNumSlider.value;
if(customOscEnabled){ if(customOscEnabled){
// exclude self->oscLayoutFingers when custom osc is enabled // exclude self->oscLayoutFingers when custom osc is enabled
if(sliderValue > self->oscLayoutFingers - 1 && sliderValue < self->oscLayoutFingers) [self.keyboardToggleFingerNumSlider setValue: self->oscLayoutFingers - 1]; if(sliderValue > self->oscLayoutFingers - 1 && sliderValue < self->oscLayoutFingers) [self.keyboardToggleFingerNumSlider setValue: self->oscLayoutFingers - 1];
if(sliderValue >= self->oscLayoutFingers && sliderValue < self->oscLayoutFingers + 1) [self.keyboardToggleFingerNumSlider setValue: self->oscLayoutFingers + 1]; if(sliderValue >= self->oscLayoutFingers && sliderValue < self->oscLayoutFingers + 1) [self.keyboardToggleFingerNumSlider setValue: self->oscLayoutFingers + 1];
} }
sliderValue = self.keyboardToggleFingerNumSlider.value; sliderValue = self.keyboardToggleFingerNumSlider.value;
if(sliderValue > 10.5f) [self.keyboardToggleFingerNumLabel setText:[LocalizationHelper localizedStringForKey:@"Local Keyboard Toggle Disabled"]]; if(sliderValue > 10.5f) [self.keyboardToggleFingerNumLabel setText:[LocalizationHelper localizedStringForKey:@"Local Keyboard Toggle Disabled"]];
else [self.keyboardToggleFingerNumLabel setText:[LocalizationHelper localizedStringForKey:@"To Toggle Local Keyboard: Tap %d Fingers", (uint16_t)sliderValue]]; // Initiate label display, exclude 5 fingers. else [self.keyboardToggleFingerNumLabel setText:[LocalizationHelper localizedStringForKey:@"To Toggle Local Keyboard: Tap %d Fingers", (uint16_t)sliderValue]]; // Initiate label display, exclude 5 fingers.
@@ -1019,8 +1003,6 @@ BOOL isCustomResolution(CGSize res) {
case 1: case 1:
return 60; return 60;
case 2: case 2:
return 90;
case 3:
return 120; return 120;
default: default:
abort(); abort();
@@ -1069,30 +1051,6 @@ BOOL isCustomResolution(CGSize res) {
return resolutionTable[[self.resolutionSelector selectedSegmentIndex]].width; return resolutionTable[[self.resolutionSelector selectedSegmentIndex]].width;
} }
- (void) frameQueueSizeSliderMoved {
assert(self.frameQueueSizeSlider.value >= 0 && self.frameQueueSizeSlider.value <= 5);
_frameQueueSize = (int)self.frameQueueSizeSlider.value;
[self updateFrameQueueSizeText];
}
- (void) updateFrameQueueSizeText {
[self.frameQueueSizeLabel setText:[NSString stringWithFormat:@"Frames to buffer: %ld", _frameQueueSize ]];
}
- (void) enableGraphsChanged {
[self.graphOpacityStepper setEnabled:[self.enableGraphsSelector selectedSegmentIndex] == 1 ? YES : NO];
}
- (void) graphOpacityStepperMoved {
assert(self.graphOpacityStepper.value >= 0 && self.graphOpacityStepper.value <= 100);
_graphOpacity = (int)self.graphOpacityStepper.value;
[self updateGraphOpacityText];
}
- (void) updateGraphOpacityText {
[self.enableGraphsLabel setText:[NSString stringWithFormat:@"Performance Graphs - Opacity: %ld%%", _graphOpacity ]];
}
- (void) saveSettings { - (void) saveSettings {
DataManager* dataMan = [[DataManager alloc] init]; DataManager* dataMan = [[DataManager alloc] init];
NSInteger framerate = [self getChosenFrameRate]; NSInteger framerate = [self getChosenFrameRate];
@@ -1116,7 +1074,6 @@ BOOL isCustomResolution(CGSize res) {
BOOL largerStickLR1 = [self.largerStickLR1Selector selectedSegmentIndex] == 1; BOOL largerStickLR1 = [self.largerStickLR1Selector selectedSegmentIndex] == 1;
BOOL liftStreamViewForKeyboard = [self.liftStreamViewForKeyboardSelector selectedSegmentIndex] == 1; BOOL liftStreamViewForKeyboard = [self.liftStreamViewForKeyboardSelector selectedSegmentIndex] == 1;
BOOL showKeyboardToolbar = [self.showKeyboardToolbarSelector selectedSegmentIndex] == 1; BOOL showKeyboardToolbar = [self.showKeyboardToolbarSelector selectedSegmentIndex] == 1;
NSInteger renderingBackend = [self.renderingBackendSelector selectedSegmentIndex];
BOOL optimizeGames = [self.optimizeSettingsSelector selectedSegmentIndex] == 1; BOOL optimizeGames = [self.optimizeSettingsSelector selectedSegmentIndex] == 1;
BOOL multiController = [self.multiControllerSelector selectedSegmentIndex] == 1; BOOL multiController = [self.multiControllerSelector selectedSegmentIndex] == 1;
BOOL swapABXYButtons = [self.swapABXYButtonsSelector selectedSegmentIndex] == 1; BOOL swapABXYButtons = [self.swapABXYButtonsSelector selectedSegmentIndex] == 1;
@@ -1130,7 +1087,6 @@ BOOL isCustomResolution(CGSize res) {
NSInteger touchMode = [self.touchModeSelector selectedSegmentIndex]; NSInteger touchMode = [self.touchModeSelector selectedSegmentIndex];
NSInteger statsOverlayLevel = [self.statsOverlaySelector selectedSegmentIndex]; NSInteger statsOverlayLevel = [self.statsOverlaySelector selectedSegmentIndex];
BOOL statsOverlayEnabled = statsOverlayLevel != 0; BOOL statsOverlayEnabled = statsOverlayLevel != 0;
BOOL enableGraphs = [self.enableGraphsSelector selectedSegmentIndex] == 1;
BOOL enableHdr = [self.hdrSelector selectedSegmentIndex] == 1; BOOL enableHdr = [self.hdrSelector selectedSegmentIndex] == 1;
BOOL unlockDisplayOrientation = [self.unlockDisplayOrientationSelector selectedSegmentIndex] == 1; BOOL unlockDisplayOrientation = [self.unlockDisplayOrientationSelector selectedSegmentIndex] == 1;
NSInteger resolutionSelected = [self.resolutionSelector selectedSegmentIndex]; NSInteger resolutionSelected = [self.resolutionSelector selectedSegmentIndex];
@@ -1164,19 +1120,16 @@ BOOL isCustomResolution(CGSize res) {
enableYUV444:enableYUV444 enableYUV444:enableYUV444
enablePIP:enablePIP enablePIP:enablePIP
useFramePacing:useFramePacing useFramePacing:useFramePacing
frameQueueSize:_frameQueueSize
enableHdr:enableHdr enableHdr:enableHdr
btMouseSupport:btMouseSupport btMouseSupport:btMouseSupport
// absoluteTouchMode:absoluteTouchMode // absoluteTouchMode:absoluteTouchMode
touchMode:touchMode touchMode:touchMode
statsOverlayLevel:statsOverlayLevel statsOverlayLevel:statsOverlayLevel
statsOverlayEnabled:statsOverlayEnabled
unlockDisplayOrientation:unlockDisplayOrientation unlockDisplayOrientation:unlockDisplayOrientation
resolutionSelected:resolutionSelected resolutionSelected:resolutionSelected
externalDisplayMode:externalDisplayMode externalDisplayMode:externalDisplayMode
localMousePointerMode:localMousePointerMode]; localMousePointerMode:localMousePointerMode];
enableGraphs:enableGraphs
graphOpacity:_graphOpacity
renderingBackend:renderingBackend];
} }
- (void)didReceiveMemoryWarning { - (void)didReceiveMemoryWarning {
-2
View File
@@ -48,8 +48,6 @@
</array> </array>
<key>NSLocalNetworkUsageDescription</key> <key>NSLocalNetworkUsageDescription</key>
<string>Moonlight uses the local network to connect to your gaming PC for streaming.</string> <string>Moonlight uses the local network to connect to your gaming PC for streaming.</string>
<key>UIDesignRequiresCompatibility</key>
<true/>
<key>UIMainStoryboardFile</key> <key>UIMainStoryboardFile</key>
<string>Main</string> <string>Main</string>
<key>UIRequiredDeviceCapabilities</key> <key>UIRequiredDeviceCapabilities</key>
+39 -111
View File
@@ -36,44 +36,6 @@
<string>4K</string> <string>4K</string>
</array> </array>
</dict> </dict>
<dict>
<key>Type</key>
<string>PSToggleSwitchSpecifier</string>
<key>Title</key>
<string>Request HDR Video</string>
<key>Key</key>
<string>enableHdr</string>
<key>DefaultValue</key>
<false/>
</dict>
<dict>
<key>Type</key>
<string>PSMultiValueSpecifier</string>
<key>Title</key>
<string>Audio Configuration</string>
<key>Key</key>
<string>audioConfig</string>
<key>DefaultValue</key>
<string>2</string>
<key>Titles</key>
<array>
<string>Stereo</string>
<string>LPCM 5.1</string>
<string>LPCM 7.1</string>
</array>
<key>Values</key>
<array>
<string>2</string>
<string>6</string>
<string>8</string>
</array>
</dict>
<dict>
<key>Type</key>
<string>PSGroupSpecifier</string>
<key>Title</key>
<string>Enable Match Content Rate for best results</string>
</dict>
<dict> <dict>
<key>Type</key> <key>Type</key>
<string>PSMultiValueSpecifier</string> <string>PSMultiValueSpecifier</string>
@@ -85,16 +47,12 @@
<string>60</string> <string>60</string>
<key>Values</key> <key>Values</key>
<array> <array>
<string>24</string>
<string>30</string> <string>30</string>
<string>59.94</string>
<string>60</string> <string>60</string>
</array> </array>
<key>Titles</key> <key>Titles</key>
<array> <array>
<string>24 FPS</string>
<string>30 FPS</string> <string>30 FPS</string>
<string>59.94 FPS (experimental)</string>
<string>60 FPS</string> <string>60 FPS</string>
</array> </array>
</dict> </dict>
@@ -138,6 +96,28 @@
<string>150000</string> <string>150000</string>
</array> </array>
</dict> </dict>
<dict>
<key>Type</key>
<string>PSMultiValueSpecifier</string>
<key>Title</key>
<string>Audio Configuration</string>
<key>Key</key>
<string>audioConfig</string>
<key>DefaultValue</key>
<string>2</string>
<key>Titles</key>
<array>
<string>Stereo</string>
<string>LPCM 5.1</string>
<string>LPCM 7.1</string>
</array>
<key>Values</key>
<array>
<string>2</string>
<string>6</string>
<string>8</string>
</array>
</dict>
<dict> <dict>
<key>Type</key> <key>Type</key>
<string>PSGroupSpecifier</string> <string>PSGroupSpecifier</string>
@@ -232,53 +212,37 @@
<key>Type</key> <key>Type</key>
<string>PSMultiValueSpecifier</string> <string>PSMultiValueSpecifier</string>
<key>Title</key> <key>Title</key>
<string>Frames to Buffer</string> <string>Frame Pacing Preference</string>
<key>Key</key> <key>Key</key>
<string>frameQueueSize</string> <string>useFramePacing</string>
<key>DefaultValue</key>
<integer>2</integer>
<key>Values</key>
<array>
<integer>1</integer>
<integer>2</integer>
<integer>3</integer>
<integer>4</integer>
<integer>5</integer>
</array>
<key>Titles</key>
<array>
<string>1 - Lowest Latency</string>
<string>2 - Recommended</string>
<string>3 - Smoother Video</string>
<string>4</string>
<string>5</string>
</array>
</dict>
<dict>
<key>Type</key>
<string>PSMultiValueSpecifier</string>
<key>Title</key>
<string>Rendering Backend</string>
<key>Key</key>
<string>renderingBackend</string>
<key>DefaultValue</key> <key>DefaultValue</key>
<string>0</string> <string>0</string>
<key>Values</key> <key>Values</key>
<array> <array>
<integer>0</integer> <string>0</string>
<integer>1</integer> <string>1</string>
</array> </array>
<key>Titles</key> <key>Titles</key>
<array> <array>
<string>Metal</string> <string>Lowest Latency</string>
<string>AVSampleBuffer</string> <string>Smoothest Video</string>
</array> </array>
</dict> </dict>
<dict>
<key>Type</key>
<string>PSToggleSwitchSpecifier</string>
<key>Title</key>
<string>Request HDR Video</string>
<key>Key</key>
<string>enableHdr</string>
<key>DefaultValue</key>
<false/>
</dict>
<dict> <dict>
<key>Type</key> <key>Type</key>
<string>PSGroupSpecifier</string> <string>PSGroupSpecifier</string>
<key>Title</key> <key>Title</key>
<string>Double-click select button for stats</string> <string>Troubleshooting</string>
</dict> </dict>
<dict> <dict>
<key>Type</key> <key>Type</key>
@@ -290,42 +254,6 @@
<key>DefaultValue</key> <key>DefaultValue</key>
<false/> <false/>
</dict> </dict>
<dict>
<key>Type</key>
<string>PSToggleSwitchSpecifier</string>
<key>Title</key>
<string>Performance Graphs</string>
<key>Key</key>
<string>enableGraphs</string>
<key>DefaultValue</key>
<false/>
</dict>
<dict>
<key>Type</key>
<string>PSMultiValueSpecifier</string>
<key>Title</key>
<string>Performance Graph Opacity</string>
<key>Key</key>
<string>graphOpacity</string>
<key>DefaultValue</key>
<integer>50</integer>
<key>Values</key>
<array>
<integer>0</integer>
<integer>25</integer>
<integer>50</integer>
<integer>75</integer>
<integer>100</integer>
</array>
<key>Titles</key>
<array>
<string>0% - Transparent</string>
<string>25%</string>
<string>50%</string>
<string>75%</string>
<string>100% - Opaque</string>
</array>
</dict>
</array> </array>
</dict> </dict>
</plist> </plist>
-271
View File
@@ -1,271 +0,0 @@
#import <XCTest/XCTest.h>
#import <CoreMedia/CoreMedia.h>
#define FRAME_QUEUE_VERBOSE
#import "FrameQueue.h"
#import "Logger.h"
#include "Limelight.h"
@interface FrameQueueTests : XCTestCase
@property (nonatomic, strong) FrameQueue *queue;
@end
@implementation FrameQueueTests {
int _frameNumber;
}
- (void)setUp {
[super setUp];
self.queue = [FrameQueue sharedInstance];
[self.queue setHighWaterMark:3];
_frameNumber = 0;
}
- (void)tearDown {
[self.queue clear];
self.queue = nil;
[super tearDown];
}
- (CMSampleBufferRef)makeEmptySampleBufferFor:(uint32_t)tick {
CMSampleTimingInfo sampleTiming = {
.duration = kCMTimeInvalid,
.presentationTimeStamp = CMTimeMake((int64_t)tick, 90000),
.decodeTimeStamp = kCMTimeInvalid,
};
CMSampleBufferRef sampleBuffer = NULL;
OSStatus status = CMSampleBufferCreate(kCFAllocatorDefault,
NULL, NO, NULL, NULL, NULL,
1, 1, &sampleTiming, 0, NULL,
&sampleBuffer);
XCTAssertEqual(status, noErr, @"makeEmptySampleBufferFor:%d ok", tick);
return sampleBuffer;
}
- (Frame *)makeFrameNumber:(int)num type:(int)type pts:(uint32_t)tick {
CMSampleBufferRef buf = [self makeEmptySampleBufferFor:tick];
CFRetain(buf); // emulate retain that happens in CMSampleBufferCreateReadyWithImageBuffer
Frame *frame = [[Frame alloc] initWithSampleBuffer:buf
frameNumber:num
frameType:type];
_frameNumber = num; // reset the counter
return frame;
}
- (Frame *)makeFrame {
const uint32_t ONE_FRAME = 90000 / 60;
static uint32_t tick = 0;
Frame *frame = [self makeFrameNumber:++_frameNumber
type:FRAME_TYPE_PFRAME
pts:tick];
tick += ONE_FRAME;
return frame;
}
- (void)testEnqueueThenDequeue {
Frame *f1 = [self makeFrameNumber:1 type:FRAME_TYPE_IDR pts:0];
Frame *f2 = [self makeFrame];
[self.queue enqueue:f1];
[self.queue enqueue:f2];
XCTAssertEqual([self.queue count], 2);
Frame *out1 = [self.queue dequeue];
XCTAssertEqual(out1.frameNumber, 1);
XCTAssertEqual([self.queue count], 1);
Frame *out2 = [self.queue dequeue];
XCTAssertEqual(out2.frameNumber, 2);
XCTAssertEqual([self.queue count], 0);
// empty now
XCTAssertNil([self.queue dequeue]);
}
- (void)testOverflow {
[self.queue setHighWaterMark:3];
for (int i = 0; i < self.queue.highWaterMark; i++) {
[self.queue enqueue:[self makeFrame]];
}
XCTAssertEqual([self.queue count], 3);
// queue should be full at 3
// 4: dropped, 5: queued, causes 1 to be dropped
// 6: dropped, 7: queued, causes 2 to be dropped
// 8: dropped, 9: queued, causes 3 to be dropped
for (int i = 0; i < 6; i++) {
[self.queue enqueue:[self makeFrame]];
}
XCTAssertEqual([self.queue count], 3);
XCTAssertEqual([[self.queue frameDropMetrics] total], 6);
Log(LOG_I, @"queue after overflow: %@", self.queue);
for (NSNumber *expected in @[@5, @7, @9]) {
Frame *frame = [self.queue dequeue];
XCTAssertEqual(frame.frameNumber, expected.integerValue);
}
}
- (void)testOverflowWithIDR {
[self.queue setHighWaterMark:5];
[self.queue enqueue:[self makeFrameNumber:1 type:FRAME_TYPE_IDR pts:0]];
[self.queue enqueue:[self makeFrame]];
[self.queue enqueue:[self makeFrameNumber:3 type:FRAME_TYPE_IDR pts:0]];
[self.queue enqueue:[self makeFrame]];
[self.queue enqueue:[self makeFrame]];
XCTAssertEqual([self.queue count], 5);
// queue should be full at 5
// 6: dropped, 7: queued, does NOT drop 1 (IDR)
// 8: dropped, 9: queued, does NOT drop 1 (IDR) - will be stuck here until it's dequeued
// 10: dropped, 11: queued, does NOT drop 3 (IDR)
for (int i = 0; i < 6; i++) {
[self.queue enqueue:[self makeFrame]];
}
XCTAssertEqual([self.queue count], 8);
XCTAssertEqual([[self.queue frameDropMetrics] total], 3);
Log(LOG_I, @"queue after overflow: %@", self.queue);
for (NSNumber *expected in @[@1, @2, @3, @4, @5, @7, @9, @11]) {
XCTAssertEqual([self.queue dequeue].frameNumber, expected.integerValue);
}
}
- (void)testClear {
for (int i = 0; i < self.queue.highWaterMark; i++) {
[self.queue enqueue:[self makeFrame]];
}
XCTAssertEqual([self.queue count], self.queue.highWaterMark);
[self.queue clear];
XCTAssertEqual([self.queue count], 0);
XCTAssertEqual([[self.queue frameDropMetrics] total], 0);
XCTAssertNil([self.queue dequeue]);
}
- (void)testDequeueWithTimeout {
[self.queue enqueue:[self makeFrame]];
// should return immediately with an item in the queue
CFTimeInterval t0 = CACurrentMediaTime();
Frame *out = [self.queue dequeueWithTimeout:1];
XCTAssertNotNil(out);
XCTAssertEqual(out.frameNumber, 1);
XCTAssertLessThan(CACurrentMediaTime() - t0, 0.9);
// should return nil after waiting
t0 = CACurrentMediaTime();
XCTAssertNil([self.queue dequeueWithTimeout:0.2]);
XCTAssertGreaterThan(CACurrentMediaTime() - t0, 0.2);
// no timeout
t0 = CACurrentMediaTime();
XCTAssertNil([self.queue dequeueWithTimeout:0]);
XCTAssertLessThan(CACurrentMediaTime() - t0, 0.1);
}
- (void)testDuration {
// 2nd frame will compute 1st frame's duration. makeFrame generates sequential frames at 60fps
[self.queue clear];
[self.queue enqueue:[self makeFrame]];
[self.queue enqueue:[self makeFrame]];
XCTAssertEqual([self.queue count], 2);
Frame *first = [self.queue dequeue];
XCTAssertEqualWithAccuracy(first.duration, (float)1.0f / 60, 0.0001f);
}
- (void)testEstimatedFramerate {
// prime the start point
[self.queue estimatedFramerate];
for (int i = 0; i < 24 * 5; i++) {
// these will all get dropped but they are still counted for fps
[self.queue enqueue:[self makeFrame]];
[NSThread sleepForTimeInterval:1.0f / 24];
}
XCTAssertEqualWithAccuracy([self.queue estimatedFramerate], 24.0f, 4.0f);
}
- (void)testProducerConsumer {
static const int TotalFrames = 500;
// We expect exactly kTotalFrames dequeues.
XCTestExpectation *dequeueAll = [self expectationWithDescription:@"consumer dequeued all frames"];
dequeueAll.expectedFulfillmentCount = TotalFrames;
// Producer
dispatch_queue_t producerQ = dispatch_queue_create("moonlight.test.producer", DISPATCH_QUEUE_CONCURRENT);
dispatch_async(producerQ, ^{
for (int i = 0; i < TotalFrames; i++) {
[self.queue enqueue:[self makeFrame]];
[NSThread sleepForTimeInterval:0.002];
}
});
// Consumer
dispatch_queue_t consumerQ = dispatch_queue_create("moonlight.test.consumer", DISPATCH_QUEUE_CONCURRENT);
dispatch_async(consumerQ, ^{
for (int expected = 0; expected < TotalFrames; expected++) {
Frame *frame = [self.queue dequeueWithTimeout:1.0];
XCTAssertNotNil(frame, @"Expected frame %d, but got nil", expected);
XCTAssertEqual(frame.frameNumber, expected + 1, @"Expected frameNumber %d but got %d", expected + 1, frame.frameNumber);
[dequeueAll fulfill];
}
});
// Wait up to 10s for all frames to be consumed
[self waitForExpectationsWithTimeout:10.0 handler:^(NSError * _Nullable error) {
if (error) {
XCTFail(@"Timed out waiting for consumer: %@", error);
}
XCTAssertEqual([self.queue count], 0, @"Queue should be empty at end");
}];
}
- (void)testPerformance {
XCTMeasureOptions *options = [[XCTMeasureOptions alloc] init];
options.iterationCount = 20;
if (@available(iOS 13.0, *)) {
static const int TotalFrames = 1000;
dispatch_queue_t producerQ = dispatch_queue_create("moonlight.test.producer", DISPATCH_QUEUE_CONCURRENT);
dispatch_queue_t consumerQ = dispatch_queue_create("moonlight.test.consumer", DISPATCH_QUEUE_CONCURRENT);
[self measureWithOptions:options block:^{
XCTestExpectation *dequeueAll = [self expectationWithDescription:@"consumer dequeued all frames"];
dequeueAll.expectedFulfillmentCount = TotalFrames;
[self.queue setHighWaterMark:5];
// Producer
dispatch_async(producerQ, ^{
for (int i = 0; i < TotalFrames; i++) {
//[self.queue enqueue:[self makeFrame]];
[self.queue enqueue:[self makeFrame] withSlackSize:3];
[NSThread sleepForTimeInterval:0.001];
}
});
// Consumer
dispatch_async(consumerQ, ^{
for (int expected = 0; expected < TotalFrames; expected++) {
[self.queue dequeueWithTimeout:1.0f];
[dequeueAll fulfill];
}
});
// Wait up to 10s for all frames to be consumed
[self waitForExpectationsWithTimeout:100.0 handler:^(NSError * _Nullable error) {
if (error) {
XCTFail(@"Timed out waiting for consumer: %@", error);
[self.queue clear];
}
}];
}];
}
}
@end
-5
View File
@@ -95,32 +95,27 @@ static NSString* DB_NAME = @"Limelight_iOS.sqlite";
{ {
// Sent when the application is about to move from active to inactive state. This can occur for certain types of temporary interruptions (such as an incoming phone call or SMS message) or when the user quits the application and it begins the transition to the background state. // Sent when the application is about to move from active to inactive state. This can occur for certain types of temporary interruptions (such as an incoming phone call or SMS message) or when the user quits the application and it begins the transition to the background state.
// Use this method to pause ongoing tasks, disable timers, and throttle down OpenGL ES frame rates. Games should use this method to pause the game. // Use this method to pause ongoing tasks, disable timers, and throttle down OpenGL ES frame rates. Games should use this method to pause the game.
Log(LOG_I, @"TODO: applicationWillResignActive");
} }
- (void)applicationDidEnterBackground:(UIApplication *)application - (void)applicationDidEnterBackground:(UIApplication *)application
{ {
// Use this method to release shared resources, save user data, invalidate timers, and store enough application state information to restore your application to its current state in case it is terminated later. // Use this method to release shared resources, save user data, invalidate timers, and store enough application state information to restore your application to its current state in case it is terminated later.
// If your application supports background execution, this method is called instead of applicationWillTerminate: when the user quits. // If your application supports background execution, this method is called instead of applicationWillTerminate: when the user quits.
Log(LOG_I, @"TODO: applicationDidEnterBackground");
} }
- (void)applicationWillEnterForeground:(UIApplication *)application - (void)applicationWillEnterForeground:(UIApplication *)application
{ {
// Called as part of the transition from the background to the inactive state; here you can undo many of the changes made on entering the background. // Called as part of the transition from the background to the inactive state; here you can undo many of the changes made on entering the background.
Log(LOG_I, @"TODO: applicationWillEnterForeground");
} }
- (void)applicationDidBecomeActive:(UIApplication *)application - (void)applicationDidBecomeActive:(UIApplication *)application
{ {
// Restart any tasks that were paused (or not yet started) while the application was inactive. If the application was previously in the background, optionally refresh the user interface. // Restart any tasks that were paused (or not yet started) while the application was inactive. If the application was previously in the background, optionally refresh the user interface.
Log(LOG_I, @"TODO: applicationDidBecomeActive");
} }
- (void)applicationWillTerminate:(UIApplication *)application - (void)applicationWillTerminate:(UIApplication *)application
{ {
// Saves changes in the application's managed object context before the application terminates. // Saves changes in the application's managed object context before the application terminates.
Log(LOG_I, @"applicationWillTerminate, calling saveContext");
[self saveContext]; [self saveContext];
} }
+1 -1
View File
@@ -75,7 +75,7 @@
if ([[event allTouches] count] > 1) { if ([[event allTouches] count] > 1) {
return; return;
} }
UITouch* touch = [touches anyObject]; UITouch* touch = [touches anyObject];
CGPoint touchLocation = [touch locationInView:streamView]; CGPoint touchLocation = [touch locationInView:streamView];
+2 -1
View File
@@ -59,7 +59,7 @@
} }
self->activePointerIds = [NSMutableSet set]; self->activePointerIds = [NSMutableSet set];
self->blacklistedTouches = [NSMutableSet set]; self->blacklistedTouches = [NSMutableSet set];
self->asyncNativeTouch = settings.asyncNativeTouchPriority.intValue != AsyncNativeTouchOff; self->asyncNativeTouch = settings.asyncNativeTouchPriority.intValue != AsyncNativeTouchOff;
switch(settings.asyncNativeTouchPriority.intValue){ switch(settings.asyncNativeTouchPriority.intValue){
@@ -239,6 +239,7 @@
}); });
else { else {
for (UITouch* touch in touches){ 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(self->activateCoordSelector) [self updatePointerObjInDict:touch]; if(self->activateCoordSelector) [self updatePointerObjInDict:touch];
[self sendTouchEvent:touch withTouchtype:LI_TOUCH_EVENT_MOVE]; [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) [[self getPointerObjFromDict:touch] doesNeedResetCoords]; // execute the judging of doesReachBoundary for current pointer instance. (happens after the event is sent to Sunshine service)
-11
View File
@@ -580,9 +580,6 @@ CFStringRef __currentColorSpace;
fragmentShaderName = is10BitInput ? @"ps_draw_triplanar_10bit" : @"ps_draw_triplanar_8bit"; fragmentShaderName = is10BitInput ? @"ps_draw_triplanar_10bit" : @"ps_draw_triplanar_8bit";
} }
Log(LOG_I, @"DEBUG: CVPixelBuffer format: 0x%X, planes: %zu, is10BitInput: %d, shader: %@, framebuffer format: %lu",
pixelFormatType, planes, is10BitInput, fragmentShaderName, (unsigned long)framebufferPixelFormat);
pipelineDesc.colorAttachments[0].pixelFormat = framebufferPixelFormat; pipelineDesc.colorAttachments[0].pixelFormat = framebufferPixelFormat;
pipelineDesc.vertexBuffers[0].mutability = MTLMutabilityImmutable; pipelineDesc.vertexBuffers[0].mutability = MTLMutabilityImmutable;
@@ -638,10 +635,6 @@ CFStringRef __currentColorSpace;
return; return;
} else { } else {
id<MTLTexture> texture = CVMetalTextureGetTexture(_cvMetalTextures[0]); id<MTLTexture> texture = CVMetalTextureGetTexture(_cvMetalTextures[0]);
Log(LOG_I, @"DEBUG: Created BGRA texture: format=%lu, width=%zu, height=%zu",
(unsigned long)MTLPixelFormatBGRA8Unorm,
CVPixelBufferGetWidth(frame.pixelBuffer),
CVPixelBufferGetHeight(frame.pixelBuffer));
} }
} else { } else {
// Handle planar YUV formats // Handle planar YUV formats
@@ -683,10 +676,6 @@ CFStringRef __currentColorSpace;
return; return;
} else { } else {
id<MTLTexture> texture = CVMetalTextureGetTexture(_cvMetalTextures[i]); id<MTLTexture> texture = CVMetalTextureGetTexture(_cvMetalTextures[i]);
Log(LOG_I, @"DEBUG: Created texture for plane %zu: format=%lu, width=%zu, height=%zu",
i, (unsigned long)fmt,
CVPixelBufferGetWidthOfPlane(frame.pixelBuffer, i),
CVPixelBufferGetHeightOfPlane(frame.pixelBuffer, i));
} }
} }
} }
-1
View File
@@ -7,7 +7,6 @@
// //
#import <Metal/Metal.h> #import <Metal/Metal.h>
#import <QuartzCore/CAMetalDisplayLink.h>
#import <QuartzCore/CAMetalLayer.h> #import <QuartzCore/CAMetalLayer.h>
#import <UIKit/UIKit.h> #import <UIKit/UIKit.h>
#import "MetalConfig.h" #import "MetalConfig.h"
View File
-88
View File
@@ -1,88 +0,0 @@
#pragma mark DisplayLink - RTP PTS timestamp-based frame pacing (experimental)
// TODO: handle presentationTimeUs rollover every 13 hours
// This frame pacing method attempts to use timestamps from Sunshine to determine when
// frames should be displayed. Sunshine v2025.600+ required.
- (void)framePacingUsingTimestamps:(CADisplayLink *)link {
// All times are in seconds
static CFTimeInterval anchorLocal = 0.0f;
static CMTime anchorHost;
static CFTimeInterval lastTargetLocal = 0.0f;
CFTimeInterval start = link.timestamp;
CFTimeInterval deadline = link.targetTimestamp;
_displayRefreshRate = 1.0f / (deadline - start);
[self checkDisplayLayer];
Frame *frame = nil;
CFTimeInterval dl0 = CACurrentMediaTime();
static CFTimeInterval avgOverhead = 0.004f; // averaged each callback
CFTimeInterval waitFor = deadline - dl0 - avgOverhead;
if (waitFor < 0.001f) {
waitFor = 0.001f;
}
// Process the next frame for display
frame = [_frameQueue dequeueWithTimeout:waitFor];
if (frame) {
CFTimeInterval now = CACurrentMediaTime();
// during stream startup, wait and let the first frames through untouched
if (!_seenFrameOne) {
Log(LOG_I, @"Frame pacing (pts mode): target %f Hz with %d FPS stream", _displayRefreshRate, self->frameRate);
_seenFrameOne = YES;
// Just let frame 1 through, we don't want to anchor to it
Log(LOG_I, @"[%.3f] rendering frame %d / %.3f, queue size %d",
deadline, frame.frameNumber, frame.pts, [_frameQueue count]);
[self renderFrame:frame atTime:frame.pts90];
return;
}
if (!CMTIME_IS_VALID(anchorHost)) {
// Use this frame as the anchor point. This is normally frame 2.
anchorHost = frame.pts90;
anchorLocal = now;
Log(LOG_I, @"Setting anchor point: anchorHost=%.3f == anchorLocal=%.3f",
CMTimeGetSeconds(anchorHost), anchorLocal);
}
//CMTime targetTime = CMTimeSubtract(frame.pts90, frameQueue.ptsCorrection); // total of all dropped frames
CMTime targetTime = frame.pts90;
CFTimeInterval targetLocal = CMTimeGetSeconds(targetTime);
#ifdef DISPLAYLINK_VERBOSE
// iOS double-buffering renders the frame we queue here during the interval +2 from now
CMTime hostDelta = CMTimeSubtract(frame.pts90, anchorHost);
CFTimeInterval hostDeltaF = CMTimeGetSeconds(hostDelta);
CMTime drift = CMTimeSubtract(hostDelta, CMTimeMakeWithSeconds(now - anchorLocal, NSEC_PER_SEC));
Log(LOG_I, @"[%.3f] rendering frame %d / %.3f @ %.3f, drift %.3f, queue size %d",
deadline - anchorLocal, frame.frameNumber, hostDeltaF, targetLocal,
CMTimeGetSeconds(drift), [_frameQueue count]);
#endif
// Schedule the frame at its original timestamp, adjusted by a correction factor (total duration of all dropped frames)
[self renderFrame:frame atTime:targetTime];
// Update metrics
if (lastTargetLocal != 0) {
[self->_callbacks observeFloat:PLOT_FRAMETIME value:(targetLocal - lastTargetLocal) * 1000.0];
}
lastTargetLocal = targetLocal;
// weighted moving average of how much time displayLink needs after dequeuing a frame.
// This is used to avoid overshooting a vsync by waiting too long.
const double alpha = 0.1f;
avgOverhead = ((CACurrentMediaTime() - dl0) * alpha) + (avgOverhead * (1.0 - alpha));
}
// are we returning too late?
CFTimeInterval dl2 = CACurrentMediaTime();
if (dl2 > deadline) {
Log(LOG_W, @"*** returned past deadline by %.3f ms (%f > %f)", (dl2 - deadline) * 1000.0, dl2, deadline);
}
}
@@ -374,7 +374,7 @@
FB290E2619B37A4E004C83CF /* Project object */ = { FB290E2619B37A4E004C83CF /* Project object */ = {
isa = PBXProject; isa = PBXProject;
attributes = { attributes = {
BuildIndependentTargetsInParallel = NO; BuildIndependentTargetsInParallel = YES;
LastUpgradeCheck = 1510; LastUpgradeCheck = 1510;
ORGANIZATIONNAME = "Moonlight Stream"; ORGANIZATIONNAME = "Moonlight Stream";
TargetAttributes = { TargetAttributes = {
-268
View File
@@ -1,268 +0,0 @@
#!/usr/bin/env perl
#
# Don't try to follow the logic in here, XML::Twig allows for some real nonsense
use strict;
use warnings;
use Getopt::Long qw(GetOptions);
use Pod::Usage qw(pod2usage);
use XML::Twig;
my $inSettings = 0;
my @list = ();
my @clipboard = ();
my $help = 0;
my $after;
my $move;
my $to;
GetOptions(
'help|?' => \$help,
'add-after=s' => \$after,
'move=s' => \$move,
'to=s' => \$to,
) or pod2usage(2);
pod2usage(1) if $help || ($move && !$to) || (!$move && $to);
my $xml = shift || pod2usage(2);
my $twig = XML::Twig->new(
twig_handlers => {
'#COMMENT' => sub {
my ($t, $comment) = @_;
if ($comment->{comment} eq 'Settings View Controller') {
$inSettings = 1;
}
else {
$inSettings = 0;
}
},
label => sub {
my ($t, $label) = @_;
if ($inSettings) {
push @list, [ label => $label ];
}
},
segmentedControl => sub {
my ($t, $control) = @_;
if ($inSettings) {
push @list, [ control => $control ];
}
},
slider => sub {
my ($t, $slider) = @_;
if ($inSettings) {
push @list, [ slider => $slider ];
}
},
view => sub {
my ($t, $view) = @_;
if ($inSettings) {
push @list, [ view => $view ];
}
},
},
comments => 'process',
keep_atts_order => 1,
pretty_print => 'indented',
empty_tags => 'normal',
);
$twig->parsefile($xml);
if ($after) {
do_after($twig, $after);
}
elsif ($move && $to) {
do_move($twig, $move, $to);
}
do_output($twig);
sub do_after {
my ($twig, $after) = @_;
my $LABEL_TEMPLATE = qq{
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" fixedFrame="YES" text="New Label Template" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="%s">
<rect key="frame" x="16" y="999" width="35" height="21"></rect>
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"></autoresizingMask>
<fontDescription key="fontDescription" type="system" pointSize="17"></fontDescription>
<color key="textColor" red="0.93902439019999995" green="0.9625305918" blue="1" alpha="1" colorSpace="custom" customColorSpace="sRGB"></color>
<nil key="highlightedColor"></nil>
</label>};
my $CONTROL_TEMPLATE = qq{
<segmentedControl opaque="NO" contentMode="scaleToFill" fixedFrame="YES" contentHorizontalAlignment="left" contentVerticalAlignment="top" segmentControlStyle="plain" selectedSegmentIndex="0" translatesAutoresizingMaskIntoConstraints="NO" id="%s" userLabel="New Control Template">
<rect key="frame" x="16" y="999" width="459" height="28"></rect>
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"></autoresizingMask>
<segments>
<segment title="No"></segment>
<segment title="Yes"></segment>
</segments>
<color key="tintColor" red="0.6716768742" green="0.61711704730000005" blue="0.99902987480000005" alpha="1" colorSpace="custom" customColorSpace="sRGB"></color>
<color key="selectedSegmentTintColor" red="0.6716768742" green="0.61711704730000005" blue="0.99902987480000005" alpha="1" colorSpace="custom" customColorSpace="sRGB"></color>
</segmentedControl>};
my @to_add = (
[ label => XML::Twig::Elt->parse( sprintf($LABEL_TEMPLATE, generate_id()) ) ],
[ control => XML::Twig::Elt->parse( sprintf($CONTROL_TEMPLATE, generate_id()) ) ],
);
# Find where to insert them
for (my $i = 0; $i < scalar @list; $i++) {
my ($type, $e) = @{$list[$i]};
if ($type eq 'label' && ($e->att('text') // q{}) eq $after) {
($type, $e) = @{$list[$i + 1]};
# paste the block to a new location
for my $tm (@to_add) {
$tm->[1]->paste(after => $e);
$e = $tm->[1];
}
# also move the block in the ordering list, which is where the y values are adjusted
$i += 2;
splice(@list, $i, 0, @to_add);
}
}
}
sub do_move {
my ($twig, $move, $to) = @_;
my @to_move = ();
# Pull out the bits to move
for (my $i = 0; $i < scalar @list; $i++) {
my ($type, $e) = @{$list[$i]};
if ($type eq 'label' && ($e->att('text') // q{}) eq $move) {
# move this item and the next one (the control)
push @to_move, [ $type => $e->cut ];
($type, $e) = @{$list[$i + 1]};
push @to_move, [ $type => $e->cut ];
splice(@list, $i, 2);
last;
}
}
# Find where to insert them
for (my $i = 0; $i < scalar @list; $i++) {
my ($type, $e) = @{$list[$i]};
if ($type eq 'label' && ($e->att('text') // q{}) eq $to) {
($type, $e) = @{$list[$i + 1]};
# paste the block to a new location
for my $tm (@to_move) {
$tm->[1]->paste(after => $e);
$e = $tm->[1];
}
# also move the block in the ordering list, which is where the y values are adjusted
$i += 2;
splice(@list, $i, 0, @to_move);
}
}
}
sub do_output {
my $twig = shift;
# first label
my $x = 16;
my $y = 20;
# spacing
my $label_to_control = 29;
my $control_to_label = 35;
my $label_to_label = 43;
my $slider_to_next = 29;
my $view_to_next = 43;
my $seen_rdv = 0;
my $last = q{};
printf STDERR "%30s %s %s\n\n", q{}, "auto", "current";
for my $row (@list) {
my ($type, $e) = @{$row};
my $rect = $e->has_child('rect');
my $orig_y = $rect->att('y');
$rect->set_att(x => $x);
$rect->set_att(y => $y);
if ($type eq "label") {
my $text = $e->att('userLabel') // $e->att('text') // "(unknown label)";
printf STDERR "%30s %d %4d\n", $text, $y, $orig_y;
if ($text =~ /Caption/) { # space after captions is a bit larger
$y += $label_to_label;
}
else {
$y += $label_to_control;
}
}
elsif ($type eq "control") {
my ($userLabel) = $e->att('userLabel') // "(unknown control)";
printf STDERR "%30s %d %4d\n", $userLabel, $y, $orig_y;
$y += $control_to_label;
}
elsif ($type eq "slider") {
printf STDERR "%30s %d %4d\n", "slider", $y, $orig_y;
$y += $slider_to_next;
}
elsif ($type eq "view") {
my $customClass = $e->att('customClass') // q{};
if ($customClass && $customClass eq "UIScrollView") {
# This is the entire view, so we're done
last;
}
printf STDERR "%30s %d %4d\n", "view", $y, $orig_y;
$y += $view_to_next;
}
$last = $type;
}
# Output the entire XML with our changes
$twig->print;
}
sub generate_id {
# generate an id of the form K4A-oH-Bs0
my $chars = [ split //, 'abcdefghiklmnopqrstuvwxyzABCDEFGHIKLMNOPQRSTUVWXYZ0123456789' ];
my $random_fragment = sub {
my $length = shift;
return '' if $length < 1;
my $fragment = '';
$fragment .= $chars->[int(rand(scalar @{$chars}))] for (1..$length);
return $fragment;
};
return join '-', (
$random_fragment->(3),
$random_fragment->(2),
$random_fragment->(3),
);
}
=pod
=head1 NAME
SettingsHelper.pl - Less painful storyboard settings elements
=head1 SYNOPSIS
SettingsHelper.pl [options] [iPad.storyboard] > New.storyboard
Run with no options to display the list of items in a storyboard.
Options:
--add-after LABEL Insert a new empty label + control pair after the given label (and its control)
--move FROM --to TO Move the first label and control after the "to" label
--help This help message.
=cut