876 lines
35 KiB
Objective-C
876 lines
35 KiB
Objective-C
#import <Foundation/Foundation.h>
|
|
#import <objc/runtime.h>
|
|
|
|
#import "DBNumberedSlider.h"
|
|
#import "LauncherNavigationController.h"
|
|
#import "LauncherMenuViewController.h"
|
|
#import "LauncherPreferences.h"
|
|
#import "LauncherPreferencesViewController.h"
|
|
#import "LauncherPrefContCfgViewController.h"
|
|
#import "LauncherPrefGameDirViewController.h"
|
|
#import "UIKit+hook.h"
|
|
|
|
#import "config.h"
|
|
#import "ios_uikit_bridge.h"
|
|
#import "utils.h"
|
|
|
|
#define sidebarNavController ((UINavigationController *)self.splitViewController.viewControllers[0])
|
|
#define sidebarViewController ((LauncherMenuViewController *)sidebarNavController.viewControllers[0])
|
|
|
|
typedef void(^CreateView)(UITableViewCell *, NSString *, NSDictionary *);
|
|
|
|
@interface UIContextMenuInteraction(private)
|
|
- (void)_presentMenuAtLocation:(CGPoint)location;
|
|
@end
|
|
@interface _UIContextMenuStyle : NSObject <NSCopying>
|
|
@property(nonatomic) NSInteger preferredLayout;
|
|
+ (instancetype)defaultStyle;
|
|
@end
|
|
|
|
@interface LauncherPreferencesViewController()<UIContextMenuInteractionDelegate>{}
|
|
@property(nonatomic) UIMenu* currentMenu;
|
|
@property(nonatomic) NSArray<NSString*>* prefSections, *rendererKeys, *rendererList;
|
|
@property(nonatomic) NSMutableArray<NSNumber*>* prefSectionsVisibility;
|
|
@property(nonatomic) NSArray<NSArray<NSDictionary*>*>* prefContents;
|
|
@property(nonatomic) BOOL prefDetailVisible;
|
|
@property(nonatomic) UIBarButtonItem *helpBtn;
|
|
|
|
@property CreateView typeButton, typeChildPane, typePickField, typeTextField, typeSlider, typeSwitch;
|
|
@end
|
|
|
|
@implementation LauncherPreferencesViewController
|
|
|
|
- (NSString *)imageName {
|
|
return @"MenuSettings";
|
|
}
|
|
|
|
- (void)viewDidLoad
|
|
{
|
|
[super viewDidLoad];
|
|
|
|
[self initViewCreation];
|
|
|
|
self.prefDetailVisible = self.navigationController == nil;
|
|
|
|
self.tableView = [[UITableView alloc] initWithFrame:CGRectZero style:UITableViewStyleInsetGrouped];
|
|
self.tableView.keyboardDismissMode = UIScrollViewKeyboardDismissModeInteractive;
|
|
|
|
if (self.navigationController == nil) {
|
|
self.tableView.alpha = 0.9;
|
|
}
|
|
|
|
BOOL(^whenNotInGame)() = ^BOOL(){
|
|
return self.navigationController != nil;
|
|
};
|
|
|
|
self.prefSections = @[@"general", @"video", @"control", @"java", @"debug"];
|
|
self.prefSectionsVisibility = [[NSMutableArray alloc] initWithCapacity:self.prefSections.count];
|
|
for (int i = 0; i < self.prefSections.count; i++) {
|
|
[self.prefSectionsVisibility addObject:@NO];
|
|
}
|
|
|
|
#if CONFIG_RELEASE
|
|
if(@available(iOS 16.0, *)) {
|
|
// Disabling Zink on iOS 16.0+ to figure out what's wrong with it
|
|
self.rendererKeys = @[
|
|
@"auto",
|
|
@ RENDERER_NAME_GL4ES,
|
|
@ RENDERER_NAME_MTL_ANGLE
|
|
];
|
|
self.rendererList = @[
|
|
localize(@"preference.title.renderer.release.auto", nil),
|
|
localize(@"preference.title.renderer.release.gl4es", nil),
|
|
localize(@"preference.title.renderer.release.angle", nil)
|
|
];
|
|
} else {
|
|
#endif
|
|
self.rendererKeys = @[
|
|
@"auto",
|
|
@ RENDERER_NAME_GL4ES,
|
|
@ RENDERER_NAME_MTL_ANGLE,
|
|
@ RENDERER_NAME_VK_ZINK
|
|
];
|
|
self.rendererList = @[
|
|
localize(@"preference.title.renderer.debug.auto", nil),
|
|
localize(@"preference.title.renderer.debug.gl4es", nil),
|
|
localize(@"preference.title.renderer.debug.angle", nil),
|
|
localize(@"preference.title.renderer.debug.zink", nil)
|
|
];
|
|
#if CONFIG_RELEASE
|
|
}
|
|
#endif
|
|
|
|
self.prefContents = @[
|
|
@[
|
|
// General settings
|
|
@{@"icon": @"cube"},
|
|
@{@"key": @"game_directory",
|
|
@"icon": @"folder",
|
|
@"type": self.typeChildPane,
|
|
@"enableCondition": ^BOOL(){
|
|
return whenNotInGame() && !getenv("DEMO_LOCK");
|
|
},
|
|
@"canDismissWithSwipe": @YES,
|
|
@"class": LauncherPrefGameDirViewController.class,
|
|
},
|
|
@{@"key": @"check_sha",
|
|
@"hasDetail": @YES,
|
|
@"icon": @"lock.shield",
|
|
@"type": self.typeSwitch,
|
|
@"enableCondition": whenNotInGame
|
|
},
|
|
@{@"key": @"cosmetica",
|
|
@"hasDetail": @YES,
|
|
@"icon": @"eyeglasses",
|
|
@"type": self.typeSwitch,
|
|
@"enableCondition": whenNotInGame
|
|
},
|
|
@{@"key": @"debug_logging",
|
|
@"hasDetail": @YES,
|
|
@"icon": @"doc.badge.gearshape",
|
|
@"type": self.typeSwitch,
|
|
@"action": ^(BOOL enabled){
|
|
debugLogEnabled = enabled;
|
|
NSLog(@"[Debugging] Debug log enabled: %@", enabled ? @"YES" : @"NO");
|
|
}
|
|
},
|
|
@{@"key": @"jitstreamer_server",
|
|
@"hasDetail": @YES,
|
|
@"icon": @"hare",
|
|
@"type": self.typeTextField,
|
|
@"enableCondition": whenNotInGame
|
|
},
|
|
@{@"key": @"enable_altkit",
|
|
@"hasDetail": @YES,
|
|
@"icon": @"network",
|
|
@"type": self.typeSwitch,
|
|
@"enableCondition": whenNotInGame
|
|
},
|
|
@{@"key": @"appicon",
|
|
@"hasDetail": @YES,
|
|
@"icon": @"paintbrush",
|
|
@"type": self.typePickField,
|
|
@"enableCondition": ^BOOL(){
|
|
return UIApplication.sharedApplication.supportsAlternateIcons;
|
|
},
|
|
@"action": ^void(NSString *iconName) {
|
|
if ([iconName isEqualToString:@"AppIcon-Light"]) {
|
|
iconName = nil;
|
|
}
|
|
[UIApplication.sharedApplication setAlternateIconName:iconName completionHandler:^(NSError * _Nullable error) {
|
|
if (error == nil) return;
|
|
NSLog(@"Error in appicon: %@", error);
|
|
showDialog(self, localize(@"Error", nil), error.localizedDescription);
|
|
}];
|
|
},
|
|
@"pickKeys": @[
|
|
@"AppIcon-Light",
|
|
@"AppIcon-Dark",
|
|
@"AppIcon-Development"
|
|
],
|
|
@"pickList": @[
|
|
localize(@"preference.title.appicon-default", nil),
|
|
localize(@"preference.title.appicon-dark", nil),
|
|
localize(@"preference.title.appicon-development", nil)
|
|
]
|
|
},
|
|
@{@"key": @"hidden_sidebar",
|
|
@"hasDetail": @YES,
|
|
@"icon": @"sidebar.leading",
|
|
@"type": self.typeSwitch,
|
|
@"enableCondition": whenNotInGame
|
|
},
|
|
@{@"key": @"reset_warnings",
|
|
@"icon": @"exclamationmark.triangle",
|
|
@"type": self.typeButton,
|
|
@"enableCondition": whenNotInGame,
|
|
@"action": ^void(){
|
|
resetWarnings();
|
|
}
|
|
},
|
|
@{@"key": @"reset_settings",
|
|
@"icon": @"trash",
|
|
@"type": self.typeButton,
|
|
@"enableCondition": whenNotInGame,
|
|
@"requestReload": @YES,
|
|
@"showConfirmPrompt": @YES,
|
|
@"destructive": @YES,
|
|
@"action": ^void(){
|
|
loadPreferences(YES);
|
|
[self.tableView reloadData];
|
|
}
|
|
},
|
|
@{@"key": @"erase_demo_data",
|
|
@"icon": @"trash",
|
|
@"type": self.typeButton,
|
|
@"enableCondition": ^BOOL(){
|
|
NSString *demoPath = [NSString stringWithFormat:@"%s/.demo", getenv("POJAV_HOME")];
|
|
int count = [NSFileManager.defaultManager contentsOfDirectoryAtPath:demoPath error:nil].count;
|
|
return whenNotInGame() && count > 0;
|
|
},
|
|
@"showConfirmPrompt": @YES,
|
|
@"destructive": @YES,
|
|
@"action": ^void(){
|
|
NSString *demoPath = [NSString stringWithFormat:@"%s/.demo", getenv("POJAV_HOME")];
|
|
NSError *error;
|
|
if([NSFileManager.defaultManager removeItemAtPath:demoPath error:&error]) {
|
|
[NSFileManager.defaultManager createDirectoryAtPath:demoPath
|
|
withIntermediateDirectories:YES attributes:nil error:nil];
|
|
if ([getPreference(@"selected_version_type") intValue] == 0 && getenv("DEMO_LOCK")) {
|
|
[(LauncherNavigationController *)self.navigationController reloadVersionList:0];
|
|
}
|
|
} else {
|
|
NSLog(@"Error in erase_demo_data: %@", error);
|
|
showDialog(self, localize(@"Error", nil), error.localizedDescription);
|
|
}
|
|
}
|
|
}
|
|
], @[
|
|
// Video and renderer settings
|
|
@{@"icon": @"video"},
|
|
@{@"key": @"renderer",
|
|
@"hasDetail": @YES,
|
|
@"icon": @"cpu",
|
|
@"type": self.typePickField,
|
|
@"enableCondition": whenNotInGame,
|
|
@"pickKeys": self.rendererKeys,
|
|
@"pickList": self.rendererList
|
|
},
|
|
@{@"key": @"resolution",
|
|
@"hasDetail": @YES,
|
|
@"icon": @"viewfinder",
|
|
@"type": self.typeSlider,
|
|
@"min": @(25),
|
|
@"max": @(150)
|
|
},
|
|
@{@"key": @"max_framerate",
|
|
@"hasDetail": @YES,
|
|
@"icon": @"timelapse",
|
|
@"type": self.typeSwitch,
|
|
@"enableCondition": ^BOOL(){
|
|
return whenNotInGame() && (UIScreen.mainScreen.maximumFramesPerSecond > 60);
|
|
}
|
|
},
|
|
@{@"key": @"fullscreen_airplay",
|
|
@"hasDetail": @YES,
|
|
@"icon": @"airplayvideo",
|
|
@"type": self.typeSwitch,
|
|
@"action": ^(BOOL enabled){
|
|
if (self.navigationController != nil) return;
|
|
if (UIApplication.sharedApplication.connectedScenes.count < 2) return;
|
|
if (enabled) {
|
|
[self.presentingViewController performSelector:@selector(switchToExternalDisplay)];
|
|
} else {
|
|
[self.presentingViewController performSelector:@selector(switchToInternalDisplay)];
|
|
}
|
|
}
|
|
},
|
|
@{@"key": @"silence_other_audio",
|
|
@"hasDetail": @YES,
|
|
@"icon": @"speaker.slash",
|
|
@"type": self.typeSwitch
|
|
},
|
|
@{@"key": @"silence_with_switch",
|
|
@"hasDetail": @YES,
|
|
@"icon": @"speaker.zzz",
|
|
@"type": self.typeSwitch
|
|
},
|
|
], @[
|
|
// Control settings
|
|
@{@"icon": @"gamecontroller"},
|
|
@{@"key": @"default_gamepad_ctrl",
|
|
@"icon": @"hammer",
|
|
@"type": self.typeChildPane,
|
|
@"enableCondition": whenNotInGame,
|
|
@"canDismissWithSwipe": @NO,
|
|
@"class": LauncherPrefContCfgViewController.class
|
|
},
|
|
@{@"key": @"hardware_hide",
|
|
@"icon": @"eye.slash",
|
|
@"hasDetail": @YES,
|
|
@"type": self.typeSwitch,
|
|
},
|
|
@{@"key": @"gesture_mouse",
|
|
@"icon": @"cursorarrow.click",
|
|
@"hasDetail": @YES,
|
|
@"type": self.typeSwitch,
|
|
},
|
|
@{@"key": @"gesture_hotbar",
|
|
@"icon": @"hand.tap",
|
|
@"hasDetail": @YES,
|
|
@"type": self.typeSwitch,
|
|
},
|
|
@{@"key": @"disable_haptics",
|
|
@"icon": @"wave.3.left",
|
|
@"hasDetail": @NO,
|
|
@"type": self.typeSwitch,
|
|
},
|
|
@{@"key": @"slideable_hotbar",
|
|
@"hasDetail": @YES,
|
|
@"icon": @"slider.horizontal.below.rectangle",
|
|
@"type": self.typeSwitch
|
|
},
|
|
@{@"key": @"press_duration",
|
|
@"hasDetail": @YES,
|
|
@"icon": @"cursorarrow.click.badge.clock",
|
|
@"type": self.typeSlider,
|
|
@"min": @(100),
|
|
@"max": @(1000),
|
|
},
|
|
@{@"key": @"button_scale",
|
|
@"hasDetail": @YES,
|
|
@"icon": @"aspectratio",
|
|
@"type": self.typeSlider,
|
|
@"min": @(50), // 80?
|
|
@"max": @(500)
|
|
},
|
|
@{@"key": @"mouse_scale",
|
|
@"hasDetail": @YES,
|
|
@"icon": @"arrow.up.left.and.arrow.down.right.circle",
|
|
@"type": self.typeSlider,
|
|
@"min": @(25),
|
|
@"max": @(300)
|
|
},
|
|
@{@"key": @"mouse_speed",
|
|
@"hasDetail": @YES,
|
|
@"icon": @"cursorarrow.motionlines",
|
|
@"type": self.typeSlider,
|
|
@"min": @(25),
|
|
@"max": @(300)
|
|
},
|
|
@{@"key": @"virtmouse_enable",
|
|
@"hasDetail": @YES,
|
|
@"icon": @"cursorarrow.rays",
|
|
@"type": self.typeSwitch
|
|
},
|
|
@{@"key": @"gyroscope_enable",
|
|
@"hasDetail": @YES,
|
|
@"icon": @"gyroscope",
|
|
@"type": self.typeSwitch,
|
|
@"enableCondition": ^BOOL(){
|
|
return realUIIdiom != UIUserInterfaceIdiomTV;
|
|
}
|
|
},
|
|
@{@"key": @"gyroscope_invert_x_axis",
|
|
@"hasDetail": @YES,
|
|
@"icon": @"arrow.left.and.right",
|
|
@"type": self.typeSwitch,
|
|
@"enableCondition": ^BOOL(){
|
|
return realUIIdiom != UIUserInterfaceIdiomTV;
|
|
}
|
|
},
|
|
@{@"key": @"gyroscope_sensitivity",
|
|
@"hasDetail": @YES,
|
|
@"icon": @"move.3d",
|
|
@"type": self.typeSlider,
|
|
@"min": @(50),
|
|
@"max": @(300),
|
|
@"enableCondition": ^BOOL(){
|
|
return realUIIdiom != UIUserInterfaceIdiomTV;
|
|
}
|
|
}
|
|
], @[
|
|
// Java tweaks
|
|
@{@"icon": @"sparkles"},
|
|
@{@"key": @"java_home", // Use Java 17 for Minecraft < 1.17
|
|
@"hasDetail": @YES,
|
|
@"icon": @"cube",
|
|
@"type": self.typeSwitch,
|
|
@"enableCondition": ^BOOL(){
|
|
return (![getPreference(@"slimmed") boolValue]) && whenNotInGame();
|
|
},
|
|
// false: 8, true: 17
|
|
@"customSwitchValue": @[@"java-8-openjdk", @"java-17-openjdk"]
|
|
},
|
|
@{@"key": @"java_args",
|
|
@"hasDetail": @YES,
|
|
@"icon": @"slider.vertical.3",
|
|
@"type": self.typeTextField,
|
|
@"enableCondition": whenNotInGame
|
|
},
|
|
@{@"key": @"env_variables",
|
|
@"hasDetail": @YES,
|
|
@"icon": @"terminal",
|
|
@"type": self.typeTextField,
|
|
@"enableCondition": whenNotInGame
|
|
},
|
|
@{@"key": @"auto_ram",
|
|
@"hasDetail": @YES,
|
|
@"icon": @"slider.horizontal.3",
|
|
@"type": self.typeSwitch,
|
|
@"enableCondition": whenNotInGame,
|
|
@"warnCondition": ^BOOL(){
|
|
return getenv("POJAV_DETECTEDJB") == NULL;
|
|
},
|
|
@"warnKey": @"auto_ram_warn",
|
|
@"requestReload": @YES
|
|
},
|
|
@{@"key": @"allocated_memory",
|
|
@"hasDetail": @YES,
|
|
@"icon": @"memorychip",
|
|
@"type": self.typeSlider,
|
|
@"min": @(250),
|
|
@"max": @((NSProcessInfo.processInfo.physicalMemory / 1048576) * 0.85),
|
|
@"enableCondition": ^BOOL(){
|
|
return ![getPreference(@"auto_ram") boolValue] && whenNotInGame();
|
|
},
|
|
@"warnCondition": ^BOOL(DBNumberedSlider *view){
|
|
return view.value >= NSProcessInfo.processInfo.physicalMemory / 1048576 * 0.37;
|
|
},
|
|
@"warnKey": @"mem_warn"
|
|
}
|
|
], @[
|
|
// Debug settings - only recommended for developer use
|
|
@{@"icon": @"ladybug"},
|
|
@{@"key": @"debug_skip_wait_jit",
|
|
@"hasDetail": @YES,
|
|
@"icon": @"forward",
|
|
@"type": self.typeSwitch,
|
|
@"enableCondition": whenNotInGame
|
|
},
|
|
@{@"key": @"debug_hide_home_indicator",
|
|
@"hasDetail": @YES,
|
|
@"icon": @"iphone.and.arrow.forward",
|
|
@"type": self.typeSwitch,
|
|
@"enableCondition": ^BOOL(){
|
|
return
|
|
self.splitViewController.view.safeAreaInsets.bottom > 0 ||
|
|
self.view.safeAreaInsets.bottom > 0;
|
|
}
|
|
},
|
|
@{@"key": @"debug_ipad_ui",
|
|
@"hasDetail": @YES,
|
|
@"icon": @"ipad",
|
|
@"type": self.typeSwitch,
|
|
@"enableCondition": whenNotInGame
|
|
},
|
|
@{@"key": @"debug_auto_correction",
|
|
@"hasDetail": @YES,
|
|
@"icon": @"textformat.abc.dottedunderline",
|
|
@"type": self.typeSwitch
|
|
},
|
|
@{@"key": @"debug_show_layout_bounds",
|
|
@"hasDetail": @YES,
|
|
@"icon": @"square.dashed",
|
|
@"type": self.typeSwitch,
|
|
@"enableCondition": whenNotInGame,
|
|
@"requestReload": @YES,
|
|
@"action": ^(BOOL enabled){
|
|
debugBoundsEnabled = enabled;
|
|
}
|
|
},
|
|
@{@"key": @"debug_show_layout_overlap",
|
|
@"hasDetail": @YES,
|
|
@"icon": @"square.on.square",
|
|
@"type": self.typeSwitch,
|
|
@"enableCondition": ^BOOL(){
|
|
return [getPreference(@"debug_show_layout_bounds") boolValue] && whenNotInGame();
|
|
}
|
|
}
|
|
]
|
|
];
|
|
|
|
self.title = localize(@"Settings", nil);
|
|
if (self.navigationController) {
|
|
self.navigationItem.rightBarButtonItems = @[[sidebarViewController drawAccountButton], [self drawHelpButton]];
|
|
}
|
|
}
|
|
|
|
- (UIBarButtonItem *)drawHelpButton {
|
|
self.helpBtn = [[UIBarButtonItem alloc] initWithImage:[UIImage systemImageNamed:@"questionmark.circle"] style:UIBarButtonItemStyleDone target:self action:@selector(toggleDetailVisibility)];
|
|
|
|
return self.helpBtn;
|
|
}
|
|
|
|
- (void)viewWillAppear:(BOOL)animated {
|
|
[super viewWillAppear:animated];
|
|
|
|
// Scan for child pane cells and reload them
|
|
// FIXME: any cheaper operations?
|
|
NSMutableArray *indexPaths = [[NSMutableArray alloc] init];
|
|
for (int section = 0; section < self.prefContents.count; section++) {
|
|
if (!self.prefSectionsVisibility[section].boolValue) {
|
|
continue;
|
|
}
|
|
for (int row = 0; row < self.prefContents[section].count; row++) {
|
|
if (self.prefContents[section][row][@"type"] == self.typeChildPane) {
|
|
[indexPaths addObject:[NSIndexPath indexPathForRow:row inSection:section]];
|
|
}
|
|
}
|
|
}
|
|
[self.tableView reloadRowsAtIndexPaths:indexPaths withRowAnimation:UITableViewRowAnimationNone];
|
|
}
|
|
|
|
- (void)viewWillDisappear:(BOOL)animated {
|
|
[super viewWillDisappear:animated];
|
|
if (self.navigationController == nil) {
|
|
[self.presentingViewController performSelector:@selector(updatePreferenceChanges)];
|
|
}
|
|
}
|
|
|
|
- (void)toggleDetailVisibility {
|
|
self.prefDetailVisible = !self.prefDetailVisible;
|
|
[self.tableView reloadData];
|
|
}
|
|
|
|
#pragma mark UITableView
|
|
|
|
- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView {
|
|
return self.prefSections.count;
|
|
}
|
|
|
|
- (NSString *)tableView:(UITableView *)tableView titleForFooterInSection:(NSInteger)section {
|
|
if (section == 0) { // Add to general section
|
|
return [NSString stringWithFormat:@"PojavLauncher %@-%s (%s/%s)\niOS %s on %s (%s)\nPID: %d",
|
|
NSBundle.mainBundle.infoDictionary[@"CFBundleShortVersionString"],
|
|
CONFIG_TYPE, CONFIG_BRANCH, CONFIG_COMMIT,
|
|
getenv("POJAV_DETECTEDSW"), getenv("POJAV_DETECTEDHW"), getenv("POJAV_DETECTEDINST"), getpid()];
|
|
}
|
|
|
|
NSString *footer = NSLocalizedStringWithDefaultValue(([NSString stringWithFormat:@"preference.section.footer.%@", self.prefSections[section]]), @"Localizable", NSBundle.mainBundle, @" ", nil);
|
|
if ([footer isEqualToString:@" "]) {
|
|
return nil;
|
|
}
|
|
return footer;
|
|
}
|
|
|
|
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section {
|
|
if (self.prefSectionsVisibility[section].boolValue) {
|
|
return self.prefContents[section].count;
|
|
}
|
|
return 1;
|
|
}
|
|
|
|
- (UITableViewCell *)tableView:(nonnull UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
|
|
NSDictionary *item = self.prefContents[indexPath.section][indexPath.row];
|
|
|
|
NSString *cellID;
|
|
UITableViewCellStyle cellStyle;
|
|
if (item[@"type"] == self.typeChildPane || item[@"type"] == self.typePickField) {
|
|
cellID = @"cellValue1";
|
|
cellStyle = UITableViewCellStyleValue1;
|
|
} else {
|
|
cellID = @"cellSubtitle";
|
|
cellStyle = UITableViewCellStyleSubtitle;
|
|
}
|
|
|
|
UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:cellID];
|
|
if (cell == nil) {
|
|
cell = [[UITableViewCell alloc] initWithStyle:cellStyle reuseIdentifier:cellID];
|
|
cell.detailTextLabel.numberOfLines = 0;
|
|
cell.detailTextLabel.lineBreakMode = NSLineBreakByWordWrapping;
|
|
}
|
|
// Reset cell properties, as it could be reused
|
|
cell.accessoryType = UITableViewCellAccessoryNone;
|
|
cell.accessoryView = nil;
|
|
cell.selectionStyle = UITableViewCellSelectionStyleNone;
|
|
cell.textLabel.textColor = nil;
|
|
cell.detailTextLabel.text = nil;
|
|
|
|
NSString *key = item[@"key"];
|
|
if (indexPath.row == 0) {
|
|
key = self.prefSections[indexPath.section];
|
|
cell.selectionStyle = UITableViewCellSelectionStyleDefault;
|
|
cell.textLabel.text = localize(([NSString stringWithFormat:@"preference.section.%@", key]), nil);
|
|
} else {
|
|
CreateView createView = item[@"type"];
|
|
createView(cell, key, item);
|
|
if (cell.accessoryView) {
|
|
objc_setAssociatedObject(cell.accessoryView, @"key", key, OBJC_ASSOCIATION_ASSIGN);
|
|
objc_setAssociatedObject(cell.accessoryView, @"item", item, OBJC_ASSOCIATION_ASSIGN);
|
|
}
|
|
cell.textLabel.text = localize(([NSString stringWithFormat:@"preference.title.%@", key]), nil);
|
|
}
|
|
|
|
// Set general properties
|
|
BOOL destructive = [item[@"destructive"] boolValue];
|
|
cell.imageView.tintColor = destructive ? UIColor.systemRedColor : nil;
|
|
cell.imageView.image = [UIImage systemImageNamed:item[@"icon"]];
|
|
|
|
if (cellStyle != UITableViewCellStyleValue1) {
|
|
cell.detailTextLabel.text = nil;
|
|
if ([item[@"hasDetail"] boolValue] && self.prefDetailVisible) {
|
|
cell.detailTextLabel.text = localize(([NSString stringWithFormat:@"preference.detail.%@", key]), nil);
|
|
}
|
|
}
|
|
|
|
// Check if one has enable condition and call if it does
|
|
BOOL(^checkEnable)(void) = item[@"enableCondition"];
|
|
cell.userInteractionEnabled = !checkEnable || checkEnable();
|
|
cell.textLabel.enabled = cell.detailTextLabel.enabled = cell.userInteractionEnabled;
|
|
[(id)cell.accessoryView setEnabled:cell.userInteractionEnabled];
|
|
|
|
return cell;
|
|
}
|
|
|
|
#pragma mark initViewCreation, showAlert, checkWarn
|
|
|
|
- (void)initViewCreation {
|
|
__weak LauncherPreferencesViewController *weakSelf = self;
|
|
|
|
self.typeButton = ^void(UITableViewCell *cell, NSString *key, NSDictionary *item) {
|
|
BOOL destructive = [item[@"destructive"] boolValue];
|
|
cell.selectionStyle = UITableViewCellSelectionStyleGray;
|
|
cell.textLabel.textColor = destructive ? UIColor.systemRedColor : weakSelf.view.tintColor;
|
|
};
|
|
|
|
self.typeChildPane = ^void(UITableViewCell *cell, NSString *key, NSDictionary *item) {
|
|
cell.accessoryType = UITableViewCellAccessoryDisclosureIndicator;
|
|
cell.selectionStyle = UITableViewCellSelectionStyleGray;
|
|
cell.detailTextLabel.text = getPreference(key);
|
|
};
|
|
|
|
self.typeTextField = ^void(UITableViewCell *cell, NSString *key, NSDictionary *item) {
|
|
UITextField *view = [[UITextField 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 = weakSelf;
|
|
view.placeholder = localize(([NSString stringWithFormat:@"preference.placeholder.%@", key]), nil);
|
|
view.returnKeyType = UIReturnKeyDone;
|
|
view.text = getPreference(key);
|
|
view.textAlignment = NSTextAlignmentRight;
|
|
view.adjustsFontSizeToFitWidth = YES;
|
|
cell.accessoryView = view;
|
|
};
|
|
|
|
self.typePickField = ^void(UITableViewCell *cell, NSString *key, NSDictionary *item) {
|
|
cell.accessoryType = UITableViewCellAccessoryDisclosureIndicator;
|
|
cell.selectionStyle = UITableViewCellSelectionStyleGray;
|
|
cell.detailTextLabel.text = getPreference(key);
|
|
};
|
|
|
|
self.typeSlider = ^void(UITableViewCell *cell, NSString *key, NSDictionary *item) {
|
|
DBNumberedSlider *view = [[DBNumberedSlider alloc] initWithFrame:CGRectMake(0, 0, cell.bounds.size.width / 2.1, cell.bounds.size.height)];
|
|
[view addTarget:weakSelf action:@selector(sliderMoved:) forControlEvents:UIControlEventValueChanged];
|
|
view.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleLeftMargin;
|
|
view.minimumValue = [item[@"min"] intValue];
|
|
view.maximumValue = [item[@"max"] intValue];
|
|
view.continuous = YES;
|
|
view.value = [getPreference(key) intValue];
|
|
cell.accessoryView = view;
|
|
};
|
|
|
|
self.typeSwitch = ^void(UITableViewCell *cell, NSString *key, NSDictionary *item) {
|
|
UISwitch *view = [[UISwitch alloc] init];
|
|
NSArray *customSwitchValue = item[@"customSwitchValue"];
|
|
if (customSwitchValue == nil) {
|
|
[view setOn:[getPreference(key) boolValue] animated:NO];
|
|
} else {
|
|
[view setOn:[getPreference(key) isEqualToString:customSwitchValue[1]] animated:NO];
|
|
}
|
|
[view addTarget:weakSelf action:@selector(switchChanged:) forControlEvents:UIControlEventValueChanged];
|
|
cell.accessoryView = view;
|
|
};
|
|
}
|
|
|
|
- (void)showAlertOnView:(UIView *)view title:(NSString *)title message:(NSString *)message {
|
|
UIAlertController *alert = [UIAlertController alertControllerWithTitle:title message:message preferredStyle:UIAlertControllerStyleActionSheet];
|
|
alert.popoverPresentationController.sourceView = view;
|
|
alert.popoverPresentationController.sourceRect = view.bounds;
|
|
UIAlertAction *ok = [UIAlertAction actionWithTitle:localize(@"OK", nil) style:UIAlertActionStyleCancel handler:nil];
|
|
[alert addAction:ok];
|
|
[self presentViewController:alert animated:YES completion:nil];
|
|
}
|
|
|
|
- (void)checkWarn:(UIView *)view {
|
|
NSDictionary *item = objc_getAssociatedObject(view, @"item");
|
|
NSString *key = item[@"key"];
|
|
|
|
BOOL(^isWarnable)(UIView *) = item[@"warnCondition"];
|
|
NSString *warnKey = item[@"warnKey"];
|
|
// Display warning if: warn condition is met and either one of these:
|
|
// - does not have warnKey, always warn
|
|
// - has warnKey and its value is YES, warn once and set it to NO
|
|
if (isWarnable && isWarnable(view) && (!warnKey || [getPreference(warnKey) boolValue])) {
|
|
if (warnKey) {
|
|
setPreference(warnKey, @NO);
|
|
}
|
|
|
|
NSString *message = localize(([NSString stringWithFormat:@"preference.warn.%@", key]), nil);
|
|
[self showAlertOnView:view title:localize(@"Warning", nil) message:message];
|
|
}
|
|
}
|
|
|
|
#pragma mark Control event handlers
|
|
|
|
- (void)sliderMoved:(DBNumberedSlider *)sender {
|
|
[self checkWarn:sender];
|
|
NSString *key = objc_getAssociatedObject(sender, @"key");
|
|
|
|
sender.value = (int)sender.value;
|
|
setPreference(key, @(sender.value));
|
|
}
|
|
|
|
- (void)switchChanged:(UISwitch *)sender {
|
|
[self checkWarn:sender];
|
|
NSDictionary *item = objc_getAssociatedObject(sender, @"item");
|
|
NSString *key = item[@"key"];
|
|
|
|
// Special switches may define custom value instead of NO/YES
|
|
NSArray *customSwitchValue = item[@"customSwitchValue"];
|
|
if (customSwitchValue != nil) {
|
|
setPreference(key, customSwitchValue[sender.isOn]);
|
|
} else {
|
|
setPreference(key, @(sender.isOn));
|
|
}
|
|
|
|
void(^invokeAction)(BOOL) = item[@"action"];
|
|
if (invokeAction) {
|
|
invokeAction(sender.isOn);
|
|
}
|
|
|
|
// Some settings may affect the availability of other settings
|
|
// In this case, a switch may request to reload to apply user interaction change
|
|
if ([item[@"requestReload"] boolValue]) {
|
|
// TODO: only reload needed rows
|
|
[self.tableView reloadData];
|
|
}
|
|
}
|
|
|
|
- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath {
|
|
[tableView deselectRowAtIndexPath:indexPath animated:NO];
|
|
if (indexPath.row == 0) {
|
|
self.prefSectionsVisibility[indexPath.section] = @(![self.prefSectionsVisibility[indexPath.section] boolValue]);
|
|
[tableView reloadSections:[NSIndexSet indexSetWithIndex:indexPath.section] withRowAnimation:UITableViewRowAnimationFade];
|
|
return;
|
|
}
|
|
|
|
NSDictionary *item = self.prefContents[indexPath.section][indexPath.row];
|
|
|
|
if (item[@"type"] == self.typeButton) {
|
|
[self tableView:tableView invokeActionWithPromptAtIndexPath:indexPath];
|
|
return;
|
|
} else if (item[@"type"] == self.typeChildPane) {
|
|
[self tableView:tableView openChildPaneAtIndexPath:indexPath];
|
|
return;
|
|
} else if (item[@"type"] == self.typePickField) {
|
|
[self tableView:tableView openPickerAtIndexPath:indexPath];
|
|
return;
|
|
} else if (realUIIdiom != UIUserInterfaceIdiomTV) {
|
|
return;
|
|
}
|
|
|
|
// userInterfaceIdiom = tvOS
|
|
UITableViewCell *cell = [tableView cellForRowAtIndexPath:indexPath];
|
|
if (item[@"type"] == self.typeSwitch) {
|
|
UISwitch *view = (id)cell.accessoryView;
|
|
view.on = !view.isOn;
|
|
[view sendActionsForControlEvents:UIControlEventValueChanged];
|
|
}
|
|
}
|
|
|
|
#pragma mark External UITableView functions
|
|
|
|
- (void)tableView:(UITableView *)tableView openChildPaneAtIndexPath:(NSIndexPath *)indexPath {
|
|
NSDictionary *item = self.prefContents[indexPath.section][indexPath.row];
|
|
UIViewController *vc = [item[@"class"] new];
|
|
if ([item[@"canDismissWithSwipe"] boolValue]) {
|
|
[self.navigationController pushViewController:vc animated:YES];
|
|
} else {
|
|
UINavigationController *nav = [[UINavigationController alloc] initWithRootViewController:vc];
|
|
nav.navigationBar.prefersLargeTitles = YES;
|
|
nav.modalInPresentation = YES;
|
|
[self.navigationController presentViewController:nav animated:YES completion:nil];
|
|
}
|
|
}
|
|
|
|
- (UIContextMenuConfiguration *)contextMenuInteraction:(UIContextMenuInteraction *)interaction configurationForMenuAtLocation:(CGPoint)location
|
|
{
|
|
return [UIContextMenuConfiguration configurationWithIdentifier:nil previewProvider:nil actionProvider:^UIMenu * _Nullable(NSArray<UIMenuElement *> * _Nonnull suggestedActions) {
|
|
return self.currentMenu;
|
|
}];
|
|
}
|
|
|
|
- (_UIContextMenuStyle *)_contextMenuInteraction:(UIContextMenuInteraction *)interaction styleForMenuWithConfiguration:(UIContextMenuConfiguration *)configuration
|
|
{
|
|
_UIContextMenuStyle *style = [_UIContextMenuStyle defaultStyle];
|
|
style.preferredLayout = 3; // _UIContextMenuLayoutCompactMenu
|
|
return style;
|
|
}
|
|
|
|
- (void)tableView:(UITableView *)tableView openPickerAtIndexPath:(NSIndexPath *)indexPath {
|
|
UITableViewCell *cell = [tableView cellForRowAtIndexPath:indexPath];
|
|
NSDictionary *item = self.prefContents[indexPath.section][indexPath.row];
|
|
|
|
NSString *message = nil;
|
|
if ([item[@"hasDetail"] boolValue]) {
|
|
message = localize(([NSString stringWithFormat:@"preference.detail.%@", item[@"key"]]), nil);
|
|
}
|
|
|
|
NSArray *pickKeys = item[@"pickKeys"];
|
|
NSArray *pickList = item[@"pickList"];
|
|
NSMutableArray *menuItems = [[NSMutableArray alloc] init];
|
|
for (int i = 0; i < pickList.count; i++) {
|
|
[menuItems addObject:[UIAction
|
|
actionWithTitle:pickList[i]
|
|
image:nil
|
|
identifier:nil
|
|
handler:^(UIAction *action) {
|
|
cell.detailTextLabel.text = pickKeys[i];
|
|
setPreference(item[@"key"], pickKeys[i]);
|
|
void(^invokeAction)(NSString *) = item[@"action"];
|
|
if (invokeAction) {
|
|
invokeAction(pickKeys[i]);
|
|
}
|
|
}]];
|
|
}
|
|
|
|
self.currentMenu = [UIMenu menuWithTitle:message children:menuItems];
|
|
UIContextMenuInteraction *interaction = [[UIContextMenuInteraction alloc] initWithDelegate:self];
|
|
cell.detailTextLabel.interactions = [NSArray new];
|
|
[cell.detailTextLabel addInteraction:interaction];
|
|
[interaction _presentMenuAtLocation:CGPointZero];
|
|
}
|
|
|
|
- (void)tableView:(UITableView *)tableView invokeActionWithPromptAtIndexPath:(NSIndexPath *)indexPath {
|
|
UITableViewCell *view = [self.tableView cellForRowAtIndexPath:indexPath];
|
|
NSDictionary *item = self.prefContents[indexPath.section][indexPath.row];
|
|
NSString *key = item[@"key"];
|
|
|
|
if ([item[@"showConfirmPrompt"] boolValue]) {
|
|
BOOL destructive = [item[@"destructive"] boolValue];
|
|
NSString *title = localize(@"preference.title.confirm", nil);
|
|
NSString *message = localize(([NSString stringWithFormat:@"preference.title.confirm.%@", key]), nil);
|
|
UIAlertController *confirmAlert = [UIAlertController alertControllerWithTitle:title message:message preferredStyle:UIAlertControllerStyleActionSheet];
|
|
confirmAlert.popoverPresentationController.sourceView = view;
|
|
confirmAlert.popoverPresentationController.sourceRect = view.bounds;
|
|
UIAlertAction *ok = [UIAlertAction actionWithTitle:localize(@"OK", nil) style:destructive?UIAlertActionStyleDestructive:UIAlertActionStyleDefault handler:^(UIAlertAction * _Nonnull action) {
|
|
[self tableView:tableView invokeActionAtIndexPath:indexPath];
|
|
}];
|
|
UIAlertAction *cancel = [UIAlertAction actionWithTitle:localize(@"Cancel", nil) style:UIAlertActionStyleCancel handler:nil];
|
|
[confirmAlert addAction:cancel];
|
|
[confirmAlert addAction:ok];
|
|
[self presentViewController:confirmAlert animated:YES completion:nil];
|
|
} else {
|
|
[self tableView:tableView invokeActionAtIndexPath:indexPath];
|
|
}
|
|
}
|
|
|
|
- (void)tableView:(UITableView *)tableView invokeActionAtIndexPath:(NSIndexPath *)indexPath {
|
|
NSDictionary *item = self.prefContents[indexPath.section][indexPath.row];
|
|
NSString *key = item[@"key"];
|
|
|
|
void(^invokeAction)(void) = item[@"action"];
|
|
if (invokeAction) {
|
|
invokeAction();
|
|
}
|
|
|
|
UIView *view = [self.tableView cellForRowAtIndexPath:indexPath];
|
|
NSString *title = localize(([NSString stringWithFormat:@"preference.title.done.%@", key]), nil);
|
|
//NSString *message = localize(([NSString stringWithFormat:@"preference.message.done.%@", key]), nil);
|
|
[self showAlertOnView:view title:title message:nil];
|
|
}
|
|
|
|
#pragma mark UITextField
|
|
|
|
- (void)textFieldDidEndEditing:(UITextField *)sender {
|
|
[self checkWarn:sender];
|
|
NSString *key = objc_getAssociatedObject(sender, @"key");
|
|
|
|
setPreference(key, sender.text);
|
|
}
|
|
|
|
@end
|