CustomOSC first integration
This commit is contained in:
@@ -0,0 +1,19 @@
|
||||
//
|
||||
// ProfileTableViewCell.h
|
||||
// Moonlight
|
||||
//
|
||||
// Created by Long Le on 12/11/22.
|
||||
// Copyright © 2022 Moonlight Game Streaming Project. All rights reserved.
|
||||
//
|
||||
|
||||
#import <UIKit/UIKit.h>
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
@interface ProfileTableViewCell : UITableViewCell
|
||||
|
||||
@property (weak, nonatomic) IBOutlet UILabel *name;
|
||||
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
||||
@@ -0,0 +1,21 @@
|
||||
//
|
||||
// ProfileTableViewCell.m
|
||||
// Moonlight
|
||||
//
|
||||
// Created by Long Le on 12/11/22.
|
||||
// Copyright © 2022 Moonlight Game Streaming Project. All rights reserved.
|
||||
//
|
||||
|
||||
#import "ProfileTableViewCell.h"
|
||||
|
||||
@implementation ProfileTableViewCell
|
||||
|
||||
- (void)awakeFromNib {
|
||||
[super awakeFromNib];
|
||||
}
|
||||
|
||||
- (void)setSelected:(BOOL)selected animated:(BOOL)animated {
|
||||
[super setSelected:selected animated:animated];
|
||||
}
|
||||
|
||||
@end
|
||||
@@ -0,0 +1,42 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<document type="com.apple.InterfaceBuilder3.CocoaTouch.XIB" version="3.0" toolsVersion="21507" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES">
|
||||
<device id="retina6_12" orientation="portrait" appearance="light"/>
|
||||
<dependencies>
|
||||
<deployment identifier="iOS"/>
|
||||
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="21505"/>
|
||||
<capability name="Safe area layout guides" minToolsVersion="9.0"/>
|
||||
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
|
||||
</dependencies>
|
||||
<objects>
|
||||
<placeholder placeholderIdentifier="IBFilesOwner" id="-1" userLabel="File's Owner"/>
|
||||
<placeholder placeholderIdentifier="IBFirstResponder" id="-2" customClass="UIResponder"/>
|
||||
<tableViewCell contentMode="scaleToFill" selectionStyle="none" accessoryType="checkmark" indentationWidth="10" reuseIdentifier="Cell" id="KGk-i7-Jjw" customClass="ProfileTableViewCell">
|
||||
<rect key="frame" x="0.0" y="0.0" width="320" height="44"/>
|
||||
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
|
||||
<tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" tableViewCell="KGk-i7-Jjw" id="H2p-sc-9uM">
|
||||
<rect key="frame" x="0.0" y="0.0" width="280" height="44"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
<subviews>
|
||||
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Profile Name" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="b1d-b4-TCK">
|
||||
<rect key="frame" x="16.000000000000007" y="11.666666666666664" width="97.666666666666686" height="21"/>
|
||||
<fontDescription key="fontDescription" type="system" pointSize="17"/>
|
||||
<color key="textColor" red="0.95294117647058818" green="0.97647058823529409" blue="1" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
|
||||
<nil key="highlightedColor"/>
|
||||
</label>
|
||||
</subviews>
|
||||
<color key="backgroundColor" red="0.1215686275" green="0.12941176469999999" blue="0.14117647059999999" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
|
||||
<constraints>
|
||||
<constraint firstItem="b1d-b4-TCK" firstAttribute="leading" secondItem="H2p-sc-9uM" secondAttribute="leading" constant="16" id="pxI-vz-oR6"/>
|
||||
<constraint firstItem="b1d-b4-TCK" firstAttribute="centerY" secondItem="H2p-sc-9uM" secondAttribute="centerY" id="xCR-Og-ne7"/>
|
||||
</constraints>
|
||||
</tableViewCellContentView>
|
||||
<viewLayoutGuide key="safeArea" id="aW0-zy-SZf"/>
|
||||
<color key="backgroundColor" red="0.1215686275" green="0.12941176469999999" blue="0.14117647059999999" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
|
||||
<color key="tintColor" red="0.6705882353" green="0.61568627450000002" blue="1" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
|
||||
<connections>
|
||||
<outlet property="name" destination="b1d-b4-TCK" id="CX8-IC-jlM"/>
|
||||
</connections>
|
||||
<point key="canvasLocation" x="108" y="39"/>
|
||||
</tableViewCell>
|
||||
</objects>
|
||||
</document>
|
||||
@@ -0,0 +1,36 @@
|
||||
//
|
||||
// LayoutOnScreenControlsViewController.h
|
||||
// Moonlight
|
||||
//
|
||||
// Created by Long Le on 9/27/22.
|
||||
// Copyright © 2022 Moonlight Game Streaming Project. All rights reserved.
|
||||
//
|
||||
|
||||
#import <UIKit/UIKit.h>
|
||||
#import "LayoutOnScreenControls.h"
|
||||
#import "ToolBarContainerView.h"
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
/**
|
||||
This view controller provides the user interface which allows the user to position on screen controller buttons anywhere they'd like on the screen. It also provides the user with the abilities to undo a change, save the on screen controller layout for later retrieval, and load previously saved controller layouts
|
||||
*/
|
||||
@interface LayoutOnScreenControlsViewController : UIViewController
|
||||
|
||||
|
||||
@property LayoutOnScreenControls *layoutOSC; // object that contains a view which contains the on screen controller buttons that allows the user to drag and positions each button on the screen using touch
|
||||
@property int OSCSegmentSelected;
|
||||
|
||||
@property (weak, nonatomic) IBOutlet UIButton *trashCanButton;
|
||||
@property (weak, nonatomic) IBOutlet UIButton *undoButton;
|
||||
|
||||
@property (weak, nonatomic) IBOutlet ToolBarContainerView *toolbarRootView;
|
||||
@property (weak, nonatomic) IBOutlet UIView *chevronView;
|
||||
@property (weak, nonatomic) IBOutlet UIImageView *chevronImageView;
|
||||
@property (weak, nonatomic) IBOutlet UIStackView *toolbarStackView;
|
||||
|
||||
|
||||
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
||||
+358
@@ -0,0 +1,358 @@
|
||||
//
|
||||
// LayoutOnScreenControlsViewController.m
|
||||
// Moonlight
|
||||
//
|
||||
// Created by Long Le on 9/27/22.
|
||||
// Copyright © 2022 Moonlight Game Streaming Project. All rights reserved.
|
||||
//
|
||||
|
||||
#import "LayoutOnScreenControlsViewController.h"
|
||||
#import "OSCProfilesTableViewController.h"
|
||||
#import "OnScreenButtonState.h"
|
||||
#import "OnScreenControls.h"
|
||||
#import "OSCProfilesManager.h"
|
||||
|
||||
@interface LayoutOnScreenControlsViewController ()
|
||||
|
||||
@end
|
||||
|
||||
|
||||
@implementation LayoutOnScreenControlsViewController {
|
||||
BOOL isToolbarHidden;
|
||||
OSCProfilesManager *profilesManager;
|
||||
__weak IBOutlet NSLayoutConstraint *toolbarTopConstraintiPhone;
|
||||
__weak IBOutlet NSLayoutConstraint *toolbarTopConstraintiPad;
|
||||
}
|
||||
|
||||
@synthesize trashCanButton;
|
||||
@synthesize undoButton;
|
||||
@synthesize OSCSegmentSelected;
|
||||
@synthesize toolbarRootView;
|
||||
@synthesize chevronView;
|
||||
@synthesize chevronImageView;
|
||||
|
||||
- (void) viewDidLoad {
|
||||
[super viewDidLoad];
|
||||
|
||||
profilesManager = [OSCProfilesManager sharedManager];
|
||||
|
||||
isToolbarHidden = NO; // keeps track if the toolbar is hidden up above the screen so that we know whether to hide or show it when the user taps the toolbar's hide/show button
|
||||
|
||||
/* add curve to bottom of chevron tab view */
|
||||
UIBezierPath *maskPath = [UIBezierPath bezierPathWithRoundedRect:self.chevronView.bounds byRoundingCorners:(UIRectCornerBottomLeft | UIRectCornerBottomRight) cornerRadii:CGSizeMake(10.0, 10.0)];
|
||||
CAShapeLayer *maskLayer = [[CAShapeLayer alloc] init];
|
||||
maskLayer.frame = self.view.bounds;
|
||||
maskLayer.path = maskPath.CGPath;
|
||||
self.chevronView.layer.mask = maskLayer;
|
||||
|
||||
/* Add swipe gesture to toolbar to allow user to swipe it up and off screen */
|
||||
UISwipeGestureRecognizer *swipeUp = [[UISwipeGestureRecognizer alloc] initWithTarget:self action:@selector(moveToolbar:)];
|
||||
swipeUp.direction = UISwipeGestureRecognizerDirectionUp;
|
||||
[self.toolbarRootView addGestureRecognizer:swipeUp];
|
||||
|
||||
/* Add tap gesture to toolbar's chevron to allow user to tap it in order to move the toolbar on and off screen */
|
||||
UITapGestureRecognizer *singleFingerTap =
|
||||
[[UITapGestureRecognizer alloc] initWithTarget:self
|
||||
action:@selector(moveToolbar:)];
|
||||
[self.chevronView addGestureRecognizer:singleFingerTap];
|
||||
|
||||
self.layoutOSC = [[LayoutOnScreenControls alloc] initWithView:self.view controllerSup:nil streamConfig:nil oscLevel:OSCSegmentSelected];
|
||||
self.layoutOSC._level = 4;
|
||||
[self.layoutOSC show]; // draw on screen controls
|
||||
|
||||
[self addInnerAnalogSticksToOuterAnalogLayers]; // allows inner and analog sticks to be dragged together around the screen together as one unit which is the expected behavior
|
||||
|
||||
self.undoButton.alpha = 0.3; // no changes to undo yet, so fade out the undo button a bit
|
||||
|
||||
if ([[profilesManager getAllProfiles] count] == 0) { // if no saved OSC profiles exist yet then create one called 'Default' and associate it with Moonlight's legacy 'Full' OSC layout that's already been laid out on the screen at this point
|
||||
[profilesManager saveProfileWithName:@"Default" andButtonLayers:self.layoutOSC.OSCButtonLayers];
|
||||
}
|
||||
|
||||
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(OSCLayoutChanged) name:@"OSCLayoutChanged" object:nil]; // used to notifiy this view controller that the user made a change to the OSC layout so that the VC can either fade in or out its 'Undo button' which will signify to the user whether there are any OSC layout changes to undo
|
||||
|
||||
/* This will animate the toolbar with a subtle up and down motion intended to telegraph to the user that they can hide the toolbar if they wish*/
|
||||
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(0.2 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
|
||||
|
||||
[UIView animateWithDuration:0.3
|
||||
delay:0.25
|
||||
usingSpringWithDamping:0.8
|
||||
initialSpringVelocity:0.5
|
||||
options:UIViewAnimationOptionCurveEaseInOut animations:^{ // Animate toolbar up a a very small distance. Note the 0.35 time delay is necessary to avoid a bug that keeps animations from playing if the animation is presented immediately on a modally presented VC
|
||||
self.toolbarRootView.frame = CGRectMake(self.toolbarRootView.frame.origin.x, self.toolbarRootView.frame.origin.y - 25, self.toolbarRootView.frame.size.width, self.toolbarRootView.frame.size.height);
|
||||
}
|
||||
completion:^(BOOL finished) {
|
||||
[UIView animateWithDuration:0.3
|
||||
delay:0
|
||||
usingSpringWithDamping:0.7
|
||||
initialSpringVelocity:1.0
|
||||
options:UIViewAnimationOptionCurveEaseIn animations:^{ // Animate the toolbar back down that same distance
|
||||
self.toolbarRootView.frame = CGRectMake(self.toolbarRootView.frame.origin.x, self.toolbarRootView.frame.origin.y + 25, self.toolbarRootView.frame.size.width, self.toolbarRootView.frame.size.height);
|
||||
}
|
||||
completion:^(BOOL finished) {
|
||||
NSLog (@"done");
|
||||
}];
|
||||
}];
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
#pragma mark - Class Helper Functions
|
||||
|
||||
/* fades the 'Undo Button' in or out depending on whether the user has any OSC layout changes to undo */
|
||||
- (void) OSCLayoutChanged {
|
||||
if ([self.layoutOSC.layoutChanges count] > 0) {
|
||||
self.undoButton.alpha = 1.0;
|
||||
}
|
||||
else {
|
||||
self.undoButton.alpha = 0.3;
|
||||
}
|
||||
}
|
||||
|
||||
/* animates the toolbar up and off the screen or back down onto the screen */
|
||||
- (void) moveToolbar:(UISwipeGestureRecognizer *)sender {
|
||||
BOOL isPad = [[UIDevice currentDevice].model hasPrefix:@"iPad"];
|
||||
NSLayoutConstraint *toolbarTopConstraint = isPad ? self->toolbarTopConstraintiPad : self->toolbarTopConstraintiPhone;
|
||||
if (isToolbarHidden == NO) {
|
||||
[UIView animateWithDuration:0.2 animations:^{ // animates toolbar up and off screen
|
||||
toolbarTopConstraint.constant -= self.toolbarRootView.frame.size.height;
|
||||
[self.view layoutIfNeeded];
|
||||
|
||||
}
|
||||
completion:^(BOOL finished) {
|
||||
if (finished) {
|
||||
self->isToolbarHidden = YES;
|
||||
self.chevronImageView.image = [UIImage imageNamed:@"ChevronCompactDown"];
|
||||
}
|
||||
}];
|
||||
}
|
||||
else {
|
||||
[UIView animateWithDuration:0.2 animations:^{ // animates the toolbar back down into the screen
|
||||
toolbarTopConstraint.constant += self.toolbarRootView.frame.size.height;
|
||||
[self.view layoutIfNeeded];
|
||||
}
|
||||
completion:^(BOOL finished) {
|
||||
if (finished) {
|
||||
self->isToolbarHidden = NO;
|
||||
self.chevronImageView.image = [UIImage imageNamed:@"ChevronCompactUp"];
|
||||
}
|
||||
}];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Makes the inner analog stick layers a child layer of its corresponding outer analog stick layers so that both the inner and its corresponding outer layers move together when the user drags them around the screen as is the expected behavior when laying out OSC. Note that this is NOT expected behavior on the game stream view where the inner analog sticks move to follow toward the user's touch and their corresponding outer analog stick layers do not move
|
||||
*/
|
||||
- (void)addInnerAnalogSticksToOuterAnalogLayers {
|
||||
// right stick
|
||||
[self.layoutOSC._rightStickBackground addSublayer: self.layoutOSC._rightStick];
|
||||
self.layoutOSC._rightStick.position = CGPointMake(self.layoutOSC._rightStickBackground.frame.size.width / 2, self.layoutOSC._rightStickBackground.frame.size.height / 2);
|
||||
|
||||
// left stick
|
||||
[self.layoutOSC._leftStickBackground addSublayer: self.layoutOSC._leftStick];
|
||||
self.layoutOSC._leftStick.position = CGPointMake(self.layoutOSC._leftStickBackground.frame.size.width / 2, self.layoutOSC._leftStickBackground.frame.size.height / 2);
|
||||
}
|
||||
|
||||
|
||||
#pragma mark - UIButton Actions
|
||||
|
||||
- (IBAction) closeTapped:(id)sender {
|
||||
[self dismissViewControllerAnimated:YES completion:nil];
|
||||
}
|
||||
|
||||
- (IBAction) trashCanTapped:(id)sender {
|
||||
UIAlertController *alert = [UIAlertController alertControllerWithTitle:@"Delete Buttons Here" message:@"Drag and drop buttons onto this trash can to remove them from the interface" preferredStyle:UIAlertControllerStyleAlert];
|
||||
|
||||
UIAlertAction *ok = [UIAlertAction actionWithTitle:@"Ok" style:UIAlertActionStyleDefault handler:nil];
|
||||
[alert addAction:ok];
|
||||
[self presentViewController:alert animated:YES completion:nil];
|
||||
}
|
||||
|
||||
- (IBAction) undoTapped:(id)sender {
|
||||
if ([self.layoutOSC.layoutChanges count] > 0) { // check if there are layout changes to roll back to
|
||||
OnScreenButtonState *buttonState = [self.layoutOSC.layoutChanges lastObject]; // Get the 'OnScreenButtonState' object that contains the name, position, and visiblity state of the button the user last moved
|
||||
|
||||
CALayer *buttonLayer = [self.layoutOSC buttonLayerFromName:buttonState.name]; // get the on screen button layer that corresponds with the 'OnScreenButtonState' object that we retrieved above
|
||||
|
||||
/* Set the button's position and visiblity to what it was before the user last moved it */
|
||||
buttonLayer.position = buttonState.position;
|
||||
buttonLayer.hidden = buttonState.isHidden;
|
||||
|
||||
/* if user is showing or hiding dPad, then show or hide all four dPad button child layers as well since setting the 'hidden' property on the parent CALayer is not automatically setting the individual dPad child CALayers */
|
||||
if ([buttonLayer.name isEqualToString:@"dPad"]) {
|
||||
self.layoutOSC._upButton.hidden = buttonState.isHidden;
|
||||
self.layoutOSC._rightButton.hidden = buttonState.isHidden;
|
||||
self.layoutOSC._downButton.hidden = buttonState.isHidden;
|
||||
self.layoutOSC._leftButton.hidden = buttonState.isHidden;
|
||||
}
|
||||
|
||||
/* if user is showing or hiding the left or right analog sticks, then show or hide their corresponding inner analog stick child layers as well since setting the 'hidden' property on the parent analog stick doesn't automatically hide its child inner analog stick CALayer */
|
||||
if ([buttonLayer.name isEqualToString:@"leftStickBackground"]) {
|
||||
self.layoutOSC._leftStick.hidden = buttonState.isHidden;
|
||||
}
|
||||
if ([buttonLayer.name isEqualToString:@"rightStickBackground"]) {
|
||||
self.layoutOSC._rightStick.hidden = buttonState.isHidden;
|
||||
}
|
||||
|
||||
[self.layoutOSC.layoutChanges removeLastObject];
|
||||
|
||||
[self OSCLayoutChanged]; // will fade the undo button in or out depending on whether there are any further changes to undo
|
||||
}
|
||||
else { // there are no changes to undo. let user know there are no changes to undo
|
||||
UIAlertController * savedAlertController = [UIAlertController alertControllerWithTitle: [NSString stringWithFormat:@"Nothing to Undo"] message: @"There are no changes to undo" preferredStyle:UIAlertControllerStyleAlert];
|
||||
[savedAlertController addAction:[UIAlertAction actionWithTitle:@"Ok" style:UIAlertActionStyleDefault handler:^(UIAlertAction *action) {
|
||||
[savedAlertController dismissViewControllerAnimated:NO completion:nil];
|
||||
}]];
|
||||
[self presentViewController:savedAlertController animated:YES completion:nil];
|
||||
}
|
||||
}
|
||||
|
||||
/* show pop up notification that lets users choose to save the current OSC layout configuration as a profile they can load when they want. User can also choose to cancel out of this pop up */
|
||||
- (IBAction) saveTapped:(id)sender {
|
||||
UIAlertController * inputNameAlertController = [UIAlertController alertControllerWithTitle: @"Enter the name you want to save this controller profile as" message: @"" preferredStyle:UIAlertControllerStyleAlert];
|
||||
[inputNameAlertController addTextFieldWithConfigurationHandler:^(UITextField *textField) { // pop up notification with text field where user can enter the text they wish to name their OSC layout profile
|
||||
textField.placeholder = @"name";
|
||||
textField.textColor = [UIColor lightGrayColor];
|
||||
textField.clearButtonMode = UITextFieldViewModeWhileEditing;
|
||||
textField.borderStyle = UITextBorderStyleNone;
|
||||
}];
|
||||
[inputNameAlertController addAction:[UIAlertAction actionWithTitle:@"Save" style:UIAlertActionStyleDefault handler:^(UIAlertAction *action) { // add save button to allow user to save the on screen controller configuration
|
||||
NSArray *textFields = inputNameAlertController.textFields;
|
||||
UITextField *nameField = textFields[0];
|
||||
NSString *enteredProfileName = nameField.text;
|
||||
|
||||
if ([enteredProfileName isEqualToString:@"Default"]) { // don't let user user overwrite the 'Default' profile
|
||||
UIAlertController *alertController = [UIAlertController alertControllerWithTitle: [NSString stringWithFormat:@""] message: [NSString stringWithFormat:@"Saving over the 'Default' profile is not allowed"] preferredStyle:UIAlertControllerStyleAlert];
|
||||
|
||||
[alertController addAction:[UIAlertAction actionWithTitle:@"Ok" style:UIAlertActionStyleDefault handler:^(UIAlertAction *action) {
|
||||
[alertController dismissViewControllerAnimated:NO completion:^{
|
||||
[self presentViewController:inputNameAlertController animated:YES completion:nil];
|
||||
}];
|
||||
}]];
|
||||
[self presentViewController:alertController animated:YES completion:nil];
|
||||
}
|
||||
else if ([enteredProfileName length] == 0) { // if user entered no text and taps the 'Save' button let them know they can't do that
|
||||
UIAlertController * savedAlertController = [UIAlertController alertControllerWithTitle: [NSString stringWithFormat:@""] message: [NSString stringWithFormat:@"Profile name cannot be blank!"] preferredStyle:UIAlertControllerStyleAlert];
|
||||
|
||||
[savedAlertController addAction:[UIAlertAction actionWithTitle:@"Ok" style:UIAlertActionStyleDefault handler:^(UIAlertAction *action) { // show pop up notification letting user know they must enter a name in the text field if they wish to save the controller profile
|
||||
|
||||
[savedAlertController dismissViewControllerAnimated:NO completion:^{
|
||||
[self presentViewController:inputNameAlertController animated:YES completion:nil];
|
||||
}];
|
||||
}]];
|
||||
[self presentViewController:savedAlertController animated:YES completion:nil];
|
||||
}
|
||||
else if ([self->profilesManager profileNameAlreadyExist:enteredProfileName] == YES) { // if the entered profile name already exists then let the user know. Offer to allow them to overwrite the existing profile
|
||||
UIAlertController * savedAlertController = [UIAlertController alertControllerWithTitle: [NSString stringWithFormat:@""] message: [NSString stringWithFormat:@"Another profile with the name '%@' already exists! Do you want to overwrite it?", enteredProfileName] preferredStyle:UIAlertControllerStyleAlert];
|
||||
|
||||
[savedAlertController addAction:[UIAlertAction actionWithTitle:@"Yes" style:UIAlertActionStyleDefault handler:^(UIAlertAction *action) { // overwrite existing profile
|
||||
[self->profilesManager saveProfileWithName: enteredProfileName andButtonLayers:self.layoutOSC.OSCButtonLayers];
|
||||
}]];
|
||||
|
||||
[savedAlertController addAction:[UIAlertAction actionWithTitle:@"No" style:UIAlertActionStyleDefault handler:^(UIAlertAction *action) { // don't overwrite the existing profile
|
||||
[savedAlertController dismissViewControllerAnimated:NO completion:nil];
|
||||
}]];
|
||||
[self presentViewController:savedAlertController animated:YES completion:nil];
|
||||
}
|
||||
else { // if user entered a valid name that doesn't already exist then save the profile to persistent storage
|
||||
[self->profilesManager saveProfileWithName: enteredProfileName andButtonLayers:self.layoutOSC.OSCButtonLayers];
|
||||
[self->profilesManager setProfileToSelected: enteredProfileName];
|
||||
|
||||
UIAlertController * savedAlertController = [UIAlertController alertControllerWithTitle: [NSString stringWithFormat:@""] message: [NSString stringWithFormat:@"%@ profile saved and set as your active in-game controller profile layout", enteredProfileName] preferredStyle:UIAlertControllerStyleAlert]; // Let user know this profile has been saved and is now the selected controller layout
|
||||
|
||||
[savedAlertController addAction:[UIAlertAction actionWithTitle:@"Ok" style:UIAlertActionStyleDefault handler:^(UIAlertAction *action) {
|
||||
[savedAlertController dismissViewControllerAnimated:NO completion:nil];
|
||||
}]];
|
||||
[self presentViewController:savedAlertController animated:YES completion:nil];
|
||||
}
|
||||
}]];
|
||||
[inputNameAlertController addAction:[UIAlertAction actionWithTitle:@"Cancel" style:UIAlertActionStyleDefault handler:^(UIAlertAction *action) { // adds a button that allows user to decline the option to save the controller layout they currently see on screen
|
||||
[inputNameAlertController dismissViewControllerAnimated:NO completion:nil];
|
||||
}]];
|
||||
[self presentViewController:inputNameAlertController animated:YES completion:nil];
|
||||
}
|
||||
|
||||
/* Presents the view controller that lists all OSC profiles the user can choose from */
|
||||
- (IBAction) loadTapped:(id)sender {
|
||||
UIStoryboard *storyboard;
|
||||
BOOL isIPhone = ([UIDevice currentDevice].userInterfaceIdiom == UIUserInterfaceIdiomPhone);
|
||||
if (isIPhone) {
|
||||
storyboard = [UIStoryboard storyboardWithName:@"iPhone" bundle:nil];
|
||||
}
|
||||
else {
|
||||
storyboard = [UIStoryboard storyboardWithName:@"iPad" bundle:nil];
|
||||
}
|
||||
|
||||
OSCProfilesTableViewController *vc = [storyboard instantiateViewControllerWithIdentifier:@"OSCProfilesTableViewController"] ;
|
||||
|
||||
vc.didDismissOSCProfilesTVC = ^() { // a block that will be called when the modally presented 'OSCProfilesTableViewController' VC is dismissed. By the time the 'OSCProfilesTableViewController' VC is dismissed the user would have potentially selected a different OSC profile with a different layout and they want to see this layout on this 'LayoutOnScreenControlsViewController.' This block of code will load the profile and then hide/show and move each OSC button to their appropriate position
|
||||
[self.layoutOSC updateControls]; // creates and saves a 'Default' OSC profile or loads the one the user selected on the previous screen
|
||||
|
||||
[self addInnerAnalogSticksToOuterAnalogLayers];
|
||||
|
||||
[self.layoutOSC.layoutChanges removeAllObjects]; // since a new OSC profile is being loaded, this will remove all previous layout changes made from the array
|
||||
|
||||
[self OSCLayoutChanged]; // fades the 'Undo Button' out
|
||||
};
|
||||
[self presentViewController:vc animated:YES completion:nil];
|
||||
}
|
||||
|
||||
|
||||
#pragma mark - Touch
|
||||
|
||||
- (void) touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event {
|
||||
for (UITouch* touch in touches) {
|
||||
|
||||
CGPoint touchLocation = [touch locationInView:self.view];
|
||||
touchLocation = [[touch view] convertPoint:touchLocation toView:nil];
|
||||
CALayer *layer = [self.view.layer hitTest:touchLocation];
|
||||
|
||||
if (layer == self.toolbarRootView.layer ||
|
||||
layer == self.chevronView.layer ||
|
||||
layer == self.chevronImageView.layer ||
|
||||
layer == self.toolbarStackView.layer ||
|
||||
layer == self.view.layer) { // don't let user move toolbar or toolbar UI buttons, toolbar's chevron 'pull tab', or the layer associated with this VC's view
|
||||
return;
|
||||
}
|
||||
}
|
||||
[self.layoutOSC touchesBegan:touches withEvent:event];
|
||||
}
|
||||
|
||||
- (void) touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event {
|
||||
[self.layoutOSC touchesMoved:touches withEvent:event];
|
||||
|
||||
if ([self.layoutOSC isLayer:self.layoutOSC.layerBeingDragged
|
||||
hoveringOverButton:trashCanButton]) { // check if user is dragging around a button and hovering it over the trash can button
|
||||
trashCanButton.tintColor = [UIColor redColor];
|
||||
}
|
||||
else {
|
||||
trashCanButton.tintColor = [UIColor colorWithRed:171.0/255.0 green:157.0/255.0 blue:255.0/255.0 alpha:1];
|
||||
}
|
||||
}
|
||||
|
||||
- (void)touchesEnded:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
|
||||
if (self.layoutOSC.layerBeingDragged != nil &&
|
||||
[self.layoutOSC isLayer:self.layoutOSC.layerBeingDragged hoveringOverButton:trashCanButton]) { // check if user wants to throw OSC button into the trash can
|
||||
|
||||
self.layoutOSC.layerBeingDragged.hidden = YES;
|
||||
|
||||
if ([self.layoutOSC.layerBeingDragged.name isEqualToString:@"dPad"]) { // if user is hiding dPad, then hide all four dPad button child layers as well since setting the 'hidden' property on the parent dPad CALayer doesn't automatically hide the four child CALayer dPad buttons
|
||||
self.layoutOSC._upButton.hidden = YES;
|
||||
self.layoutOSC._rightButton.hidden = YES;
|
||||
self.layoutOSC._downButton.hidden = YES;
|
||||
self.layoutOSC._leftButton.hidden = YES;
|
||||
}
|
||||
|
||||
/* if user is hiding left or right analog sticks, then hide their corresponding inner analog stick child layers as well since setting the 'hidden' property on the parent analog stick doesn't automatically hide its child inner analog stick CALayer */
|
||||
if ([self.layoutOSC.layerBeingDragged.name isEqualToString:@"leftStickBackground"]) {
|
||||
self.layoutOSC._leftStick.hidden = YES;
|
||||
}
|
||||
if ([self.layoutOSC.layerBeingDragged.name isEqualToString:@"rightStickBackground"]) {
|
||||
self.layoutOSC._rightStick.hidden = YES;
|
||||
}
|
||||
|
||||
trashCanButton.tintColor = [UIColor colorWithRed:171.0/255.0 green:157.0/255.0 blue:255.0/255.0 alpha:1];
|
||||
}
|
||||
[self.layoutOSC touchesEnded:touches withEvent:event];
|
||||
}
|
||||
|
||||
@end
|
||||
@@ -0,0 +1,24 @@
|
||||
//
|
||||
// OSCProfilesTableViewController.h
|
||||
// Moonlight
|
||||
//
|
||||
// Created by Long Le on 11/28/22.
|
||||
// Copyright © 2022 Moonlight Game Streaming Project. All rights reserved.
|
||||
//
|
||||
|
||||
#import <UIKit/UIKit.h>
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
/**
|
||||
This view displays a list of on screen controller profiles and gives the user the ability to select any of the profiles to be the 'Selected' profile whose on screen controller layout configuration will be shown on the game stream view, or in the on screen controller layout view. This view also allows the user to swipe and delete any of the listed profiles.
|
||||
*/
|
||||
@interface OSCProfilesTableViewController : UIViewController <UITableViewDelegate, UITableViewDataSource>
|
||||
|
||||
@property (weak, nonatomic) IBOutlet UITableView *tableView;
|
||||
@property (nonatomic, copy) void (^didDismissOSCProfilesTVC)(void);
|
||||
|
||||
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
||||
@@ -0,0 +1,158 @@
|
||||
//
|
||||
// OSCProfilesTableViewController.m
|
||||
// Moonlight
|
||||
//
|
||||
// Created by Long Le on 11/28/22.
|
||||
// Copyright © 2022 Moonlight Game Streaming Project. All rights reserved.
|
||||
//
|
||||
|
||||
#import "OSCProfilesTableViewController.h"
|
||||
#import "LayoutOnScreenControlsViewController.h"
|
||||
#import "ProfileTableViewCell.h"
|
||||
#import "OSCProfile.h"
|
||||
#import "OnScreenButtonState.h"
|
||||
#import "OSCProfilesManager.h"
|
||||
|
||||
const double NAV_BAR_HEIGHT = 50;
|
||||
|
||||
@interface OSCProfilesTableViewController ()
|
||||
|
||||
@end
|
||||
|
||||
@implementation OSCProfilesTableViewController {
|
||||
OSCProfilesManager *profilesManager;
|
||||
}
|
||||
|
||||
@synthesize tableView;
|
||||
|
||||
- (void) viewDidLoad {
|
||||
[super viewDidLoad];
|
||||
|
||||
profilesManager = [OSCProfilesManager sharedManager];
|
||||
|
||||
self.tableView.tableHeaderView = [[UIView alloc] initWithFrame:CGRectMake(0, 0, 0, NAV_BAR_HEIGHT)];
|
||||
|
||||
self.tableView.delegate = self;
|
||||
self.tableView.dataSource = self;
|
||||
|
||||
[self.tableView registerNib:[UINib nibWithNibName:@"ProfileTableViewCell" bundle:nil]
|
||||
forCellReuseIdentifier:@"Cell"]; // Register the custom cell nib file with the table view
|
||||
|
||||
}
|
||||
|
||||
- (void) viewDidAppear:(BOOL)animated {
|
||||
[super viewDidAppear: animated];
|
||||
|
||||
if ([[profilesManager getAllProfiles] count] > 0) { // scroll to selected profile if user has any saved profiles
|
||||
NSIndexPath *indexPath = [NSIndexPath indexPathForRow:[profilesManager getIndexOfSelectedProfile] inSection:0];
|
||||
[self.tableView scrollToRowAtIndexPath:indexPath atScrollPosition:UITableViewScrollPositionMiddle animated:YES];
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
#pragma mark - UIButton Actions
|
||||
|
||||
/* Loads the OSC profile that user selected, dismisses this VC, then tells the presenting view controller to lay out the on screen buttons according to the selected profile's instructions */
|
||||
- (IBAction) loadTapped:(id)sender {
|
||||
[self dismissViewControllerAnimated:YES completion:nil];
|
||||
|
||||
if (self.didDismissOSCProfilesTVC) { // tells the presenting view controller to lay out the on screen buttons according to the selected profile's instructions
|
||||
self.didDismissOSCProfilesTVC();
|
||||
}
|
||||
}
|
||||
|
||||
- (IBAction) cancelTapped:(id)sender {
|
||||
[self dismissViewControllerAnimated:YES completion:nil];
|
||||
}
|
||||
|
||||
|
||||
#pragma mark - TableView DataSource
|
||||
|
||||
- (NSInteger) tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section {
|
||||
return [[profilesManager getAllProfiles] count];
|
||||
}
|
||||
|
||||
- (UITableViewCell *) tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
|
||||
ProfileTableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:@"Cell" forIndexPath:indexPath];
|
||||
OSCProfile *profile = [[profilesManager getAllProfiles] objectAtIndex: indexPath.row];
|
||||
cell.name.text = profile.name;
|
||||
|
||||
if ([profile.name isEqualToString: [profilesManager getSelectedProfile].name]) { // if this cell contains the name of the currently selected OSC profile then add a checkmark to the right side of the cell
|
||||
cell.accessoryType = UITableViewCellAccessoryCheckmark;
|
||||
}
|
||||
else {
|
||||
cell.accessoryType = UITableViewCellAccessoryNone;
|
||||
}
|
||||
return cell;
|
||||
}
|
||||
|
||||
- (BOOL) tableView:(UITableView *)tableView canEditRowAtIndexPath:(NSIndexPath *)indexPath {
|
||||
return YES;
|
||||
}
|
||||
|
||||
- (void) tableView:(UITableView *)tableView commitEditingStyle:(UITableViewCellEditingStyle)editingStyle forRowAtIndexPath:(NSIndexPath *)indexPath {
|
||||
NSMutableArray *profiles = [profilesManager getAllProfiles];
|
||||
|
||||
if ([[[profiles objectAtIndex:indexPath.row] name] isEqualToString:@"Default"]) { // if user is attempting to delete the 'Default' profile then show a pop up telling user they can't do that and return out of this method
|
||||
UIAlertController *alertController = [UIAlertController alertControllerWithTitle: [NSString stringWithFormat:@""] message: @"Deleting the 'Default' profile is not allowed" preferredStyle:UIAlertControllerStyleAlert];
|
||||
|
||||
[alertController addAction:[UIAlertAction actionWithTitle:@"Ok" style:UIAlertActionStyleDefault handler:^(UIAlertAction *action) {
|
||||
[alertController dismissViewControllerAnimated:NO completion:nil];
|
||||
}]];
|
||||
[self presentViewController:alertController animated:YES completion:nil];
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (editingStyle == UITableViewCellEditingStyleDelete) {
|
||||
OSCProfile *profile = [profiles objectAtIndex:indexPath.row];
|
||||
if (profile.isSelected) { // if user is deleting the currently selected OSC profile then make the profile at its previous index the currently selected profile
|
||||
if (indexPath.row > 0) { // check that row is greater than zero to avoid an out of bounds crash, although that should not be possible right now since the 'Default' profile is always at row 0 and they're not allowed to delete it
|
||||
OSCProfile *profile = [profiles objectAtIndex:indexPath.row - 1];
|
||||
profile.isSelected = YES;
|
||||
}
|
||||
}
|
||||
|
||||
[profiles removeObjectAtIndex:indexPath.row];
|
||||
|
||||
/* save OSC profiles array to persistent storage */
|
||||
NSMutableArray *profilesEncoded = [[NSMutableArray alloc] init];
|
||||
for (OSCProfile *profileDecoded in profiles) { // encode each OSC profile object and add them to an array
|
||||
|
||||
NSData *profileEncoded = [NSKeyedArchiver archivedDataWithRootObject:profileDecoded requiringSecureCoding:YES error:nil];
|
||||
[profilesEncoded addObject:profileEncoded];
|
||||
}
|
||||
NSData *data = [NSKeyedArchiver archivedDataWithRootObject:profilesEncoded
|
||||
requiringSecureCoding:YES error:nil]; // encode the array itself, NOT the objects in the array
|
||||
[[NSUserDefaults standardUserDefaults] setObject:data forKey:@"OSCProfiles"];
|
||||
[[NSUserDefaults standardUserDefaults] synchronize];
|
||||
|
||||
[tableView reloadData];
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
#pragma mark - TableView Delegate
|
||||
|
||||
/* When user taps a cell it moves the checkmark to that cell indicating to the user the profile associated with that cell is now the selected profile. It also sets that cell's associated OSCProfile object's 'isSelected' property to YES */
|
||||
- (void) tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath
|
||||
{
|
||||
NSIndexPath *selectedIndexPath = [NSIndexPath indexPathForRow:indexPath.row inSection:0];
|
||||
NSIndexPath *lastSelectedIndexPath = [NSIndexPath indexPathForRow:[profilesManager getIndexOfSelectedProfile] inSection:0];
|
||||
|
||||
if (selectedIndexPath != lastSelectedIndexPath) {
|
||||
/* Place checkmark on selected cell and set profile associated with cell as selected profile */
|
||||
UITableViewCell *selectedCell = [tableView cellForRowAtIndexPath: selectedIndexPath];
|
||||
selectedCell.accessoryType = UITableViewCellAccessoryCheckmark; // add checkmark to the cell the user tapped
|
||||
OSCProfile *profile = [[profilesManager getAllProfiles] objectAtIndex:indexPath.row];
|
||||
[profilesManager setProfileToSelected: profile.name]; // set the profile associated with this cell's 'isSelected' property to YES
|
||||
|
||||
/* Remove checkmark on the previously selected cell */
|
||||
UITableViewCell *lastSelectedCell = [tableView cellForRowAtIndexPath: lastSelectedIndexPath];
|
||||
lastSelectedCell.accessoryType = UITableViewCellAccessoryNone;
|
||||
[tableView deselectRowAtIndexPath:lastSelectedIndexPath animated:YES];
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@end
|
||||
@@ -0,0 +1,18 @@
|
||||
//
|
||||
// ToolBarContainerView.h
|
||||
// Moonlight
|
||||
//
|
||||
// Created by Long Le on 12/10/22.
|
||||
// Copyright © 2022 Moonlight Game Streaming Project. All rights reserved.
|
||||
//
|
||||
|
||||
#import <UIKit/UIKit.h>
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
/* Allows the ToolBar in the 'LayoutOnScreenControlsViewController' to accept touches on the small UIView 'tab' protruding from its bottom center*/
|
||||
@interface ToolBarContainerView : UIView
|
||||
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
||||
@@ -0,0 +1,26 @@
|
||||
//
|
||||
// ToolBarContainerView.m
|
||||
// Moonlight
|
||||
//
|
||||
// Created by Long Le on 12/10/22.
|
||||
// Copyright © 2022 Moonlight Game Streaming Project. All rights reserved.
|
||||
//
|
||||
|
||||
#import "ToolBarContainerView.h"
|
||||
|
||||
@implementation ToolBarContainerView
|
||||
|
||||
- (BOOL) pointInside:(CGPoint)point withEvent:(UIEvent *)event {
|
||||
|
||||
for (UIView *view in self.subviews) {
|
||||
|
||||
if (!view.hidden && view.alpha > 0 &&
|
||||
view.userInteractionEnabled &&
|
||||
[view pointInside:[self convertPoint:point toView:view] withEvent:event])
|
||||
return YES;
|
||||
}
|
||||
|
||||
return NO;
|
||||
}
|
||||
|
||||
@end
|
||||
Reference in New Issue
Block a user