diff --git a/Natives/CMakeLists.txt b/Natives/CMakeLists.txt index 6cdc08bd..36c6428c 100644 --- a/Natives/CMakeLists.txt +++ b/Natives/CMakeLists.txt @@ -118,6 +118,7 @@ add_executable(PojavLauncher LauncherPreferences.m LauncherPreferencesViewController.m LauncherPreferencesViewController2.m + LauncherPrefGameDirViewController.m LauncherSplitViewController.m LauncherViewController.m LoginViewController.m diff --git a/Natives/LauncherPrefGameDirViewController.h b/Natives/LauncherPrefGameDirViewController.h new file mode 100644 index 00000000..9ef15fc1 --- /dev/null +++ b/Natives/LauncherPrefGameDirViewController.h @@ -0,0 +1,6 @@ +#import + +@interface LauncherPrefGameDirViewController : UITableViewController + +@end + diff --git a/Natives/LauncherPrefGameDirViewController.m b/Natives/LauncherPrefGameDirViewController.m new file mode 100644 index 00000000..7173ccd2 --- /dev/null +++ b/Natives/LauncherPrefGameDirViewController.m @@ -0,0 +1,227 @@ +#import "LauncherPreferences.h" +#import "LauncherPrefGameDirViewController.h" +#import "TOInsetGroupedTableView.h" +#import "ios_uikit_bridge.h" + +@interface LauncherPrefGameDirViewController () +@property NSMutableArray *array; +@end + +@implementation LauncherPrefGameDirViewController + +- (void)viewDidLoad +{ + [super viewDidLoad]; + [self setTitle:NSLocalizedString(@"preference.title.game_directory", nil)]; + + self.array = [[NSMutableArray alloc] init]; + [self.array addObject:@"default"]; + + self.tableView = [[TOInsetGroupedTableView alloc] init]; + self.tableView.keyboardDismissMode = UIScrollViewKeyboardDismissModeInteractive; + self.tableView.sectionFooterHeight = 50; + + NSString *path = [NSString stringWithFormat:@"%s/instances", getenv("POJAV_HOME")]; + + NSFileManager *fm = NSFileManager.defaultManager; + NSArray *files = [fm contentsOfDirectoryAtPath:path error:nil]; + BOOL isDir; + for (NSString *file in files) { + [fm fileExistsAtPath:path isDirectory:(&isDir)]; + if (isDir && ![file isEqualToString:@"default"]) { + [self.array addObject:file]; + } + } +} + +- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section +{ + return self.array.count; +} + +- (UITableViewCell *)tableView:(nonnull UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath +{ + UITextField *view; + UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:@"cell"]; + if (cell == nil) { + cell = [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:@"cell"]; + + if (indexPath.row == 0) { + cell.textLabel.text = self.array[indexPath.row]; + } else { + view = [[UITextField alloc] initWithFrame:CGRectMake(20, 10, cell.bounds.size.width/2, cell.bounds.size.height-20)]; + [view addTarget:view action:@selector(resignFirstResponder) forControlEvents:UIControlEventEditingDidEndOnExit]; + view.autocorrectionType = UITextAutocorrectionTypeNo; + view.autocapitalizationType = UITextAutocapitalizationTypeNone; + view.autoresizingMask = UIViewAutoresizingFlexibleWidth; + view.delegate = self; + view.returnKeyType = UIReturnKeyDone; + [cell.contentView addSubview:view]; + } + } + if (indexPath.row > 0) { + view = cell.contentView.subviews.lastObject; + view.placeholder = self.array[indexPath.row]; + view.text = self.array[indexPath.row]; + } + + if ([getPreference(@"game_directory") isEqualToString:self.array[indexPath.row]]) { + cell.accessoryType = UITableViewCellAccessoryCheckmark; + } else { + cell.accessoryType = UITableViewCellAccessoryNone; + } + + // TODO: display size in the detail label + return cell; +} + +- (UIView *)tableView:(UITableView *)tableView +viewForFooterInSection:(NSInteger)section +{ + UITextField *view = [[UITextField alloc] init]; + [view addTarget:view action:@selector(resignFirstResponder) forControlEvents:UIControlEventEditingDidEndOnExit]; + view.autocorrectionType = UITextAutocorrectionTypeNo; + view.autocapitalizationType = UITextAutocapitalizationTypeNone; + view.delegate = self; + view.placeholder = @"add game directory"; + view.returnKeyType = UIReturnKeyDone; + return view; +} + +- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath { + setPreference(@"game_directory", self.array[indexPath.row]); + [tableView deselectRowAtIndexPath:indexPath animated:NO]; + for (int i = 0; i < self.array.count; i++) { + UITableViewCell *cell = [self.tableView cellForRowAtIndexPath:[NSIndexPath indexPathForRow:i inSection:0]]; + if (i == indexPath.row) { + cell.accessoryType = UITableViewCellAccessoryCheckmark; + } else { + cell.accessoryType = UITableViewCellAccessoryNone; + } + } +} + +- (UIContextMenuConfiguration *)tableView:(UITableView *)tableView contextMenuConfigurationForRowAtIndexPath:(NSIndexPath *)indexPath point:(CGPoint)point API_AVAILABLE(ios(13.0)) +{ + // You can't delete or rename the default instance, though there will be a reset action (TODO) + if (indexPath.row == 0) { + return nil; + } + + if (@available(iOS 13.0, *)) { + UIAction *delete = [UIAction actionWithTitle:@"Delete" image:[UIImage systemImageNamed:@"trash"] identifier:@"deleteAction" handler:^(__kindof UIAction * _Nonnull action) { + [self actionDeleteAtIndexPath:indexPath]; + }]; + delete.attributes = UIMenuElementAttributesDestructive; + + UIAction *rename = [UIAction actionWithTitle:@"Rename" image:[UIImage systemImageNamed:@"pencil"] identifier:@"editAction" handler:^(__kindof UIAction * _Nonnull action) { + UITableViewCell *view = [self.tableView cellForRowAtIndexPath:indexPath]; + [view.contentView.subviews.lastObject becomeFirstResponder]; + }]; + + return [UIContextMenuConfiguration configurationWithIdentifier:nil previewProvider:nil actionProvider:^UIMenu * _Nullable(NSArray * _Nonnull suggestedActions) { + return [UIMenu menuWithTitle:self.array[indexPath.row] children:@[delete, rename]]; + }]; + } + return nil; +} + +- (void)tableView:(UITableView *)tableView commitEditingStyle:(UITableViewCellEditingStyle)editingStyle forRowAtIndexPath:(NSIndexPath *)indexPath +{ + if (editingStyle == UITableViewCellEditingStyleDelete) { + [self actionDeleteAtIndexPath:indexPath]; + } +} + +- (UITableViewCellEditingStyle)tableView:(UITableView *)tableView editingStyleForRowAtIndexPath:(NSIndexPath *)indexPath +{ + if (indexPath.row == 0) { + // TODO: Reset action? + return UITableViewCellEditingStyleNone; + } else { + return UITableViewCellEditingStyleDelete; + } +} + +- (void)actionDeleteAtIndexPath:(NSIndexPath *)indexPath +{ + UITableViewCell *view = [self.tableView cellForRowAtIndexPath:indexPath]; + NSString *title = NSLocalizedString(@"preference.title.confirm", nil); + NSString *message = [NSString stringWithFormat:NSLocalizedString(@"preference.title.confirm.delete_game_directory", nil), self.array[indexPath.row]]; + UIAlertController *confirmAlert = [UIAlertController alertControllerWithTitle:title message:message preferredStyle:UIAlertControllerStyleActionSheet]; + confirmAlert.popoverPresentationController.sourceView = view; + confirmAlert.popoverPresentationController.sourceRect = view.bounds; + UIAlertAction *ok = [UIAlertAction actionWithTitle:NSLocalizedString(@"OK", nil) style:UIAlertActionStyleDestructive handler:^(UIAlertAction * _Nonnull action) { + NSString *directory = [NSString stringWithFormat:@"%s/instances/%@", getenv("POJAV_HOME"), self.array[indexPath.row]]; + NSError *error; + if([[NSFileManager defaultManager] removeItemAtPath:directory error:&error]) { + if ([getPreference(@"game_directory") isEqualToString:self.array[indexPath.row]]) { + setPreference(@"game_directory", self.array[0]); + [self.tableView cellForRowAtIndexPath:[NSIndexPath indexPathForRow:0 inSection:0]].accessoryType = UITableViewCellAccessoryCheckmark; + } + [self.array removeObjectAtIndex:indexPath.row]; + [self.tableView deleteRowsAtIndexPaths:@[indexPath] withRowAnimation:UITableViewRowAnimationFade]; + } else { + showDialog(self, NSLocalizedString(@"Error", nil), error.localizedDescription); + } + }]; + UIAlertAction *cancel = [UIAlertAction actionWithTitle:NSLocalizedString(@"Cancel", nil) style:UIAlertActionStyleCancel handler:nil]; + [confirmAlert addAction:cancel]; + [confirmAlert addAction:ok]; + [self presentViewController:confirmAlert animated:YES completion:nil]; +} + +#pragma mark UITextField + +- (void)textFieldDidEndEditing:(UITextField *)sender { + BOOL isFooterView = sender.superview == self.tableView; + if (!sender.hasText || [sender.text isEqualToString:sender.placeholder]) { + if (isFooterView) { + return; + } + sender.text = sender.placeholder; + return; + } + + NSError *error; + + NSString *dest = [NSString stringWithFormat:@"%s/instances/%@", getenv("POJAV_HOME"), sender.text]; + if (isFooterView) { + [[NSFileManager defaultManager] createDirectoryAtPath:dest withIntermediateDirectories:NO attributes:nil error:&error]; + } else { + NSString *source = [NSString stringWithFormat:@"%s/instances/%@", getenv("POJAV_HOME"), sender.placeholder]; + [NSFileManager.defaultManager moveItemAtPath:source toPath:dest error:&error]; + } + + if (error == nil) { + setPreference(@"game_directory", sender.text); + if (isFooterView) { + NSIndexPath *indexPath = [NSIndexPath indexPathForRow:self.array.count inSection:0]; + [self.array addObject:sender.text]; + [self.tableView beginUpdates]; + [self.tableView insertRowsAtIndexPaths:@[indexPath] withRowAnimation:UITableViewRowAnimationAutomatic]; + [self.tableView endUpdates]; + [self tableView:self.tableView didSelectRowAtIndexPath:indexPath]; + // Clear text + sender.text = @""; + } else { + int index = [self.array indexOfObject:sender.placeholder]; + self.array[index] = sender.placeholder = sender.text; + } + } else { + // Restore to the previous name if we encounter an error + if (!isFooterView) { + sender.text = sender.placeholder; + } + showDialog(self, NSLocalizedString(@"Error", nil), error.localizedDescription); + } +} + +- (BOOL)textField:(UITextField *)textField shouldChangeCharactersInRange:(NSRange)range replacementString:(NSString *)string { + CGRect frame = textField.frame; + frame.size.width = MAX(50, textField.intrinsicContentSize.width + 10); + textField.frame = frame; + return YES; +} + +@end diff --git a/Natives/LauncherPreferencesViewController2.m b/Natives/LauncherPreferencesViewController2.m index 4bd3ee3a..7bf41bdc 100644 --- a/Natives/LauncherPreferencesViewController2.m +++ b/Natives/LauncherPreferencesViewController2.m @@ -4,6 +4,7 @@ #import "DBNumberedSlider.h" #import "LauncherPreferences.h" #import "LauncherPreferencesViewController2.h" +#import "LauncherPrefGameDirViewController.h" #import "TOInsetGroupedTableView.h" #include "utils.h" @@ -14,7 +15,7 @@ typedef void(^CreateView)(UITableViewCell *, NSString *, NSDictionary *); @property NSArray* prefSections; @property NSArray*>* prefContents; -@property CreateView typeButton, typePickField, typeTextField, typeSlider, typeSwitch; +@property CreateView typeButton, typeChildPane, typePickField, typeTextField, typeSlider, typeSwitch; @end @implementation LauncherPreferencesViewController2 @@ -35,8 +36,8 @@ typedef void(^CreateView)(UITableViewCell *, NSString *, NSDictionary *); // General settings @{@"key": @"game_directory", @"icon": @"folder", - @"type": self.typePickField, - @"pickList": @[@"default", @"TODO"] + @"type": self.typeChildPane, + @"class": LauncherPrefGameDirViewController.class, }, @{@"key": @"home_symlink", @"icon": @"link", @@ -73,7 +74,18 @@ typedef void(^CreateView)(UITableViewCell *, NSString *, NSDictionary *); @{@"key": @"renderer", @"icon": @"cpu", @"type": self.typePickField, - @"pickList": @[@"gl4es", @"zink", @"wip"] + @"pickKeys": @[ + @"auto", + @ RENDERER_NAME_GL4ES, + @ RENDERER_NAME_MTL_ANGLE, + @ RENDERER_NAME_VK_ZINK + ], + @"pickList": @[ + NSLocalizedString(@"preference.title.renderer.auto", nil), + NSLocalizedString(@"preference.title.renderer.gl4es", nil), + NSLocalizedString(@"preference.title.renderer.tinygl4angle", nil), + NSLocalizedString(@"preference.title.renderer.zink", nil) + ] }, @{@"key": @"resolution", @"icon": @"viewfinder", @@ -171,10 +183,14 @@ typedef void(^CreateView)(UITableViewCell *, NSString *, NSDictionary *); - (UITableViewCell *)tableView:(nonnull UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath { UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:@"cell"]; if (cell == nil) { - cell = [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:@"cell"]; + cell = [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleValue1 reuseIdentifier:@"cell"]; } + // 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; NSDictionary *item = self.prefContents[indexPath.section][indexPath.row]; NSString *key = item[@"key"]; @@ -206,11 +222,16 @@ typedef void(^CreateView)(UITableViewCell *, NSString *, NSDictionary *); self.typeButton = ^void(UITableViewCell *cell, NSString *key, NSDictionary *item) { BOOL destructive = [item[@"destructive"] boolValue]; - cell.accessoryView = nil; cell.selectionStyle = UITableViewCellSelectionStyleGray; cell.textLabel.textColor = destructive ? UIColor.systemRedColor : UIColor.systemBlueColor; }; + self.typeChildPane = ^void(UITableViewCell *cell, NSString *key, NSDictionary *item) { + cell.accessoryType = UITableViewCellAccessoryDisclosureIndicator; + cell.selectionStyle = UITableViewCellSelectionStyleGray; + cell.detailTextLabel.text = getPreference(key); + }; + /* self.typePickField = ^void(UITableViewCell *cell, NSString *key, NSDictionary *item) { cell.accessoryView = [[UITextField alloc] initWithFrame:CGRectMake(0, 0, cell.bounds.size.width / 2, cell.bounds.size.height)]; @@ -238,9 +259,11 @@ typedef void(^CreateView)(UITableViewCell *, NSString *, NSDictionary *); UITextField *view = (UITextField *)cell.accessoryView; UIPickerView *picker = [[UIPickerView alloc] init]; objc_setAssociatedObject(picker, @"item", item, OBJC_ASSOCIATION_ASSIGN); + objc_setAssociatedObject(picker, @"view", cell.accessoryView, OBJC_ASSOCIATION_ASSIGN); picker.delegate = weakSelf; picker.dataSource = weakSelf; [picker reloadAllComponents]; + [picker selectRow:[item[@"pickKeys"] indexOfObject:getPreference(key)] inComponent:0 animated:NO]; UIToolbar *toolbar = [[UIToolbar alloc] initWithFrame:CGRectMake(0.0, 0.0, weakSelf.view.frame.size.width, 44.0)]; UIBarButtonItem *flexibleSpace = [[UIBarButtonItem alloc] initWithBarButtonSystemItem:UIBarButtonSystemItemFlexibleSpace target:nil action:nil]; UIBarButtonItem *doneButton = [[UIBarButtonItem alloc] initWithBarButtonSystemItem:UIBarButtonSystemItemDone target:view action:@selector(resignFirstResponder)]; @@ -296,7 +319,18 @@ typedef void(^CreateView)(UITableViewCell *, NSString *, NSDictionary *); #pragma mark - UIPickerView - (void)pickerView:(UIPickerView *)pickerView didSelectRow:(NSInteger)row inComponent:(NSInteger)component { - + NSDictionary *item = objc_getAssociatedObject(pickerView, @"item"); + UITextField *view = objc_getAssociatedObject(pickerView, @"view"); + + // If there is key, use it + if (item[@"pickKeys"] != nil) { + view.text = item[@"pickKeys"][row]; + } else { + view.text = item[@"pickList"][row]; + } + + // Save the preference + [self textFieldDidEndEditing:view]; } - (NSInteger)numberOfComponentsInPickerView:(UIPickerView *)thePickerView { @@ -347,12 +381,25 @@ typedef void(^CreateView)(UITableViewCell *, NSString *, NSDictionary *); - (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath { [tableView deselectRowAtIndexPath:indexPath animated:NO]; - UITableViewCell *view = [self.tableView cellForRowAtIndexPath:indexPath]; - if (view.selectionStyle == UITableViewCellSelectionStyleNone) { - // Ignore if selection style is none - return; - } + NSDictionary *item = self.prefContents[indexPath.section][indexPath.row]; + if (item[@"type"] == self.typeButton) { + [self tableView:tableView invokeActionWithPromptAtIndexPath:indexPath]; + } else if (item[@"type"] == self.typeChildPane) { + [self tableView:tableView openChildPaneAtIndexPath:indexPath]; + } +} + +#pragma mark External UITableView functions + +- (void)tableView:(UITableView *)tableView openChildPaneAtIndexPath:(NSIndexPath *)indexPath { + NSDictionary *item = self.prefContents[indexPath.section][indexPath.row]; + UIViewController *vc = [[item[@"class"] alloc] init]; + [self.navigationController pushViewController:vc animated:YES]; +} + +- (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"]; @@ -390,6 +437,8 @@ typedef void(^CreateView)(UITableViewCell *, NSString *, NSDictionary *); [self showAlertOnView:view title:title message:nil]; } +#pragma mark UITextField + - (void)textFieldDidEndEditing:(UITextField *)sender { [self checkWarn:sender]; NSString *key = objc_getAssociatedObject(sender, @"key"); diff --git a/Natives/en.lproj/Localizable.strings b/Natives/en.lproj/Localizable.strings index 62d7865b..85d468ef 100644 --- a/Natives/en.lproj/Localizable.strings +++ b/Natives/en.lproj/Localizable.strings @@ -73,6 +73,7 @@ "preference.title.confirm" = "Are you sure?"; "preference.title.game_directory" = "Minecraft game directory"; +"preference.title.confirm.delete_game_directory" = "The instance %@ will be deleted forever and cannot be restored."; "preference.title.home_symlink" = "Enable compatibility symlink"; "preference.title.check_sha" = "Enable asset verification when launching"; "preference.title.cosmetica" = "Enable Cosmetica integration"; @@ -85,6 +86,11 @@ "preference.title.done.reset_settings" = "All of the default settings have been restored."; "preference.title.renderer" = "Game renderer"; +"preference.title.renderer.auto" = "Auto: gl4es or tinygl4angle"; +"preference.title.renderer.gl4es" = "gl4es 1.1.4 - exports OpenGL 2.1"; +"preference.title.renderer.tinygl4angle" = "tinygl4angle (1.17+) - exports OpenGL 3.2 (Core Profile, limited)"; +"preference.title.renderer.zink" = "Zink (Mesa 21.0) - exports OpenGL 4.1"; + "preference.title.resolution" = "Game resolution"; "preference.title.disable_gesture" = "Disable gestures"; "preference.title.press_duration" = "Long-press duration"; diff --git a/Natives/utils.h b/Natives/utils.h index dd3e136a..43b77d28 100644 --- a/Natives/utils.h +++ b/Natives/utils.h @@ -30,7 +30,7 @@ #define RENDERER_NAME_GL4ES "libgl4es_114.dylib" #define RENDERER_NAME_MTL_ANGLE "libtinygl4angle.dylib" -#define RENDERER_NAME_VK_ZINK "libOSMesaOverride.dylib" +#define RENDERER_NAME_VK_ZINK "libOSMesa.8.dylib" #define SPECIALBTN_KEYBOARD -1 #define SPECIALBTN_TOGGLECTRL -2