Files
PojavLauncher_iOS/Natives/LauncherPrefContCfgViewController.m
khanhduytran0 6cf61e62c0 Feats: preference isolation & profile management
- Rewrote LauncherPreferences -> PLPreferences
 + Now the file layout is the same as UI layout
 + (get/set)Preference are replaced with corresponding get/set types + Getting/setting is now done through keyPath instead of manually iterating over subdictionaries.
 + Implemented an isolation layer, allowing an instance to have its own settings. This is off by default

- Implemented launcher_profiles
 + Added Profile manager that can be accessed from the sidebar
 + Moved "Game directory (instances)" to this screen
 + Replaced version selector with profile selector. Version selector is moved into profile editor.

- Other changes
 + Cleanup code by using NSPredicate instead of huge for loops
 + Titles in settings are made multilined
 + Controller config: replaced bar button items with localized ones

Due to how large the changes are, this is committed directly to the mainline branch. Bugs are expected to be found (although I did enough tests for this locally)
2023-07-06 13:59:07 +07:00

321 lines
15 KiB
Objective-C

#import <Foundation/Foundation.h>
#import <objc/runtime.h>
#import "input/ControllerInput.h"
#import "customcontrols/CustomControlsUtils.h"
#import "FileListViewController.h"
#import "LauncherNavigationController.h"
#import "LauncherPreferences.h"
#import "LauncherPrefContCfgViewController.h"
#import "PickTextField.h"
#import "ios_uikit_bridge.h"
#import "utils.h"
#include "glfw_keycodes.h"
#define contentNavigationController (LauncherNavigationController *)UIApplication.sharedApplication.keyWindow.rootViewController.splitViewController.viewControllers[1]
typedef void(^CreateView)(UITableViewCell *, NSString *, NSDictionary *);
@interface LauncherPrefContCfgViewController ()<UITextFieldDelegate, UIPopoverPresentationControllerDelegate, UIPickerViewDataSource, UIPickerViewDelegate>
@property(nonatomic) NSString *currentFileName;
@property(nonatomic) NSMutableDictionary *currentMappings;
@property(nonatomic) NSDictionary *keycodePlist;
@property(nonatomic) UIPickerView *editPickMapping;
@property(nonatomic) UIToolbar *editPickToolbar;
@property(nonatomic) UITextField *activeTextField;
@property(nonatomic) NSArray<NSString*>* prefSections;
@property(nonatomic) NSMutableArray<NSNumber*>* prefSectionsVisibility;
@property(nonatomic) NSArray<NSDictionary*> *prefControllerTypes;
@property(nonatomic) NSMutableArray *keyCodeMap, *keyValueMap;
@end
@implementation LauncherPrefContCfgViewController
- (void)viewDidLoad
{
[super viewDidLoad];
[self setTitle:localize(@"preference.title.default_gamepad_ctrl", nil)];
self.keycodePlist = [NSDictionary dictionaryWithContentsOfFile:[[NSBundle mainBundle] pathForResource:@"glfw_keycodes" ofType:@"plist"]];
self.keyCodeMap = [[NSMutableArray alloc] init];
self.keyValueMap = [[NSMutableArray alloc] init];
initKeycodeTable(self.keyCodeMap, self.keyValueMap);
self.tableView = [[UITableView alloc] initWithFrame:CGRectZero style:UITableViewStyleInsetGrouped];
self.tableView.keyboardDismissMode = UIScrollViewKeyboardDismissModeInteractive;
self.tableView.sectionFooterHeight = 50;
[self loadGamepadConfigurationFile];
self.prefControllerTypes = @[@{@"name": @"xbox"}, @{@"name": @"playstation"}];
self.prefSections = @[@"config_files", @"game_mappings", @"menu_mappings", @"controller_style"];
self.navigationItem.leftBarButtonItem = [[UIBarButtonItem alloc] initWithBarButtonSystemItem:UIBarButtonSystemItemSave target:self action:@selector(actionMenuSave)];
self.navigationItem.rightBarButtonItem = [[UIBarButtonItem alloc] initWithBarButtonSystemItem:UIBarButtonSystemItemClose target:self action:@selector(exitButtonSelector)];
self.editPickMapping = [[UIPickerView alloc] init];
self.editPickMapping.delegate = self;
self.editPickMapping.dataSource = self;
self.editPickToolbar = [[UIToolbar alloc] initWithFrame:CGRectMake(0.0, 0.0, self.view.frame.size.width, 44.0)];
UIBarButtonItem *btnFlexibleSpace = [[UIBarButtonItem alloc] initWithBarButtonSystemItem:UIBarButtonSystemItemFlexibleSpace target:self action:nil];
UIBarButtonItem *editDoneButton = [[UIBarButtonItem alloc] initWithBarButtonSystemItem:UIBarButtonSystemItemDone target:self action:@selector(closeTextField:)];
self.editPickToolbar.items = @[btnFlexibleSpace, editDoneButton];
}
- (void)loadGamepadConfigurationFile {
NSString *gamepadPath = [NSString stringWithFormat:@"%s/controlmap/gamepads/%@", getenv("POJAV_HOME"), getPrefObject(@"control.default_gamepad_ctrl")];
self.currentMappings = parseJSONFromFile(gamepadPath);
self.currentFileName = [getPrefObject(@"control.default_ctrl") stringByDeletingPathExtension];
NSPredicate *filterPredicate = [NSPredicate predicateWithBlock:^BOOL(id obj, NSDictionary *dict) {
return ![obj[@"name"] hasPrefix:@"mouse_"];
}];
[self.currentMappings[@"mGameMappingList"] filterUsingPredicate:filterPredicate];
[self.currentMappings[@"mMenuMappingList"] filterUsingPredicate:filterPredicate];
}
- (void)viewWillAppear:(BOOL)animated {
[super viewWillAppear:animated];
}
#pragma mark External UITableView functions
- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView {
return self.prefSections.count;
}
- (NSArray *)prefContentForIndex:(NSInteger)index {
switch (index) {
case 0: return nil; // one single cell is defined in cellForRowAtIndexPath
case 1: return self.currentMappings[@"mGameMappingList"];
case 2: return self.currentMappings[@"mMenuMappingList"];
case 3: return self.prefControllerTypes;
default: return nil;
}
}
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section {
if (section == 0) {
return 1;
} else {
return [self prefContentForIndex:section].count;
}
}
- (UITableViewCell *)tableView:(nonnull UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
NSMutableDictionary *item = [self prefContentForIndex:indexPath.section][indexPath.row];
NSString *cellID = [NSString stringWithFormat:@"cellValue%ld", indexPath.section];
UITableViewCellStyle cellStyle = UITableViewCellStyleValue1;
UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:cellID];
if (cell == nil) {
cell = [[UITableViewCell alloc] initWithStyle:cellStyle reuseIdentifier:cellID];
cell.detailTextLabel.numberOfLines = 0;
cell.detailTextLabel.lineBreakMode = NSLineBreakByWordWrapping;
cell.selectionStyle = UITableViewCellSelectionStyleGray;
}
if(indexPath.section == 0) {
cell.textLabel.text = localize(@"controller_configurator.title.current", nil);
cell.accessoryType = UITableViewCellAccessoryDisclosureIndicator;
cell.detailTextLabel.text = getPrefObject(@"control.default_gamepad_ctrl");
} else if(indexPath.section == 1 || indexPath.section == 2) {
NSNumber *keycode = (NSNumber *)item[@"keycode"];
cell.textLabel.text = localize(([NSString stringWithFormat:@"controller_configurator.%@.title.%@", getPrefObject(@"control.controller_type"), item[@"name"]]), nil);
UITextField *view = (id)cell.accessoryView;
if (view == nil) {
view = [[PickTextField alloc] initWithFrame:CGRectMake(0, 0, cell.bounds.size.width / 2.1, cell.bounds.size.height)];
[view addTarget:view action:@selector(resignFirstResponder) forControlEvents:UIControlEventEditingDidEndOnExit];
view.autocorrectionType = UITextAutocorrectionTypeNo;
view.autocapitalizationType = UITextAutocapitalizationTypeNone;
view.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleLeftMargin;
view.delegate = self;
view.returnKeyType = UIReturnKeyDone;
view.tag = indexPath.section;
view.textAlignment = NSTextAlignmentRight;
view.tintColor = UIColor.clearColor;
view.adjustsFontSizeToFitWidth = YES;
view.inputAccessoryView = self.editPickToolbar;
view.inputView = self.editPickMapping;
cell.accessoryView = view;
}
view.text = self.keyCodeMap[[self.keyValueMap indexOfObject:keycode]];
objc_setAssociatedObject(view, @"gamepad_button", item[@"name"], OBJC_ASSOCIATION_ASSIGN);
objc_setAssociatedObject(view, @"item", item, OBJC_ASSOCIATION_ASSIGN);
} else if(indexPath.section == 3) {
cell.textLabel.text = localize([NSString stringWithFormat:@"controller_configurator.title.type.%@", item[@"name"]], nil);
if ([getPrefObject(@"control.controller_type") isEqualToString:item[@"name"]]) {
cell.accessoryType = UITableViewCellAccessoryCheckmark;
} else {
cell.accessoryType = UITableViewCellAccessoryNone;
}
}
return cell;
}
- (NSString *)tableView:(UITableView *)tableView titleForHeaderInSection:(NSInteger)section {
return localize([NSString stringWithFormat:@"controller_configurator.section.%@", self.prefSections[section]], nil);
}
- (NSString *)tableView:(UITableView *)tableView titleForFooterInSection:(NSInteger)section {
NSString *key = [NSString stringWithFormat:@"controller_configurator.section.footer.%@", self.prefSections[section]];
NSString *footer = localize(key, nil);
return [footer isEqualToString:key] ? nil : footer;
}
- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath {
[tableView deselectRowAtIndexPath:indexPath animated:NO];
NSDictionary *item = [self prefContentForIndex:indexPath.section][indexPath.row];
UITableViewCell *cell = [self.tableView cellForRowAtIndexPath:indexPath];
if(indexPath.section == 0) {
[self actionMenuLoad];
} else if(indexPath.section == 3) {
setPrefObject(@"control.controller_type", self.prefControllerTypes[indexPath.row][@"name"]);
NSIndexSet *section = [NSIndexSet indexSetWithIndexesInRange:NSMakeRange(1, 3)];
[self.tableView reloadSections:section withRowAnimation:UITableViewRowAnimationNone];
}
// 1 and 2 handle themselves with picker views.
}
#pragma mark UITextField + UIPickerView
- (UIView *)pickerView:(UIPickerView *)pickerView viewForRow:(NSInteger)row forComponent:(NSInteger)component reusingView:(UIView *)view
{
UILabel *label = (UILabel *)view;
if (label == nil) {
label = [UILabel new];
label.adjustsFontSizeToFitWidth = YES;
label.minimumScaleFactor = 0.5;
label.textAlignment = NSTextAlignmentCenter;
}
label.text = [self pickerView:pickerView titleForRow:row forComponent:component];
return label;
}
- (void)pickerView:(UIPickerView *)pickerView didSelectRow:(NSInteger)row inComponent:(NSInteger)component {
self.activeTextField.text = [self.keyCodeMap objectAtIndex:row];
}
- (NSInteger)numberOfComponentsInPickerView:(UIPickerView *)thePickerView {
return 1;
}
- (NSInteger)pickerView:(UIPickerView *)pickerView numberOfRowsInComponent:(NSInteger)component {
return self.keyCodeMap.count;
}
- (NSString *)pickerView:(UIPickerView *)pickerView titleForRow:(NSInteger)row forComponent:(NSInteger)component {
return [self.keyCodeMap objectAtIndex:row];
}
- (void)textFieldDidBeginEditing:(UITextField *)textField
{
self.activeTextField = textField;
[self.editPickMapping selectRow:[self.keyCodeMap indexOfObject:textField.text] inComponent:0 animated:NO];
}
- (void)textFieldDidEndEditing:(UITextField *)textField
{
UITableViewCell *cell = (UITableViewCell *)textField.superview;
NSMutableDictionary *item = objc_getAssociatedObject(cell.accessoryView, @"item");
if(![textField.text hasPrefix:@"SPECIALBTN"]) {
item[@"keycode"] = self.keycodePlist[[@"GLFW_KEY_" stringByAppendingString:textField.text]];
} else {
item[@"keycode"] = self.keycodePlist[textField.text];
}
self.activeTextField = nil;
}
- (void)closeTextField:(UIBarButtonItem *)sender {
[self.activeTextField endEditing:YES];
}
#pragma mark UI
- (void) dismissModalViewController {
[self.navigationController dismissViewControllerAnimated:YES completion:nil];
}
- (void)actionOpenFilePicker:(void (^)(NSString *name))handler {
FileListViewController *vc = [[FileListViewController alloc] init];
vc.listPath = [NSString stringWithFormat:@"%s/controlmap/gamepads", getenv("POJAV_HOME")];
vc.whenItemSelected = handler;
vc.modalPresentationStyle = UIModalPresentationPopover;
vc.preferredContentSize = CGSizeMake(350, 250);
UIPopoverPresentationController *popoverController = [vc popoverPresentationController];
popoverController.sourceView = self.tableView;
popoverController.sourceRect = [self.tableView rectForRowAtIndexPath:[NSIndexPath indexPathForRow:0 inSection:0]];
popoverController.delegate = self;
[self presentViewController:vc animated:YES completion:nil];
}
- (void)actionMenuLoad {
[self actionOpenFilePicker:^void(NSString* name) {
NSString *currentFile = [NSString stringWithFormat:@"%@.json", getPrefObject(@"control.default_gamepad_ctrl")];
if(![currentFile isEqualToString:name]) {
setPrefObject(@"control.default_gamepad_ctrl", [NSString stringWithFormat:@"%@.json", name]);
[self loadGamepadConfigurationFile];
[self.tableView reloadData];
}
}];
}
- (void)actionMenuSaveWithExit:(BOOL)exit {
UIAlertController *controller = [UIAlertController alertControllerWithTitle:localize(@"custom_controls.control_menu.save", nil)
message:exit?localize(@"custom_controls.control_menu.exit.warn", nil):@""
preferredStyle:UIAlertControllerStyleAlert];
[controller addTextFieldWithConfigurationHandler:^(UITextField *textField) {
textField.placeholder = @"Name";
textField.text = self.currentFileName;
textField.clearButtonMode = UITextFieldViewModeWhileEditing;
textField.borderStyle = UITextBorderStyleRoundedRect;
}];
[controller addAction:[UIAlertAction actionWithTitle:localize(@"OK", nil) style:UIAlertActionStyleDefault handler:^(UIAlertAction *action) {
NSArray *textFields = controller.textFields;
UITextField *field = textFields[0];
NSError *error;
NSData *jsonData = [NSJSONSerialization dataWithJSONObject:self.currentMappings options:NSJSONWritingPrettyPrinted error:&error];
if (jsonData == nil) {
showDialog(localize(@"custom_controls.control_menu.save.error.json", nil), error.localizedDescription);
return;
}
BOOL success = [jsonData writeToFile:[NSString stringWithFormat:@"%s/controlmap/gamepads/%@.json", getenv("POJAV_HOME"), field.text] options:NSDataWritingAtomic error:&error];
if (!success) {
showDialog(localize(@"custom_controls.control_menu.save.error.write", nil), error.localizedDescription);
return;
}
if (exit) {
[self dismissModalViewController];
}
setPrefObject(@"control.default_gamepad_ctrl", [NSString stringWithFormat:@"%@.json", field.text]);
}]];
if (exit) {
[controller addAction:[UIAlertAction actionWithTitle:localize(@"custom_controls.control_menu.discard_changes", nil) style:UIAlertActionStyleDestructive handler:^(UIAlertAction *action) {
[self dismissModalViewController];
}]];
}
[controller addAction:[UIAlertAction actionWithTitle:localize(@"Cancel", nil) style:UIAlertActionStyleCancel handler:nil]];
[self presentViewController:controller animated:YES completion:nil];
}
- (void)actionMenuSave {
[self actionMenuSaveWithExit:NO];
}
- (void)exitButtonSelector {
NSString *gamepadPath = [NSString stringWithFormat:@"%s/controlmap/gamepads/%@", getenv("POJAV_HOME"), getPrefObject(@"control.default_gamepad_ctrl")];
if([self.currentMappings isEqualToDictionary:parseJSONFromFile(gamepadPath)]) {
[self dismissModalViewController];
} else {
[self actionMenuSaveWithExit:YES];
}
}
@end