refactor: remove unnecessary changes
This commit is contained in:
@@ -32,8 +32,7 @@
|
||||
@property (strong, nonatomic) IBOutlet UISegmentedControl *yuv444Selector;
|
||||
@property (strong, nonatomic) IBOutlet UISegmentedControl *pipSelector;
|
||||
@property (strong, nonatomic) IBOutlet UISegmentedControl *hdrSelector;
|
||||
@property (strong, nonatomic) IBOutlet UILabel *frameQueueSizeLabel;
|
||||
@property (strong, nonatomic) IBOutlet UISlider *frameQueueSizeSlider;
|
||||
@property (strong, nonatomic) IBOutlet UISegmentedControl *framePacingSelector;
|
||||
@property (strong, nonatomic) IBOutlet UISegmentedControl *btMouseSelector;
|
||||
@property (strong, nonatomic) IBOutlet UISegmentedControl *reverseMouseWheelDirectionSelector;
|
||||
@property (strong, nonatomic) IBOutlet UISegmentedControl *statsOverlaySelector;
|
||||
@@ -63,10 +62,6 @@
|
||||
|
||||
@property (strong, nonatomic) IBOutlet UISegmentedControl *externalDisplayModeSelector;
|
||||
@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 ignored "-Wunguarded-availability"
|
||||
|
||||
@@ -22,8 +22,6 @@
|
||||
bool justEnteredSettingsViewDoNotOpenOscLayoutTool;
|
||||
uint16_t oscLayoutFingers;
|
||||
CustomEdgeSlideGestureRecognizer *slideToCloseSettingsViewRecognizer;
|
||||
NSInteger _frameQueueSize;
|
||||
NSInteger _graphOpacity;
|
||||
}
|
||||
|
||||
@dynamic overrideUserInterfaceStyle;
|
||||
@@ -368,8 +366,6 @@ BOOL isCustomResolution(CGSize res) {
|
||||
|
||||
// Ensure we pick a bitrate that falls exactly onto a slider notch
|
||||
_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
|
||||
UIWindow *window = UIApplication.sharedApplication.windows.firstObject;
|
||||
@@ -383,9 +379,9 @@ BOOL isCustomResolution(CGSize res) {
|
||||
UITapGestureRecognizer *resolutionDisplayViewTap = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(resolutionDisplayViewTapped:)];
|
||||
[self.resolutionDisplayView addGestureRecognizer:resolutionDisplayViewTap];
|
||||
|
||||
resolutionTable[0] = CGSizeMake(1280, 720);
|
||||
resolutionTable[1] = CGSizeMake(1920, 1080);
|
||||
resolutionTable[2] = CGSizeMake(2560, 1440);
|
||||
resolutionTable[0] = CGSizeMake(640, 360);
|
||||
resolutionTable[1] = CGSizeMake(1280, 720);
|
||||
resolutionTable[2] = CGSizeMake(1920, 1080);
|
||||
resolutionTable[3] = CGSizeMake(3840, 2160);
|
||||
resolutionTable[4] = CGSizeMake(safeAreaWidth, fullScreenHeight);
|
||||
resolutionTable[5] = CGSizeMake(fullScreenWidth, fullScreenHeight);
|
||||
@@ -397,12 +393,6 @@ BOOL isCustomResolution(CGSize res) {
|
||||
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;
|
||||
switch ([currentSettings.framerate integerValue]) {
|
||||
case 30:
|
||||
@@ -412,11 +402,8 @@ BOOL isCustomResolution(CGSize res) {
|
||||
case 60:
|
||||
framerate = 1;
|
||||
break;
|
||||
case 90:
|
||||
framerate = 2;
|
||||
break;
|
||||
case 120:
|
||||
framerate = 3;
|
||||
framerate = 2;
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -425,6 +412,17 @@ BOOL isCustomResolution(CGSize res) {
|
||||
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))
|
||||
@@ -469,23 +467,9 @@ BOOL isCustomResolution(CGSize res) {
|
||||
[self.yuv444Selector setSelectedSegmentIndex:currentSettings.enableYUV444 ? 1 : 0];
|
||||
[self.pipSelector setSelectedSegmentIndex:currentSettings.enablePIP ? 1 : 0];
|
||||
[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.optimizeSettingsSelector setSelectedSegmentIndex:currentSettings.optimizeGames ? 1 : 0];
|
||||
[self.frameQueueSizeSlider setMinimumValue:1];
|
||||
[self.frameQueueSizeSlider setMaximumValue:5];
|
||||
[self.frameQueueSizeSlider setValue:_frameQueueSize];
|
||||
[self.frameQueueSizeSlider addTarget:self action:@selector(frameQueueSizeSliderMoved) forControlEvents:UIControlEventValueChanged];
|
||||
[self updateFrameQueueSizeText];
|
||||
[self.framePacingSelector setSelectedSegmentIndex:currentSettings.useFramePacing ? 1 : 0];
|
||||
[self.multiControllerSelector setSelectedSegmentIndex:currentSettings.multiController ? 1 : 0];
|
||||
[self.swapABXYButtonsSelector setSelectedSegmentIndex:currentSettings.swapABXYButtons ? 1 : 0];
|
||||
[self.audioOnPCSelector setSelectedSegmentIndex:currentSettings.playAudioOnPC ? 1 : 0];
|
||||
@@ -845,7 +829,7 @@ BOOL isCustomResolution(CGSize res) {
|
||||
}
|
||||
|
||||
defaultBitrate = round(resolutionFactor * frameRateFactor) * 1000;
|
||||
_bitrate = MIN(defaultBitrate, 150000);
|
||||
_bitrate = MIN(defaultBitrate, 100000);
|
||||
[self.bitrateSlider setValue:[self getSliderValueForBitrate:_bitrate] animated:YES];
|
||||
|
||||
[self updateBitrateText];
|
||||
@@ -1019,8 +1003,6 @@ BOOL isCustomResolution(CGSize res) {
|
||||
case 1:
|
||||
return 60;
|
||||
case 2:
|
||||
return 90;
|
||||
case 3:
|
||||
return 120;
|
||||
default:
|
||||
abort();
|
||||
@@ -1069,30 +1051,6 @@ BOOL isCustomResolution(CGSize res) {
|
||||
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 {
|
||||
DataManager* dataMan = [[DataManager alloc] init];
|
||||
NSInteger framerate = [self getChosenFrameRate];
|
||||
@@ -1116,7 +1074,6 @@ BOOL isCustomResolution(CGSize res) {
|
||||
BOOL largerStickLR1 = [self.largerStickLR1Selector selectedSegmentIndex] == 1;
|
||||
BOOL liftStreamViewForKeyboard = [self.liftStreamViewForKeyboardSelector selectedSegmentIndex] == 1;
|
||||
BOOL showKeyboardToolbar = [self.showKeyboardToolbarSelector selectedSegmentIndex] == 1;
|
||||
NSInteger renderingBackend = [self.renderingBackendSelector selectedSegmentIndex];
|
||||
BOOL optimizeGames = [self.optimizeSettingsSelector selectedSegmentIndex] == 1;
|
||||
BOOL multiController = [self.multiControllerSelector selectedSegmentIndex] == 1;
|
||||
BOOL swapABXYButtons = [self.swapABXYButtonsSelector selectedSegmentIndex] == 1;
|
||||
@@ -1130,7 +1087,6 @@ BOOL isCustomResolution(CGSize res) {
|
||||
NSInteger touchMode = [self.touchModeSelector selectedSegmentIndex];
|
||||
NSInteger statsOverlayLevel = [self.statsOverlaySelector selectedSegmentIndex];
|
||||
BOOL statsOverlayEnabled = statsOverlayLevel != 0;
|
||||
BOOL enableGraphs = [self.enableGraphsSelector selectedSegmentIndex] == 1;
|
||||
BOOL enableHdr = [self.hdrSelector selectedSegmentIndex] == 1;
|
||||
BOOL unlockDisplayOrientation = [self.unlockDisplayOrientationSelector selectedSegmentIndex] == 1;
|
||||
NSInteger resolutionSelected = [self.resolutionSelector selectedSegmentIndex];
|
||||
@@ -1164,19 +1120,16 @@ BOOL isCustomResolution(CGSize res) {
|
||||
enableYUV444:enableYUV444
|
||||
enablePIP:enablePIP
|
||||
useFramePacing:useFramePacing
|
||||
frameQueueSize:_frameQueueSize
|
||||
enableHdr:enableHdr
|
||||
btMouseSupport:btMouseSupport
|
||||
// absoluteTouchMode:absoluteTouchMode
|
||||
touchMode:touchMode
|
||||
statsOverlayLevel:statsOverlayLevel
|
||||
statsOverlayEnabled:statsOverlayEnabled
|
||||
unlockDisplayOrientation:unlockDisplayOrientation
|
||||
resolutionSelected:resolutionSelected
|
||||
externalDisplayMode:externalDisplayMode
|
||||
localMousePointerMode:localMousePointerMode];
|
||||
enableGraphs:enableGraphs
|
||||
graphOpacity:_graphOpacity
|
||||
renderingBackend:renderingBackend];
|
||||
}
|
||||
|
||||
- (void)didReceiveMemoryWarning {
|
||||
|
||||
@@ -48,8 +48,6 @@
|
||||
</array>
|
||||
<key>NSLocalNetworkUsageDescription</key>
|
||||
<string>Moonlight uses the local network to connect to your gaming PC for streaming.</string>
|
||||
<key>UIDesignRequiresCompatibility</key>
|
||||
<true/>
|
||||
<key>UIMainStoryboardFile</key>
|
||||
<string>Main</string>
|
||||
<key>UIRequiredDeviceCapabilities</key>
|
||||
|
||||
@@ -36,44 +36,6 @@
|
||||
<string>4K</string>
|
||||
</array>
|
||||
</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>
|
||||
<key>Type</key>
|
||||
<string>PSMultiValueSpecifier</string>
|
||||
@@ -85,16 +47,12 @@
|
||||
<string>60</string>
|
||||
<key>Values</key>
|
||||
<array>
|
||||
<string>24</string>
|
||||
<string>30</string>
|
||||
<string>59.94</string>
|
||||
<string>60</string>
|
||||
</array>
|
||||
<key>Titles</key>
|
||||
<array>
|
||||
<string>24 FPS</string>
|
||||
<string>30 FPS</string>
|
||||
<string>59.94 FPS (experimental)</string>
|
||||
<string>60 FPS</string>
|
||||
</array>
|
||||
</dict>
|
||||
@@ -138,6 +96,28 @@
|
||||
<string>150000</string>
|
||||
</array>
|
||||
</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>
|
||||
@@ -232,53 +212,37 @@
|
||||
<key>Type</key>
|
||||
<string>PSMultiValueSpecifier</string>
|
||||
<key>Title</key>
|
||||
<string>Frames to Buffer</string>
|
||||
<string>Frame Pacing Preference</string>
|
||||
<key>Key</key>
|
||||
<string>frameQueueSize</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>
|
||||
<string>useFramePacing</string>
|
||||
<key>DefaultValue</key>
|
||||
<string>0</string>
|
||||
<key>Values</key>
|
||||
<array>
|
||||
<integer>0</integer>
|
||||
<integer>1</integer>
|
||||
<string>0</string>
|
||||
<string>1</string>
|
||||
</array>
|
||||
<key>Titles</key>
|
||||
<array>
|
||||
<string>Metal</string>
|
||||
<string>AVSampleBuffer</string>
|
||||
<string>Lowest Latency</string>
|
||||
<string>Smoothest Video</string>
|
||||
</array>
|
||||
</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>PSGroupSpecifier</string>
|
||||
<key>Title</key>
|
||||
<string>Double-click select button for stats</string>
|
||||
<string>Troubleshooting</string>
|
||||
</dict>
|
||||
<dict>
|
||||
<key>Type</key>
|
||||
@@ -290,42 +254,6 @@
|
||||
<key>DefaultValue</key>
|
||||
<false/>
|
||||
</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>
|
||||
</dict>
|
||||
</plist>
|
||||
|
||||
@@ -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
|
||||
@@ -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.
|
||||
// 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
|
||||
{
|
||||
// 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.
|
||||
Log(LOG_I, @"TODO: applicationDidEnterBackground");
|
||||
}
|
||||
|
||||
- (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.
|
||||
Log(LOG_I, @"TODO: applicationWillEnterForeground");
|
||||
}
|
||||
|
||||
- (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.
|
||||
Log(LOG_I, @"TODO: applicationDidBecomeActive");
|
||||
}
|
||||
|
||||
- (void)applicationWillTerminate:(UIApplication *)application
|
||||
{
|
||||
// Saves changes in the application's managed object context before the application terminates.
|
||||
Log(LOG_I, @"applicationWillTerminate, calling saveContext");
|
||||
[self saveContext];
|
||||
}
|
||||
|
||||
|
||||
@@ -239,6 +239,7 @@
|
||||
});
|
||||
else {
|
||||
for (UITouch* touch in touches){
|
||||
// continue to the next loop if current touch is already captured by OSC. works only in regular native touch
|
||||
if(self->activateCoordSelector) [self updatePointerObjInDict:touch];
|
||||
[self sendTouchEvent:touch withTouchtype:LI_TOUCH_EVENT_MOVE];
|
||||
[[self getPointerObjFromDict:touch] doesNeedResetCoords]; // execute the judging of doesReachBoundary for current pointer instance. (happens after the event is sent to Sunshine service)
|
||||
|
||||
@@ -580,9 +580,6 @@ CFStringRef __currentColorSpace;
|
||||
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.vertexBuffers[0].mutability = MTLMutabilityImmutable;
|
||||
|
||||
@@ -638,10 +635,6 @@ CFStringRef __currentColorSpace;
|
||||
return;
|
||||
} else {
|
||||
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 {
|
||||
// Handle planar YUV formats
|
||||
@@ -683,10 +676,6 @@ CFStringRef __currentColorSpace;
|
||||
return;
|
||||
} else {
|
||||
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));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,7 +7,6 @@
|
||||
//
|
||||
|
||||
#import <Metal/Metal.h>
|
||||
#import <QuartzCore/CAMetalDisplayLink.h>
|
||||
#import <QuartzCore/CAMetalLayer.h>
|
||||
#import <UIKit/UIKit.h>
|
||||
#import "MetalConfig.h"
|
||||
|
||||
@@ -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 */ = {
|
||||
isa = PBXProject;
|
||||
attributes = {
|
||||
BuildIndependentTargetsInParallel = NO;
|
||||
BuildIndependentTargetsInParallel = YES;
|
||||
LastUpgradeCheck = 1510;
|
||||
ORGANIZATIONNAME = "Moonlight Stream";
|
||||
TargetAttributes = {
|
||||
|
||||
@@ -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
|
||||
Reference in New Issue
Block a user