Add Joy-Con pairing interface

This commit is contained in:
Lior Halphon 2022-10-30 14:42:54 +02:00
parent 5ef668251c
commit 2776c8ad36
16 changed files with 502 additions and 43 deletions

View File

@ -1,8 +1,9 @@
#import "GBApp.h"
#include "GBButtons.h"
#include "GBView.h"
#include "Document.h"
#include <Core/gb.h>
#import "GBButtons.h"
#import "GBView.h"
#import "Document.h"
#import "GBJoyConManager.h"
#import <Core/gb.h>
#import <Carbon/Carbon.h>
#import <JoyKit/JoyKit.h>
#import <WebKit/WebKit.h>
@ -79,6 +80,8 @@ static uint32_t color_to_int(NSColor *color)
@"GBMBC7JoystickOverride": @NO,
@"GBMBC7AllowMouse": @YES,
@"GBJoyConAutoPair": @YES,
// Default themes
@"GBThemes": @{
@"Desert": @{
@ -147,6 +150,8 @@ static uint32_t color_to_int(NSColor *color)
JOYHatsEmulateButtonsKey: @YES,
}];
[GBJoyConManager sharedInstance]; // Starts handling Joy-Cons
[JOYController registerListener:self];
if ([[NSUserDefaults standardUserDefaults] boolForKey:@"GBNotificationsUsed"]) {

13
Cocoa/GBJoyConManager.h Normal file
View File

@ -0,0 +1,13 @@
#import <Foundation/Foundation.h>
#import <AppKit/AppKit.h>
#import <JoyKit/JoyKit.h>
@interface GBJoyConManager : NSObject<JOYListener, NSTableViewDataSource, NSTableViewDelegate>
+ (instancetype) sharedInstance;
@property bool arrangementMode;
@property (weak) IBOutlet NSTableView *tableView;
@property (nonatomic) IBOutlet NSButton *autoPairCheckbox;
@end

251
Cocoa/GBJoyConManager.m Normal file
View File

@ -0,0 +1,251 @@
#import "GBJoyConManager.h"
#import "GBTintedImageCell.h"
#import <objc/runtime.h>
@implementation GBJoyConManager
{
GBTintedImageCell *_tintedImageCell;
NSImageCell *_imageCell;
NSMutableDictionary<NSString *, NSString *> *_pairings;
NSButton *_autoPairCheckbox;
bool _unpairing;
}
+ (instancetype)sharedInstance
{
static GBJoyConManager *manager = nil;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
manager = [[self alloc] _init];
});
return manager;
}
- (NSArray <JOYController *> *)joycons
{
NSMutableArray *ret = [[JOYController allControllers] mutableCopy];
for (JOYController *controller in [JOYController allControllers]) {
if (controller.joyconType == JOYJoyConTypeNone) {
[ret removeObject:controller];
}
}
return ret;
}
- (instancetype)init
{
return [self.class sharedInstance];
}
- (instancetype) _init
{
self = [super init];
_imageCell = [[NSImageCell alloc] init];
_tintedImageCell = [[GBTintedImageCell alloc] init];
if (@available(macOS 10.14, *)) {
_tintedImageCell.tint = [NSColor controlAccentColor];
}
else {
_tintedImageCell.tint = [NSColor selectedMenuItemColor];
}
_pairings = [[[NSUserDefaults standardUserDefaults] dictionaryForKey:@"GBJoyConPairings"] ?: @{} mutableCopy];
// Sanity check the pairings
for (NSString *key in _pairings) {
if (![_pairings[_pairings[key]] isEqualToString:key]) {
[_pairings removeAllObjects];
break;
}
}
[JOYController registerListener:self];
for (JOYController *controller in [JOYController allControllers]) {
[self controllerConnected:controller];
}
return self;
}
- (NSInteger)numberOfRowsInTableView:(NSTableView *)tableView
{
return self.joycons.count;
}
- (id)tableView:(NSTableView *)tableView objectValueForTableColumn:(NSTableColumn *)tableColumn row:(NSInteger)row
{
if (row >= [self numberOfRowsInTableView:tableView]) return nil;
unsigned columnIndex = [[tableView tableColumns] indexOfObject:tableColumn];
switch (columnIndex) {
case 0: {
JOYController *controller = self.joycons[row];
switch (controller.joyconType) {
case JOYJoyConTypeNone:
return nil;
case JOYJoyConTypeLeft:
return [NSImage imageNamed:@"JoyConLeftTemplate"];
case JOYJoyConTypeRight:
return [NSImage imageNamed:@"JoyConRightTemplate"];
case JOYJoyConTypeCombined:
return [NSImage imageNamed:@"JoyConCombinedTemplate"];
}
}
case 1: {
JOYController *controller = self.joycons[row];
NSMutableAttributedString *ret = [[NSMutableAttributedString alloc] initWithString:controller.deviceName
attributes:@{NSFontAttributeName:
[NSFont systemFontOfSize:[NSFont systemFontSize]]}];
[ret appendAttributedString:[[NSAttributedString alloc] initWithString:[@"\n" stringByAppendingString:controller.uniqueID]
attributes:@{NSFontAttributeName:
[NSFont systemFontOfSize:[NSFont smallSystemFontSize]],
NSForegroundColorAttributeName:[NSColor disabledControlTextColor]}]];
return ret;
}
case 2:
return @(rand() % 3);
}
return nil;
}
-(NSCell *)tableView:(NSTableView *)tableView dataCellForTableColumn:(NSTableColumn *)tableColumn row:(NSInteger)row
{
if (row >= [self numberOfRowsInTableView:tableView]) return [[NSCell alloc] init];
unsigned columnIndex = [[tableView tableColumns] indexOfObject:tableColumn];
if (columnIndex == 2) {
JOYCombinedController *controller = (JOYCombinedController *)self.joycons[row];
if (controller.joyconType == JOYJoyConTypeCombined) {
NSButtonCell *cell = [[NSButtonCell alloc] initTextCell:@"Separate Joy-Cons"];
cell.bezelStyle = NSBezelStyleRounded;
cell.action = @selector(invoke);
id block = ^(void) {
for (JOYController *child in controller.children) {
[_pairings removeObjectForKey:child.uniqueID];
}
[[NSUserDefaults standardUserDefaults] setObject:_pairings forKey:@"GBJoyConPairings"];
_unpairing = true;
[controller breakApart];
_unpairing = false;
};
// To retain the block
objc_setAssociatedObject(cell, @selector(breakApart), block, OBJC_ASSOCIATION_RETAIN);
cell.target = block;
return cell;
}
}
if (columnIndex == 0) {
JOYController *controller = self.joycons[row];
for (JOYButton *button in controller.buttons) {
if (button.isPressed) {
return _tintedImageCell;
}
}
return _imageCell;
}
return nil;
}
- (void)controllerConnected:(JOYController *)controller
{
for (JOYController *partner in [JOYController allControllers]) {
if ([partner.uniqueID isEqualToString:_pairings[controller.uniqueID]]) {
[self pairJoyCon:controller withJoyCon:partner];
break;
}
}
if (controller.joyconType == JOYJoyConTypeLeft || controller.joyconType == JOYJoyConTypeRight) {
[self autopair];
}
[self.tableView reloadData];
}
- (void)autopair
{
if (_unpairing) return;
if (![[NSUserDefaults standardUserDefaults] boolForKey:@"GBJoyConAutoPair"]) return;
NSArray<JOYController *> *controllers = [[JOYController allControllers] copy];
for (JOYController *first in controllers) {
if (_pairings[first.uniqueID]) continue; // Has an established partner
if (first.joyconType != JOYJoyConTypeLeft) continue;
for (JOYController *second in controllers) {
if (_pairings[second.uniqueID]) continue; // Has an established partner
if (second.joyconType != JOYJoyConTypeRight) continue;
[self pairJoyCon:first withJoyCon:second];
break;
}
}
[self.tableView reloadData];
}
- (void)controllerDisconnected:(JOYController *)controller
{
[self.tableView reloadData];
}
- (BOOL)tableView:(NSTableView *)tableView shouldEditTableColumn:(NSTableColumn *)tableColumn row:(NSInteger)row
{
return false;
}
- (JOYCombinedController *)pairJoyCon:(JOYController *)first withJoyCon:(JOYController *)second
{
if (first.joyconType != JOYJoyConTypeLeft && first.joyconType != JOYJoyConTypeRight) return nil; // Not a Joy-Con
if (second.joyconType != JOYJoyConTypeLeft && second.joyconType != JOYJoyConTypeRight) return nil; // Not a Joy-Con
if (first.joyconType == second.joyconType) return nil; // Not a sensible pair
_pairings[first.uniqueID] = second.uniqueID;
_pairings[second.uniqueID] = first.uniqueID;
[[NSUserDefaults standardUserDefaults] setObject:_pairings forKey:@"GBJoyConPairings"];
return [[JOYCombinedController alloc] initWithChildren:@[first, second]];
}
- (void)controller:(JOYController *)controller buttonChangedState:(JOYButton *)button
{
if (!_arrangementMode) return;
if (controller.joyconType == JOYJoyConTypeNone) return;
[self.tableView setNeedsDisplay:true];
if (controller.joyconType != JOYJoyConTypeLeft && controller.joyconType != JOYJoyConTypeRight) return;
if (button.usage != JOYButtonUsageL1 && button.usage != JOYButtonUsageR1) return;
// L or R were pressed on a single Joy-Con, try and pair available Joy-Cons
JOYController *left = nil;
JOYController *right = nil;
for (JOYController *controller in [JOYController allControllers]) {
if (!left && controller.joyconType == JOYJoyConTypeLeft) {
for (JOYButton *button in controller.buttons) {
if (button.usage == JOYButtonUsageL1 && button.isPressed) {
left = controller;
break;
}
}
}
if (!right && controller.joyconType == JOYJoyConTypeRight) {
for (JOYButton *button in controller.buttons) {
if (button.usage == JOYButtonUsageR1 && button.isPressed) {
right = controller;
break;
}
}
}
if (left && right) {
[self pairJoyCon:left withJoyCon:right];
return;
}
}
}
- (void)setAutoPairCheckbox:(NSButton *)autoPairCheckbox
{
_autoPairCheckbox = autoPairCheckbox;
[_autoPairCheckbox setState:[[NSUserDefaults standardUserDefaults] boolForKey:@"GBJoyConAutoPair"]];
}
- (IBAction)toggleAutoPair:(NSButton *)sender
{
[[NSUserDefaults standardUserDefaults] setBool:sender.state forKey:@"GBJoyConAutoPair"];
[self autopair];
}
@end

View File

@ -38,4 +38,5 @@
@property IBOutlet NSWindow *paletteEditor;
@property IBOutlet NSButton *joystickMBC7Checkbox;
@property IBOutlet NSButton *mouseMBC7Checkbox;
@property IBOutlet NSWindow *joyconsSheet;
@end

View File

@ -1,4 +1,5 @@
#import "GBPreferencesWindow.h"
#import "GBJoyConManager.h"
#import "NSString+StringForKey.h"
#import "GBButtons.h"
#import "BigSurToolbar.h"
@ -999,4 +1000,16 @@ static inline NSString *keyEquivalentString(NSMenuItem *item)
preferredEdge:NSRectEdgeMaxX];
}
- (IBAction)arrangeJoyCons:(id)sender
{
[GBJoyConManager sharedInstance].arrangementMode = true;
[self beginSheet:self.joyconsSheet completionHandler:nil];
}
- (IBAction)closeJoyConsSheet:(id)sender
{
[self endSheet:self.joyconsSheet];
[GBJoyConManager sharedInstance].arrangementMode = false;
}
@end

View File

@ -0,0 +1,5 @@
#import <Cocoa/Cocoa.h>
@interface GBTintedImageCell : NSImageCell
@property NSColor *tint;
@end

20
Cocoa/GBTintedImageCell.m Normal file
View File

@ -0,0 +1,20 @@
#import "GBTintedImageCell.h"
@implementation GBTintedImageCell
- (NSImage *)image
{
if (!self.tint || !super.image.isTemplate) {
return [super image];
}
NSImage *tinted = [super.image copy];
[tinted lockFocus];
[self.tint set];
NSRectFillUsingOperation((NSRect){.size = tinted.size}, NSCompositeSourceIn);
[tinted unlockFocus];
tinted.template = false;
return tinted;
}
@end

Binary file not shown.

After

Width:  |  Height:  |  Size: 315 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 483 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 266 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 502 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 262 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 411 B

View File

@ -89,6 +89,7 @@
<outlet property="hotkey1PopupButton" destination="qsH-w5-Rja" id="TFr-p8-IYo"/>
<outlet property="hotkey2PopupButton" destination="Ryk-mD-c0y" id="m2J-O5-dng"/>
<outlet property="interferenceSlider" destination="FpE-5i-j5L" id="hfH-e8-7cx"/>
<outlet property="joyconsSheet" destination="bn1-MD-iQb" id="rOY-jn-trO"/>
<outlet property="joystickMBC7Checkbox" destination="i7F-1r-NkQ" id="msM-WX-w9H"/>
<outlet property="mouseMBC7Checkbox" destination="wUE-aB-ub1" id="mXX-mZ-sKJ"/>
<outlet property="paletteEditor" destination="g32-xe-7al" id="WLk-Hh-h5v"/>
@ -940,6 +941,9 @@
<behavior key="behavior" pushIn="YES" lightByBackground="YES" lightByGray="YES"/>
<font key="font" metaFont="system"/>
</buttonCell>
<connections>
<action selector="arrangeJoyCons:" target="QvC-M9-y7g" id="BuK-Mb-nkq"/>
</connections>
</button>
<scrollView focusRingType="none" fixedFrame="YES" autohidesScrollers="YES" horizontalLineScroll="19" horizontalPageScroll="10" verticalLineScroll="19" verticalPageScroll="10" hasHorizontalScroller="NO" hasVerticalScroller="NO" usesPredominantAxisScrolling="NO" horizontalScrollElasticity="none" verticalScrollElasticity="none" translatesAutoresizingMaskIntoConstraints="NO" id="PBp-dj-EIa">
<rect key="frame" x="32" y="124" width="262" height="211"/>
@ -1074,7 +1078,7 @@
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" heightSizable="YES"/>
<clipView key="contentView" ambiguous="YES" drawsBackground="NO" id="5Al-aC-tq8">
<rect key="frame" x="1" y="1" width="158" height="316"/>
<autoresizingMask key="autoresizingMask"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<subviews>
<tableView focusRingType="none" verticalHuggingPriority="750" ambiguous="YES" allowsExpansionToolTips="YES" columnAutoresizingStyle="lastColumnOnly" selectionHighlightStyle="sourceList" columnResizing="NO" multipleSelection="NO" emptySelection="NO" autosaveColumns="NO" id="ZVn-bk-duk">
<rect key="frame" x="0.0" y="0.0" width="158" height="316"/>
@ -1306,6 +1310,138 @@
<outlet property="themesList" destination="ZVn-bk-duk" id="S4b-vM-ioi"/>
</connections>
</customObject>
<window title="Arrange Joy-Cons" allowsToolTipsWhenApplicationIsInactive="NO" autorecalculatesKeyViewLoop="NO" releasedWhenClosed="NO" visibleAtLaunch="NO" frameAutosaveName="" animationBehavior="default" id="bn1-MD-iQb">
<windowStyleMask key="styleMask" titled="YES" closable="YES"/>
<windowPositionMask key="initialPositionMask" leftStrut="YES" rightStrut="YES" topStrut="YES" bottomStrut="YES"/>
<rect key="contentRect" x="283" y="305" width="521" height="322"/>
<rect key="screenRect" x="0.0" y="0.0" width="2560" height="1417"/>
<view key="contentView" id="waz-WG-RYg">
<rect key="frame" x="0.0" y="0.0" width="521" height="322"/>
<autoresizingMask key="autoresizingMask"/>
<subviews>
<button focusRingType="none" verticalHuggingPriority="750" fixedFrame="YES" translatesAutoresizingMaskIntoConstraints="NO" id="xhI-kr-h3J">
<rect key="frame" x="18" y="287" width="485" height="18"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" flexibleMinY="YES"/>
<buttonCell key="cell" type="check" title="Automatically pair together left and right Joy-Cons when they're connected" bezelStyle="regularSquare" imagePosition="left" state="on" focusRingType="none" inset="2" id="dPv-XO-6Fp">
<behavior key="behavior" changeContents="YES" doesNotDimImage="YES" lightByContents="YES"/>
<font key="font" metaFont="system"/>
</buttonCell>
<connections>
<action selector="toggleAutoPair:" target="fMo-Ht-Dis" id="Luy-Rm-C5j"/>
</connections>
</button>
<button focusRingType="none" verticalHuggingPriority="750" fixedFrame="YES" translatesAutoresizingMaskIntoConstraints="NO" id="DaV-H7-VVr">
<rect key="frame" x="18" y="266" width="485" height="18"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" flexibleMinY="YES"/>
<buttonCell key="cell" type="check" title="Single Joy-Cons default to horizontal orientation" bezelStyle="regularSquare" imagePosition="left" state="on" focusRingType="none" inset="2" id="18L-S2-0wg">
<behavior key="behavior" changeContents="YES" doesNotDimImage="YES" lightByContents="YES"/>
<font key="font" metaFont="system"/>
</buttonCell>
</button>
<textField focusRingType="none" horizontalHuggingPriority="251" verticalHuggingPriority="750" fixedFrame="YES" translatesAutoresizingMaskIntoConstraints="NO" id="gmw-t5-3nG">
<rect key="frame" x="18" y="20" width="411" height="14"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES"/>
<textFieldCell key="cell" controlSize="small" lineBreakMode="clipping" focusRingType="none" title="Hold L and R on two Joy-Cons to pair them together" id="kRC-pE-5Pd">
<font key="font" metaFont="smallSystem"/>
<color key="textColor" name="labelColor" catalog="System" colorSpace="catalog"/>
<color key="backgroundColor" name="textBackgroundColor" catalog="System" colorSpace="catalog"/>
</textFieldCell>
</textField>
<scrollView focusRingType="none" fixedFrame="YES" autohidesScrollers="YES" horizontalLineScroll="38" horizontalPageScroll="10" verticalLineScroll="38" verticalPageScroll="10" usesPredominantAxisScrolling="NO" translatesAutoresizingMaskIntoConstraints="NO" id="9m1-mD-ctP">
<rect key="frame" x="20" y="55" width="481" height="205"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<clipView key="contentView" ambiguous="YES" id="4U7-cB-J7O">
<rect key="frame" x="1" y="1" width="479" height="203"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<subviews>
<tableView focusRingType="none" verticalHuggingPriority="750" ambiguous="YES" allowsExpansionToolTips="YES" columnAutoresizingStyle="lastColumnOnly" selectionHighlightStyle="none" alternatingRowBackgroundColors="YES" columnReordering="NO" columnResizing="NO" multipleSelection="NO" autosaveColumns="NO" typeSelect="NO" rowHeight="36" rowSizeStyle="large" id="XQa-0K-gl3">
<rect key="frame" x="0.0" y="0.0" width="479" height="203"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<size key="intercellSpacing" width="3" height="2"/>
<color key="backgroundColor" name="controlBackgroundColor" catalog="System" colorSpace="catalog"/>
<color key="gridColor" name="gridColor" catalog="System" colorSpace="catalog"/>
<tableColumns>
<tableColumn width="40" minWidth="40" maxWidth="40" id="GX5-ka-beq">
<tableHeaderCell key="headerCell" lineBreakMode="truncatingTail" borderStyle="border">
<font key="font" metaFont="smallSystem"/>
<color key="textColor" name="headerTextColor" catalog="System" colorSpace="catalog"/>
<color key="backgroundColor" name="headerColor" catalog="System" colorSpace="catalog"/>
</tableHeaderCell>
<imageCell key="dataCell" alignment="left" imageScaling="proportionallyDown" id="HEi-ii-9Hv" customClass="GBTintedImageCell"/>
<tableColumnResizingMask key="resizingMask" resizeWithTable="YES" userResizable="YES"/>
</tableColumn>
<tableColumn width="250" minWidth="40" maxWidth="1000" id="YhU-Z2-p6i">
<tableHeaderCell key="headerCell" lineBreakMode="truncatingTail" borderStyle="border">
<font key="font" metaFont="smallSystem"/>
<color key="textColor" name="headerTextColor" catalog="System" colorSpace="catalog"/>
<color key="backgroundColor" name="headerColor" catalog="System" colorSpace="catalog"/>
</tableHeaderCell>
<textFieldCell key="dataCell" scrollable="YES" lineBreakMode="clipping" selectable="YES" editable="YES" id="5rv-V2-pBy">
<font key="font" metaFont="system"/>
<string key="title">Text Cell
Test</string>
<color key="textColor" name="controlTextColor" catalog="System" colorSpace="catalog"/>
<color key="backgroundColor" name="controlBackgroundColor" catalog="System" colorSpace="catalog"/>
</textFieldCell>
<tableColumnResizingMask key="resizingMask" resizeWithTable="YES" userResizable="YES"/>
</tableColumn>
<tableColumn width="180" minWidth="180" maxWidth="180" id="9fC-GK-Fzn">
<tableHeaderCell key="headerCell" lineBreakMode="truncatingTail" borderStyle="border" alignment="left">
<font key="font" metaFont="smallSystem"/>
<color key="textColor" name="headerTextColor" catalog="System" colorSpace="catalog"/>
<color key="backgroundColor" white="0.0" alpha="0.0" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
</tableHeaderCell>
<popUpButtonCell key="dataCell" type="bevel" title="Horizontal Orientation" bezelStyle="rounded" alignment="left" lineBreakMode="truncatingTail" state="on" borderStyle="bezel" imageScaling="proportionallyDown" inset="2" arrowPosition="arrowAtCenter" preferredEdge="maxY" selectedItem="IJX-Qq-TaU" id="rif-6W-xsN">
<behavior key="behavior" lightByBackground="YES" lightByGray="YES"/>
<font key="font" metaFont="system"/>
<menu key="menu" id="36b-lh-dDy">
<items>
<menuItem title="Default Orientation" id="8im-vm-Bj9"/>
<menuItem title="Vertical Orientation" id="bU8-po-vzF"/>
<menuItem title="Horizontal Orientation" state="on" id="IJX-Qq-TaU"/>
</items>
</menu>
</popUpButtonCell>
<tableColumnResizingMask key="resizingMask" resizeWithTable="YES" userResizable="YES"/>
</tableColumn>
</tableColumns>
<connections>
<outlet property="dataSource" destination="fMo-Ht-Dis" id="7oa-g5-U2s"/>
<outlet property="delegate" destination="fMo-Ht-Dis" id="7Vg-ta-SaM"/>
</connections>
</tableView>
</subviews>
</clipView>
<scroller key="horizontalScroller" hidden="YES" wantsLayer="YES" verticalHuggingPriority="750" horizontal="YES" id="QaD-xE-QPs">
<rect key="frame" x="1" y="188" width="476" height="16"/>
<autoresizingMask key="autoresizingMask"/>
</scroller>
<scroller key="verticalScroller" hidden="YES" wantsLayer="YES" verticalHuggingPriority="750" horizontal="NO" id="Plj-YY-6jm">
<rect key="frame" x="224" y="17" width="15" height="102"/>
<autoresizingMask key="autoresizingMask"/>
</scroller>
</scrollView>
<button focusRingType="none" verticalHuggingPriority="750" fixedFrame="YES" translatesAutoresizingMaskIntoConstraints="NO" id="6Tm-Sf-8w1">
<rect key="frame" x="432" y="13" width="75" height="32"/>
<autoresizingMask key="autoresizingMask" flexibleMinX="YES" flexibleMaxY="YES"/>
<buttonCell key="cell" type="push" title="Close" bezelStyle="rounded" alignment="center" borderStyle="border" focusRingType="none" imageScaling="proportionallyDown" inset="2" id="leb-Jp-RfR">
<behavior key="behavior" pushIn="YES" lightByBackground="YES" lightByGray="YES"/>
<font key="font" metaFont="system"/>
</buttonCell>
<connections>
<action selector="closeJoyConsSheet:" target="QvC-M9-y7g" id="K2P-xu-B1o"/>
</connections>
</button>
</subviews>
</view>
<point key="canvasLocation" x="-1062.5" y="-96"/>
</window>
<customObject id="fMo-Ht-Dis" customClass="GBJoyConManager">
<connections>
<outlet property="autoPairCheckbox" destination="xhI-kr-h3J" id="CaJ-9q-dUh"/>
<outlet property="tableView" destination="XQa-0K-gl3" id="fhZ-wM-dnm"/>
</connections>
</customObject>
</objects>
<resources>
<image name="CPU" width="32" height="32"/>

View File

@ -29,13 +29,18 @@ typedef enum {
JOYControllerCombinedTypeCombined,
} JOYControllerCombinedType;
typedef enum {
JOYJoyConTypeNone,
JOYJoyConTypeLeft,
JOYJoyConTypeRight,
JOYJoyConTypeCombined,
} JOYJoyConType;
@interface JOYController : NSObject
+ (void)startOnRunLoop:(NSRunLoop *)runloop withOptions: (NSDictionary *)options;
+ (NSArray<JOYController *> *) allControllers;
+ (void) registerListener:(id<JOYListener>)listener;
+ (void) unregisterListener:(id<JOYListener>)listener;
- (NSString *)deviceName;
- (NSString *)uniqueID;
- (JOYControllerCombinedType)combinedControllerType;
- (NSArray<JOYButton *> *) buttons;
- (NSArray<JOYAxis *> *) axes;
@ -47,12 +52,15 @@ typedef enum {
- (void)setPlayerLEDs:(uint8_t)mask;
- (uint8_t)LEDMaskForPlayer:(unsigned)player;
@property (readonly, getter=isConnected) bool connected;
@property (readonly) JOYJoyConType joyconType;
@property (readonly) NSString *deviceName;
@property (readonly) NSString *uniqueID;
@end
@interface JOYCombinedController : JOYController
- (instancetype)initWithChildren:(NSArray<JOYController *> *)children;
- (void)breakApart;
@property (readonly) NSArray<JOYController *> *chidlren;
@property (readonly) NSArray<JOYController *> *children;
@end

View File

@ -12,13 +12,6 @@ extern NSTextField *globalDebugField;
#define PWM_RESOLUTION 16
typedef enum {
JOYJoyConTypeNone,
JOYJoyConTypeLeft,
JOYJoyConTypeRight,
JOYJoyConTypeCombined,
} JOYJoyConType;
static NSString const *JOYAxisGroups = @"JOYAxisGroups";
static NSString const *JOYReportIDFilters = @"JOYReportIDFilters";
static NSString const *JOYButtonUsageMapping = @"JOYButtonUsageMapping";
@ -458,6 +451,7 @@ typedef union {
_device = (IOHIDDeviceRef)CFRetain(device);
_serialSuffix = suffix;
_playerLEDs = -1;
[self obtainInfo];
IOHIDDeviceRegisterInputValueCallback(device, HIDInput, (void *)self);
IOHIDDeviceScheduleWithRunLoop(device, CFRunLoopGetCurrent(), kCFRunLoopDefaultMode);
@ -478,6 +472,7 @@ typedef union {
_isSwitch = [_hacks[JOYIsSwitch] boolValue];
_isDualShock3 = [_hacks[JOYIsDualShock3] boolValue];
_isSony = [_hacks[JOYIsSony] boolValue];
_joyconType = [_hacks[JOYJoyCon] unsignedIntValue];
NSDictionary *customReports = hacks[JOYCustomReports];
_lastReport = [NSMutableData dataWithLength:MAX(
@ -657,15 +652,10 @@ typedef union {
return self;
}
- (NSString *)deviceName
{
if (!_device) return nil;
return IOHIDDeviceGetProperty(_device, CFSTR(kIOHIDProductKey));
}
- (NSString *)uniqueID
- (void)obtainInfo
{
if (!_device) return nil;
_deviceName = IOHIDDeviceGetProperty(_device, CFSTR(kIOHIDProductKey));
NSString *serial = (__bridge NSString *)IOHIDDeviceGetProperty(_device, CFSTR(kIOHIDSerialNumberKey));
if (!serial || [(__bridge NSString *)IOHIDDeviceGetProperty(_device, CFSTR(kIOHIDTransportKey)) isEqualToString:@"USB"]) {
serial = [NSString stringWithFormat:@"%04x%04x%08x",
@ -674,9 +664,10 @@ typedef union {
[(__bridge NSNumber *)IOHIDDeviceGetProperty(_device, CFSTR(kIOHIDLocationIDKey)) unsignedIntValue]];
}
if (_serialSuffix) {
return [NSString stringWithFormat:@"%@-%@", serial, _serialSuffix];
_uniqueID = [NSString stringWithFormat:@"%@-%@", serial, _serialSuffix];
return;
}
return serial;
_uniqueID = serial;
}
- (JOYControllerCombinedType)combinedControllerType
@ -798,7 +789,7 @@ typedef union {
[listener controller:_parent ?: self movedAxis:axis];
}
}
JOYEmulatedButton *button = _axisEmulatedButtons[@(axis.uniqueID)];
JOYEmulatedButton *button = _axisEmulatedButtons[@(axis.uniqueID & 0xFFFFFFFF)]; // Mask the combined prefix away
if ([button updateStateFromAxis:axis]) {
for (id<JOYListener> listener in listeners) {
if ([listener respondsToSelector:@selector(controller:buttonChangedState:)]) {
@ -820,7 +811,7 @@ typedef union {
[listener controller:_parent ?: self movedAxes2D:axes];
}
}
NSArray <JOYEmulatedButton *> *buttons = _axes2DEmulatedButtons[@(axes.uniqueID)];
NSArray <JOYEmulatedButton *> *buttons = _axes2DEmulatedButtons[@(axes.uniqueID & 0xFFFFFFFF)]; // Mask the combined prefix away
for (JOYEmulatedButton *button in buttons) {
if ([button updateStateFromAxes2D:axes]) {
for (id<JOYListener> listener in listeners) {
@ -859,7 +850,7 @@ typedef union {
}
}
NSArray <JOYEmulatedButton *> *buttons = _hatEmulatedButtons[@(hat.uniqueID)];
NSArray <JOYEmulatedButton *> *buttons = _hatEmulatedButtons[@(hat.uniqueID & 0xFFFFFFFF)]; // Mask the combined prefix away
for (JOYEmulatedButton *button in buttons) {
if ([button updateStateFromHat:hat]) {
for (id<JOYListener> listener in listeners) {
@ -877,6 +868,7 @@ typedef union {
- (void)disconnected
{
_physicallyConnected = false;
[_parent breakApart];
if (_logicallyConnected && [exposedControllers containsObject:self]) {
for (id<JOYListener> listener in listeners) {
@ -885,7 +877,6 @@ typedef union {
}
}
}
_physicallyConnected = false;
[exposedControllers removeObject:self];
[self setRumbleAmplitude:0];
dispatch_sync(_rumbleQueue, ^{
@ -1211,13 +1202,13 @@ typedef union {
{
self = [super init];
// Sorting makes the device name and unique id consistent
_chidlren = [children sortedArrayUsingComparator:^NSComparisonResult(JOYController *a, JOYController *b) {
_children = [children sortedArrayUsingComparator:^NSComparisonResult(JOYController *a, JOYController *b) {
return [a.uniqueID compare:b.uniqueID];
}];
if (_chidlren.count == 0) return nil;
if (_children.count == 0) return nil;
for (JOYController *child in _chidlren) {
for (JOYController *child in _children) {
if (child.combinedControllerType != JOYControllerCombinedTypeSingle) {
NSLog(@"Cannot combine non-single controller %@", child);
return nil;
@ -1229,7 +1220,7 @@ typedef union {
}
unsigned index = 0;
for (JOYController *child in _chidlren) {
for (JOYController *child in _children) {
for (id<JOYListener> listener in listeners) {
if ([listener respondsToSelector:@selector(controllerDisconnected:)]) {
[listener controllerDisconnected:child];
@ -1267,11 +1258,12 @@ typedef union {
}
}
for (JOYController *child in _chidlren) {
for (JOYController *child in _children) {
child->_parent = nil;
for (JOYInput *input in child.allInputs) {
input.combinedIndex = 0;
}
if (!child.connected) break;
[exposedControllers addObject:child];
for (id<JOYListener> listener in listeners) {
if ([listener respondsToSelector:@selector(controllerConnected:)]) {
@ -1284,7 +1276,7 @@ typedef union {
- (NSString *)deviceName
{
NSString *ret = nil;
for (JOYController *child in _chidlren) {
for (JOYController *child in _children) {
if (ret) {
ret = [ret stringByAppendingFormat:@" + %@", child.deviceName];
}
@ -1298,7 +1290,7 @@ typedef union {
- (NSString *)uniqueID
{
NSString *ret = nil;
for (JOYController *child in _chidlren) {
for (JOYController *child in _children) {
if (ret) {
ret = [ret stringByAppendingFormat:@"+%@", child.uniqueID];
}
@ -1317,7 +1309,7 @@ typedef union {
- (NSArray<JOYButton *> *)buttons
{
NSArray<JOYButton *> *ret = nil;
for (JOYController *child in _chidlren) {
for (JOYController *child in _children) {
if (ret) {
ret = [ret arrayByAddingObjectsFromArray:child.buttons];
}
@ -1331,7 +1323,7 @@ typedef union {
- (NSArray<JOYAxis *> *)axes
{
NSArray<JOYAxis *> *ret = nil;
for (JOYController *child in _chidlren) {
for (JOYController *child in _children) {
if (ret) {
ret = [ret arrayByAddingObjectsFromArray:child.axes];
}
@ -1345,7 +1337,7 @@ typedef union {
- (NSArray<JOYAxes2D *> *)axes2D
{
NSArray<JOYAxes2D *> *ret = nil;
for (JOYController *child in _chidlren) {
for (JOYController *child in _children) {
if (ret) {
ret = [ret arrayByAddingObjectsFromArray:child.axes2D];
}
@ -1359,7 +1351,7 @@ typedef union {
- (NSArray<JOYAxes3D *> *)axes3D
{
NSArray<JOYAxes3D *> *ret = nil;
for (JOYController *child in _chidlren) {
for (JOYController *child in _children) {
if (ret) {
ret = [ret arrayByAddingObjectsFromArray:child.axes3D];
}
@ -1373,7 +1365,7 @@ typedef union {
- (NSArray<JOYHat *> *)hats
{
NSArray<JOYHat *> *ret = nil;
for (JOYController *child in _chidlren) {
for (JOYController *child in _children) {
if (ret) {
ret = [ret arrayByAddingObjectsFromArray:child.hats];
}
@ -1386,7 +1378,7 @@ typedef union {
- (void)setRumbleAmplitude:(double)amp
{
for (JOYController *child in _chidlren) {
for (JOYController *child in _children) {
[child setRumbleAmplitude:amp];
}
}
@ -1395,7 +1387,7 @@ typedef union {
{
// Mask is actually just the player ID in a combined controller to
// allow combining controllers with different LED layouts
for (JOYController *child in _chidlren) {
for (JOYController *child in _children) {
[child setPlayerLEDs:[child LEDMaskForPlayer:mask]];
}
}
@ -1412,7 +1404,7 @@ typedef union {
return false;
}
for (JOYController *child in _chidlren) {
for (JOYController *child in _children) {
if (!child.isConnected) {
return false; // Should never happen
}
@ -1421,4 +1413,19 @@ typedef union {
return true;
}
- (JOYJoyConType)joyconType
{
if (_children.count != 2) return JOYJoyConTypeNone;
if (_children[0].joyconType == JOYJoyConTypeLeft &&
_children[1].joyconType == JOYJoyConTypeRight) {
return JOYJoyConTypeCombined;
}
if (_children[1].joyconType == JOYJoyConTypeLeft &&
_children[0].joyconType == JOYJoyConTypeRight) {
return JOYJoyConTypeCombined;
}
return JOYJoyConTypeNone;
}
@end