From dda777625239c46cc138dc9888d059061fe3a529 Mon Sep 17 00:00:00 2001 From: Eric Warmenhoven Date: Tue, 9 May 2023 18:32:55 -0400 Subject: [PATCH] tvOS Siri remote handling It's not really usable as a game controller, but it is good for going through the menu as a simple LRUD. This also adds better support for names of mFI controllers, as well as being able to do the expected tvOS behavior of "backing out" of the app. --- config.def.h | 2 +- input/drivers_joypad/mfi_joypad.m | 12 +- ui/drivers/cocoa/cocoa_common.m | 175 ++++++++++++++++++++++++++++++ ui/drivers/ui_cocoatouch.m | 2 + 4 files changed, 187 insertions(+), 4 deletions(-) diff --git a/config.def.h b/config.def.h index 34bbb961af..cc2ac387b8 100644 --- a/config.def.h +++ b/config.def.h @@ -873,7 +873,7 @@ #define DEFAULT_OVERLAY_DPAD_DIAGONAL_SENSITIVITY 80 #define DEFAULT_OVERLAY_ABXY_DIAGONAL_SENSITIVITY 50 -#if defined(ANDROID) || defined(_WIN32) || defined(HAVE_STEAM) +#if defined(ANDROID) || defined(_WIN32) || defined(HAVE_STEAM) || TARGET_OS_TV #define DEFAULT_MENU_SWAP_OK_CANCEL_BUTTONS true #else #define DEFAULT_MENU_SWAP_OK_CANCEL_BUTTONS false diff --git a/input/drivers_joypad/mfi_joypad.m b/input/drivers_joypad/mfi_joypad.m index 6ecb028396..fb8ed70997 100644 --- a/input/drivers_joypad/mfi_joypad.m +++ b/input/drivers_joypad/mfi_joypad.m @@ -137,6 +137,9 @@ static void apple_gamecontroller_joypad_poll_internal(GCController *controller, mfi_axes[slot][3] = gp.rightThumbstick.yAxis.value * 32767.0f; } + else if (controller.microGamepad) + { + } /* GCGamepad is deprecated */ #pragma clang diagnostic push @@ -255,9 +258,9 @@ static void apple_gamecontroller_joypad_register(GCController *controller) #pragma clang diagnostic pop } -static void mfi_joypad_autodetect_add(unsigned autoconf_pad) +static void mfi_joypad_autodetect_add(unsigned autoconf_pad, const char *display_name) { - input_autoconfigure_connect("mFi Controller", NULL, mfi_joypad.ident, autoconf_pad, 0, 0); + input_autoconfigure_connect("mFi Controller", display_name, mfi_joypad.ident, autoconf_pad, 0, 0); } #define MFI_RUMBLE_AVAIL API_AVAILABLE(macos(11.0), ios(14.0), tvos(14.0)) @@ -455,9 +458,12 @@ static void apple_gamecontroller_joypad_connect(GCController *controller) gc.playerIndex = newPlayerIndex++; } + if (controller.microGamepad && !controller.extendedGamepad) + return; + apple_gamecontroller_joypad_register(controller); apple_gamecontroller_joypad_setup_haptics(controller); - mfi_joypad_autodetect_add((unsigned)controller.playerIndex); + mfi_joypad_autodetect_add((unsigned)controller.playerIndex, [controller.vendorName cStringUsingEncoding:NSUTF8StringEncoding]); } } diff --git a/ui/drivers/cocoa/cocoa_common.m b/ui/drivers/cocoa/cocoa_common.m index 5d77509d45..3ec4084864 100644 --- a/ui/drivers/cocoa/cocoa_common.m +++ b/ui/drivers/cocoa/cocoa_common.m @@ -97,9 +97,167 @@ void *glkitview_init(void); video_driver_display_userdata_set((uintptr_t)self); #endif +#if TARGET_OS_TV + /* This causes all inputs to be handled by both mfi and uikit. + * + * For "extended gamepads" the only button we want to handle is 'cancel' + * (buttonB), and only when the cancel button wouldn't do anything. + */ + self.controllerUserInteractionEnabled = YES; +#endif + return self; } +#if TARGET_OS_TV +- (bool)menuIsAtTop +{ + struct menu_state *menu_st = menu_state_get_ptr(); + if (!(menu_st->flags & MENU_ST_FLAG_ALIVE)) // content + return false; + if (menu_st->flags & MENU_ST_FLAG_INP_DLG_KB_DISPLAY) // search + return false; + if (menu_st->selection_ptr != 0) // not the first item + return false; + if (menu_st->entries.list->menu_stack[0]->size != 1) // submenu + return false; + if (!string_is_equal(menu_st->entries.list->menu_stack[0]->list->label, // not on the main menu + msg_hash_to_str(MENU_ENUM_LABEL_MAIN_MENU))) + return false; + return true; +} + +- (bool)didMicroGamepadPress:(UIPressType)type +{ + if (type != UIPressTypeMenu && + type != UIPressTypeSelect && + type != UIPressTypePlayPause) + return false; + + NSArray* controllers = [GCController controllers]; + if ([controllers count] == 1) + return !controllers[0].extendedGamepad; + + bool microPress = false; + bool extendedPress = false; + for (GCController *controller in [GCController controllers]) { + // the microGamepad does not always know if Menu has been pressed, + // so we have to check all the extended gamepads as well + if (controller.extendedGamepad) + { + if (type == UIPressTypeMenu) + extendedPress |= controller.extendedGamepad.buttonB.pressed; + else if (type == UIPressTypeSelect) + extendedPress |= controller.extendedGamepad.buttonA.pressed; + else if (type == UIPressTypePlayPause) + extendedPress |= controller.extendedGamepad.buttonX.pressed; + } + else + { + if (type == UIPressTypeSelect) + extendedPress |= controller.extendedGamepad.buttonA.pressed; + else if (type == UIPressTypePlayPause) + extendedPress |= controller.extendedGamepad.buttonX.pressed; + else if (@available(tvOS 13, *)) { + if (type == UIPressTypeMenu) + extendedPress |= controller.microGamepad.buttonMenu.pressed || + controller.microGamepad.buttonMenu.isPressed; + } + } + } + + return microPress || !extendedPress; +} + +- (void)pressesBegan:(NSSet *)presses + withEvent:(UIPressesEvent *)event +{ + for (UIPress *press in presses) { + switch (press.type) + { + case UIPressTypePlayPause: + if ([self didMicroGamepadPress:press.type]) + apple_direct_input_keyboard_event(true, RETROK_s, 's', 0, RETRO_DEVICE_KEYBOARD); + break; + case UIPressTypeSelect: + if ([self didMicroGamepadPress:press.type]) + apple_direct_input_keyboard_event(true, RETROK_z, 'z', 0, RETRO_DEVICE_KEYBOARD); + break; + case UIPressTypeMenu: + if ([self menuIsAtTop]) + { + // if we're at the top it doesn't matter who pressed it, we want to leave + [super pressesBegan:presses withEvent:event]; + } + else if ([self didMicroGamepadPress:press.type]) + apple_direct_input_keyboard_event(true, RETROK_x, 0, 0, RETRO_DEVICE_KEYBOARD); + break; + default: + break; + } + } +} + +-(void)pressesEnded:(NSSet *)presses withEvent:(UIPressesEvent *)event +{ + for (UIPress *press in presses) { + switch (press.type) + { + case UIPressTypePlayPause: + apple_direct_input_keyboard_event(false, RETROK_s, 's', 0, RETRO_DEVICE_KEYBOARD); + break; + case UIPressTypeSelect: + apple_direct_input_keyboard_event(false, RETROK_z, 'z', 0, RETRO_DEVICE_KEYBOARD); + break; + case UIPressTypeMenu: + dispatch_after(dispatch_time(DISPATCH_TIME_NOW, NSEC_PER_MSEC), dispatch_get_main_queue(), ^{ + apple_direct_input_keyboard_event(false, RETROK_x, 0, 0, RETRO_DEVICE_KEYBOARD); + }); + break; + default: + break; + } + } +} + +-(void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event +{ +} + +-(void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event +{ +} + +-(void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event +{ +} + +-(void)touchesEstimatedPropertiesUpdated:(NSSet *)touches +{ +} + +-(void)touchesCancelled:(NSSet *)touches withEvent:(UIEvent *)event +{ +} + +-(void)handleSiriSwipe:(id)sender +{ + UISwipeGestureRecognizer *gestureRecognizer = (UISwipeGestureRecognizer*)sender; + unsigned code; + switch (gestureRecognizer.direction) + { + case UISwipeGestureRecognizerDirectionUp: code = RETROK_UP; break; + case UISwipeGestureRecognizerDirectionDown: code = RETROK_DOWN; break; + case UISwipeGestureRecognizerDirectionLeft: code = RETROK_LEFT; break; + case UISwipeGestureRecognizerDirectionRight: code = RETROK_RIGHT; break; + } + apple_direct_input_keyboard_event(true, code, 0, 0, RETRO_DEVICE_KEYBOARD); + dispatch_after(dispatch_time(DISPATCH_TIME_NOW, 1 * NSEC_PER_MSEC), dispatch_get_main_queue(), ^{ + apple_direct_input_keyboard_event(false, code, 0, 0, RETRO_DEVICE_KEYBOARD); + }); +} +#endif + #if defined(OSX) - (void)setFrame:(NSRect)frameRect { @@ -314,6 +472,23 @@ void *glkitview_init(void); #if __IPHONE_OS_VERSION_MIN_REQUIRED >= 130000 [self setupHelperBar]; #endif +#elif TARGET_OS_TV + UISwipeGestureRecognizer *siriSwipeUp = [[UISwipeGestureRecognizer alloc] initWithTarget:self action:@selector(handleSiriSwipe:)]; + siriSwipeUp.direction = UISwipeGestureRecognizerDirectionUp; + siriSwipeUp.delegate = self; + [self.view addGestureRecognizer:siriSwipeUp]; + UISwipeGestureRecognizer *siriSwipeDown = [[UISwipeGestureRecognizer alloc] initWithTarget:self action:@selector(handleSiriSwipe:)]; + siriSwipeDown.direction = UISwipeGestureRecognizerDirectionDown; + siriSwipeDown.delegate = self; + [self.view addGestureRecognizer:siriSwipeDown]; + UISwipeGestureRecognizer *siriSwipeLeft = [[UISwipeGestureRecognizer alloc] initWithTarget:self action:@selector(handleSiriSwipe:)]; + siriSwipeLeft.direction = UISwipeGestureRecognizerDirectionLeft; + siriSwipeLeft.delegate = self; + [self.view addGestureRecognizer:siriSwipeLeft]; + UISwipeGestureRecognizer *siriSwipeRight = [[UISwipeGestureRecognizer alloc] initWithTarget:self action:@selector(handleSiriSwipe:)]; + siriSwipeRight.direction = UISwipeGestureRecognizerDirectionRight; + siriSwipeRight.delegate = self; + [self.view addGestureRecognizer:siriSwipeRight]; #endif } diff --git a/ui/drivers/ui_cocoatouch.m b/ui/drivers/ui_cocoatouch.m index 615859d792..0129c9e3f0 100644 --- a/ui/drivers/ui_cocoatouch.m +++ b/ui/drivers/ui_cocoatouch.m @@ -109,6 +109,7 @@ void get_ios_version(int *major, int *minor) /* Input helpers: This is kept here because it needs ObjC */ static void handle_touch_event(NSArray* touches) { +#if !TARGET_OS_TV unsigned i; cocoa_input_data_t *apple = (cocoa_input_data_t*) input_state_get_ptr()->current_data; @@ -129,6 +130,7 @@ static void handle_touch_event(NSArray* touches) apple->touches[apple->touch_count ++].screen_y = coord.y * scale; } } +#endif } #ifndef HAVE_APPLE_STORE