/* RetroArch - A frontend for libretro. * Copyright (C) 2013-2014 - Jason Fetters * Copyright (C) 2011-2017 - Daniel De Matteis * Copyright (C) 2014-2015 - Jay McCarthy * * RetroArch is free software: you can redistribute it and/or modify it under the terms * of the GNU General Public License as published by the Free Software Found- * ation, either version 3 of the License, or (at your option) any later version. * * RetroArch is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR * PURPOSE. See the GNU General Public License for more details. * * You should have received a copy of the GNU General Public License along with RetroArch. * If not, see . */ #include #include #include #include #include #include #include #include "cocoa_common.h" #include "../../../input/input_config.h" #include "../../../input/input_keymaps.h" #include "../../../input/drivers/cocoa_input.h" #include "../../../configuration.h" #include "../../../runloop.h" #ifdef HAVE_MENU #include "../../../menu/widgets/menu_entry.h" #include "../../../menu/menu_navigation.h" #include "../../../menu/drivers/menu_generic.h" #endif // Menu Support static const void* const associated_delegate_key = &associated_delegate_key; typedef void (^RAActionSheetCallback)(UIActionSheet*, NSInteger); @interface RARunActionSheetDelegate : NSObject @property (nonatomic, copy) RAActionSheetCallback callbackBlock; @end @implementation RARunActionSheetDelegate - (id)initWithCallbackBlock:(RAActionSheetCallback)callback { if ((self = [super init])) _callbackBlock = callback; return self; } - (void)actionSheet:(UIActionSheet *)actionSheet willDismissWithButtonIndex:(NSInteger)buttonIndex { if (self.callbackBlock) self.callbackBlock(actionSheet, buttonIndex); } @end static void RunActionSheet(const char* title, const struct string_list* items, UIView* parent, RAActionSheetCallback callback) { size_t i; RARunActionSheetDelegate* delegate = [[RARunActionSheetDelegate alloc] initWithCallbackBlock:callback]; UIActionSheet* actionSheet = [UIActionSheet new]; actionSheet.title = BOXSTRING(title); actionSheet.delegate = delegate; for (i = 0; i < items->size; i ++) [actionSheet addButtonWithTitle:BOXSTRING(items->elems[i].data)]; actionSheet.cancelButtonIndex = [actionSheet addButtonWithTitle:BOXSTRING("Cancel")]; objc_setAssociatedObject(actionSheet, associated_delegate_key, delegate, OBJC_ASSOCIATION_RETAIN_NONATOMIC); [actionSheet showInView:parent]; } // Menu Entries @protocol RAMenuActioner - (void)menuSelect:(uint32_t) i; @end @interface RAMenuItemBase : NSObject @property (nonatomic) id main; @property (nonatomic) uint32_t i; @property (nonatomic, weak) UITableView* parentTable; - (void)initialize:(NSObject *) main idx:(uint32_t) i; - (UITableViewCell*)cellForTableView:(UITableView*)tableView; - (void)wasSelectedOnTableView:(UITableView*)tableView ofController:(UIViewController*)controller; @end @implementation RAMenuItemBase - (void)initialize:(NSObject*)main idx:(uint32_t) i { _main = main; _i = i; } - (UITableViewCell*)cellForTableView:(UITableView*)tableView { char buffer[PATH_MAX_LENGTH]; char label[PATH_MAX_LENGTH]; static NSString* const cell_id = @"text"; self.parentTable = tableView; UITableViewCell* result = [tableView dequeueReusableCellWithIdentifier:cell_id]; if (!result) result = [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleValue1 reuseIdentifier:cell_id]; menu_entry_get_path(self.i, label, sizeof(label)); menu_entry_get_value(self.i, NULL, buffer, sizeof(buffer)); result.textLabel.text = BOXSTRING(label); if (string_is_empty(label)) strlcpy(buffer, msg_hash_to_str(MENU_ENUM_LABEL_VALUE_NOT_AVAILABLE), sizeof(buffer)); result.detailTextLabel.text = BOXSTRING(buffer); return result; } - (void)wasSelectedOnTableView:(UITableView*)tableView ofController:(UIViewController*)controller { } @end @interface RAMenuItemBool : RAMenuItemBase @end @implementation RAMenuItemBool - (UITableViewCell*)cellForTableView:(UITableView*)tableView { char label[PATH_MAX_LENGTH]; static NSString* const cell_id = @"boolean_setting"; UITableViewCell* result = (UITableViewCell*)[tableView dequeueReusableCellWithIdentifier:cell_id]; if (!result) { result = [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleValue1 reuseIdentifier:cell_id]; result.selectionStyle = UITableViewCellSelectionStyleNone; result.accessoryView = [UISwitch new]; } menu_entry_get_path(self.i, label, sizeof(label)); result.textLabel.text = BOXSTRING(label); [(id)result.accessoryView removeTarget:nil action:NULL forControlEvents:UIControlEventValueChanged]; [(id)result.accessoryView addTarget:self action:@selector(handleBooleanSwitch:) forControlEvents:UIControlEventValueChanged]; [(id)result.accessoryView setOn:(menu_entry_get_bool_value(self.i))]; return result; } - (void)handleBooleanSwitch:(UISwitch*)swt { [self.main menuSelect: self.i]; } - (void)wasSelectedOnTableView:(UITableView*)tableView ofController:(UIViewController*)controller { } @end @interface RAMenuItemAction : RAMenuItemBase @end @implementation RAMenuItemAction - (void)wasSelectedOnTableView:(UITableView*)tableView ofController:(UIViewController*)controller { [self.main menuSelect: self.i]; } @end @interface RAMenuItemEnum : RAMenuItemBase @end @implementation RAMenuItemEnum - (void)wasSelectedOnTableView:(UITableView*)tableView ofController:(UIViewController*)controller { struct string_list* items; char label[PATH_MAX_LENGTH]; RAMenuItemEnum __weak* weakSelf = self; menu_entry_get_path(self.i, label, sizeof(label)); items = menu_entry_enum_values(self.i); RunActionSheet(label, items, self.parentTable, ^(UIActionSheet* actionSheet, NSInteger buttonIndex) { if (buttonIndex == actionSheet.cancelButtonIndex) return; menu_entry_enum_set_value_with_string (self.i, [[actionSheet buttonTitleAtIndex:buttonIndex] UTF8String]); [weakSelf.parentTable reloadData]; }); string_list_free(items); } @end @interface RAMenuItemBind : RAMenuItemBase @property (nonatomic) NSTimer* bindTimer; @property (nonatomic) UIAlertView* alert; @end @implementation RAMenuItemBind - (void)wasSelectedOnTableView:(UITableView *)tableView ofController:(UIViewController *)controller { char label[PATH_MAX_LENGTH]; menu_entry_get_path(self.i, label, sizeof(label)); self.alert = [[UIAlertView alloc] initWithTitle:BOXSTRING("RetroArch") message:BOXSTRING(label) delegate:self cancelButtonTitle:BOXSTRING("Cancel") otherButtonTitles:BOXSTRING("Clear Keyboard"), BOXSTRING("Clear Joystick"), BOXSTRING("Clear Axis"), nil]; [self.alert show]; [self.parentTable reloadData]; self.bindTimer = [NSTimer scheduledTimerWithTimeInterval:.1f target:self selector:@selector(checkBind:) userInfo:nil repeats:YES]; } - (void)finishWithClickedButton:(bool)clicked { if (!clicked) [self.alert dismissWithClickedButtonIndex:self.alert.cancelButtonIndex animated:YES]; self.alert = nil; [self.parentTable reloadData]; [self.bindTimer invalidate]; self.bindTimer = nil; } - (void)alertView:(UIAlertView*)alertView clickedButtonAtIndex:(NSInteger)buttonIndex { if (buttonIndex == alertView.firstOtherButtonIndex) menu_entry_bind_key_set(self.i, RETROK_UNKNOWN); else if(buttonIndex == alertView.firstOtherButtonIndex + 1) menu_entry_bind_joykey_set(self.i, NO_BTN); else if(buttonIndex == alertView.firstOtherButtonIndex + 2) menu_entry_bind_joyaxis_set(self.i, AXIS_NONE); [self finishWithClickedButton:true]; } - (void)checkBind:(NSTimer*)send { int32_t value = 0; int32_t idx = menu_entry_bind_index(self.i); if ((value = cocoa_input_find_any_key())) menu_entry_bind_key_set(self.i, input_keymaps_translate_keysym_to_rk(value)); else if ((value = cocoa_input_find_any_button(idx)) >= 0) menu_entry_bind_joykey_set(self.i, value); else if ((value = cocoa_input_find_any_axis(idx))) menu_entry_bind_joyaxis_set(self.i, (value > 0) ? AXIS_POS(value - 1) : AXIS_NEG(abs(value) - 1)); else return; [self finishWithClickedButton:false]; } @end @interface RAMenuItemPathDir : RAMenuItemBase @end @implementation RAMenuItemPathDir - (void)wasSelectedOnTableView:(UITableView*)tableView ofController:(UIViewController*)controller { } @end @interface RAMenuItemPath : RAMenuItemPathDir @end @implementation RAMenuItemPath - (void)wasSelectedOnTableView:(UITableView*)tableView ofController:(UIViewController*)controller { [self.main menuSelect: self.i]; } @end @interface RAMenuItemDir : RAMenuItemPathDir @end @implementation RAMenuItemDir - (void)wasSelectedOnTableView:(UITableView*)tableView ofController:(UIViewController*)controller { [self.main menuSelect: self.i]; } @end @interface RANumberFormatter : NSNumberFormatter - (void)setRangeFrom: (NSNumber*) min To: (NSNumber*) max; @end @implementation RANumberFormatter - (void)setRangeFrom: (NSNumber*) min To: (NSNumber*) max { [self setMinimum: min]; [self setMaximum: max]; } - (BOOL)isPartialStringValid:(NSString*)partialString newEditingString:(NSString**)newString errorDescription:(NSString**)error { unsigned i; bool hasDot = false; if (partialString.length) for (i = 0; i < partialString.length; i ++) { unichar ch = [partialString characterAtIndex:i]; if (i == 0 && (!self.minimum || self.minimum.intValue < 0) && ch == '-') continue; else if (self.allowsFloats && !hasDot && ch == '.') hasDot = true; else if (!isdigit(ch)) return NO; } return YES; } - (BOOL)textField:(UITextField *)textField shouldChangeCharactersInRange:(NSRange)range replacementString:(NSString *)string { NSString* text = (NSString*)[[textField text] stringByReplacingCharactersInRange:range withString:string]; return [self isPartialStringValid:text newEditingString:nil errorDescription:nil]; } @end @interface RAMenuItemGeneric : RAMenuItemBase @property (nonatomic) RANumberFormatter* formatter; @end @implementation RAMenuItemGeneric - (UITableViewCell*)cellForTableView:(UITableView*)tableView { UITableViewCell* result; result = [super cellForTableView: tableView]; [self attachDefaultingGestureTo:result]; return result; } - (void)wasSelectedOnTableView:(UITableView*)tableView ofController:(UIViewController*)controller { char buffer[PATH_MAX_LENGTH]; char label[PATH_MAX_LENGTH]; UIAlertView *alertView = NULL; UITextField *field = NULL; NSString *desc = NULL; menu_entry_get_path(self.i, label, sizeof(label)); desc = BOXSTRING(label); alertView = [[UIAlertView alloc] initWithTitle:BOXSTRING("Enter new value") message:desc delegate:self cancelButtonTitle:BOXSTRING("Cancel") otherButtonTitles:BOXSTRING("OK"), nil]; alertView.alertViewStyle = UIAlertViewStylePlainTextInput; field = [alertView textFieldAtIndex:0]; field.delegate = self.formatter; menu_entry_get_value(self.i, NULL, buffer, sizeof(buffer)); if (string_is_empty(buffer)) strlcpy(buffer, msg_hash_to_str(MENU_ENUM_LABEL_VALUE_NOT_AVAILABLE), sizeof(buffer)); field.placeholder = BOXSTRING(buffer); [alertView show]; } - (void)alertView:(UIAlertView*)alertView clickedButtonAtIndex:(NSInteger)buttonIndex { NSString* text = (NSString*)[alertView textFieldAtIndex:0].text; if (buttonIndex != alertView.firstOtherButtonIndex) return; if (!text.length) return; menu_entry_set_value(self.i, [text UTF8String]); [self.parentTable reloadData]; } - (void)attachDefaultingGestureTo:(UIView*)view { for (UIGestureRecognizer* i in view.gestureRecognizers) [view removeGestureRecognizer:i]; [view addGestureRecognizer: [[UILongPressGestureRecognizer alloc] initWithTarget:self action:@selector(resetValue:)]]; } - (void)resetValue:(UIGestureRecognizer*)gesture { struct string_list* items; RAMenuItemGeneric __weak* weakSelf; if (gesture.state != UIGestureRecognizerStateBegan) return; weakSelf = self; items = (struct string_list*)string_split("OK", "|"); RunActionSheet("Really Reset Value?", items, self.parentTable, ^(UIActionSheet* actionSheet, NSInteger buttonIndex) { if (buttonIndex != actionSheet.cancelButtonIndex) { menu_entry_reset(self.i); } [weakSelf.parentTable reloadData]; }); string_list_free(items); } @end @interface RAMenuItemNum : RAMenuItemGeneric @end @implementation RAMenuItemNum - (void)initialize:(NSObject *) main idx:(uint32_t) i { [super initialize:main idx:i]; self.formatter = [RANumberFormatter new]; if (menu_entry_num_has_range(self.i)) [self.formatter setRangeFrom:BOXFLOAT(menu_entry_num_min(self.i)) To:BOXFLOAT(menu_entry_num_max(self.i))]; } @end @interface RAMenuItemInt : RAMenuItemNum @end @implementation RAMenuItemInt @end @interface RAMenuItemUInt : RAMenuItemNum @end @implementation RAMenuItemUInt @end @interface RAMenuItemFloat : RAMenuItemNum @end @implementation RAMenuItemFloat - (void)initialize:(NSObject *) main idx:(uint32_t) i { [super initialize:main idx:i]; [self.formatter setAllowsFloats: true]; } @end @interface RAMenuItemString : RAMenuItemGeneric @end @implementation RAMenuItemString @end // XXX This should be a new kind that opens a color picker @interface RAMenuItemHex : RAMenuItemGeneric @end @implementation RAMenuItemHex @end @implementation RAMenuBase - (id)initWithStyle:(UITableViewStyle)style { if ((self = [super initWithStyle:style])) _sections = [NSMutableArray array]; return self; } - (NSInteger)numberOfSectionsInTableView:(UITableView*)tableView { return self.sections.count; } - (NSString*)tableView:(UITableView*)tableView titleForHeaderInSection:(NSInteger)section { return nil; } - (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section { return [self.sections[section] count]; } - (id)itemForIndexPath:(NSIndexPath*)indexPath { return self.sections[indexPath.section][indexPath.row]; } - (UITableViewCell*)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath { return [[self itemForIndexPath:indexPath] cellForTableView:tableView]; } - (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath { return UITableViewAutomaticDimension; } - (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath { [[self itemForIndexPath:indexPath] wasSelectedOnTableView:tableView ofController:self]; } - (void)willReloadData { } - (void)reloadData { [self willReloadData]; [self.tableView reloadData]; } -(void)renderMessageBox:(NSString *)msg { UIAlertView *message = [[UIAlertView alloc] initWithTitle:@"Help" message:msg delegate:self cancelButtonTitle:@"OK" otherButtonTitles:nil]; [message show]; } -(void)msgQueuePush:(NSString *)msg { self.osdmessage.text = msg; } - (void)alertView:(UIAlertView*)alertView clickedButtonAtIndex:(NSInteger)buttonIndex { menu_ctx_iterate_t iter; switch (buttonIndex) { case 0: iter.action = MENU_ACTION_OK; menu_driver_ctl(RARCH_MENU_CTL_ITERATE, &iter); break; } } @end @interface RAMainMenu : RAMenuBase @end @implementation RAMainMenu - (id)init { if ((self = [super initWithStyle:UITableViewStylePlain])) self.title = BOXSTRING("RetroArch"); return self; } - (void)viewWillAppear:(BOOL)animated { UIBarButtonItem *item = NULL; settings_t *settings = config_get_ptr(); [self reloadData]; self.osdmessage = [[UILabel alloc] initWithFrame:CGRectMake(0, 0, 300, 44)]; self.osdmessage.backgroundColor = [UIColor clearColor]; item = [[UIBarButtonItem alloc] initWithCustomView:self.osdmessage]; [self setToolbarItems: [NSArray arrayWithObject:item]]; if (settings->menu.core_enable) { char title_msg[256]; menu_entries_get_core_title(title_msg, sizeof(title_msg)); self.osdmessage.text = BOXSTRING(title_msg); } } - (void)willReloadData { size_t i, end; char title[256]; RAMainMenu* __weak weakSelf = NULL; NSMutableArray *everything = [NSMutableArray array]; settings_t *settings = config_get_ptr(); if (settings->menu.core_enable) { char title_msg[256]; menu_entries_get_core_title(title_msg, sizeof(title_msg)); self.osdmessage.text = BOXSTRING(title_msg); } menu_entries_get_title(title, sizeof(title)); self.title = BOXSTRING(title); end = menu_entries_get_end(); menu_entries_ctl(MENU_ENTRIES_CTL_START_GET, &i); for (; i < end; i++) [everything addObject:[self make_menu_item_for_entry: (uint32_t)i]]; self.sections = [NSMutableArray array]; [self.sections addObject:everything]; weakSelf = self; if (menu_entries_ctl(MENU_ENTRIES_CTL_SHOW_BACK, NULL)) [self set_leftbutton:BOXSTRING("Back") target:weakSelf action:@selector(menuBack)]; [self set_rightbutton:BOXSTRING("Switch") target:[RetroArch_iOS get] action:@selector(showGameView)]; } - (void) set_leftbutton:(NSString *)title target:(id)target action:(SEL)action { self.navigationItem.leftBarButtonItem = [[UIBarButtonItem alloc] initWithTitle:title style:UIBarButtonItemStyleBordered target:target action:action]; } - (void) set_rightbutton:(NSString *)title target:(id)target action:(SEL)action { self.navigationItem.rightBarButtonItem = [[UIBarButtonItem alloc] initWithTitle:title style:UIBarButtonItemStyleBordered target:target action:action]; } - (RAMenuItemBase*)make_menu_item_for_entry: (uint32_t) i { RAMenuItemBase *me = nil; switch (menu_entry_get_type(i)) { case MENU_ENTRY_ACTION: me = [RAMenuItemAction new]; break; case MENU_ENTRY_BOOL: me = [RAMenuItemBool new]; break; case MENU_ENTRY_INT: me = [RAMenuItemInt new]; break; case MENU_ENTRY_UINT: me = [RAMenuItemUInt new]; break; case MENU_ENTRY_FLOAT: me = [RAMenuItemFloat new]; break; case MENU_ENTRY_PATH: me = [RAMenuItemPath new]; break; case MENU_ENTRY_DIR: me = [RAMenuItemDir new]; break; case MENU_ENTRY_STRING: me = [RAMenuItemString new]; break; case MENU_ENTRY_HEX: me = [RAMenuItemHex new]; break; case MENU_ENTRY_BIND: me = [RAMenuItemBind new]; break; case MENU_ENTRY_ENUM: me = [RAMenuItemEnum new]; break; }; [me initialize:self idx:i]; return me; } - (void)menuSelect: (uint32_t) i { menu_entry_select(i); task_queue_ctl(TASK_QUEUE_CTL_CHECK, NULL); } - (void)menuBack { #ifdef HAVE_MENU size_t selection; menu_entry_t entry = {{0}}; menu_navigation_ctl(MENU_NAVIGATION_CTL_GET_SELECTION, & selection); menu_entry_get(&entry, 0, selection, NULL, false); menu_entry_action(&entry, (unsigned int)selection, MENU_ACTION_CANCEL); #endif } @end