SameBoy | Accurate GB/GBC emulator |
| download: https://git.y1.nz/archives/sameboy.tar.gz | |
| README | Files | Log | Refs | LICENSE |
commit 2776c8ad36e06d1f211b5f73438594bcd6e25f9b parent 5ef668251c9e0e857ba1910968a6e82c2201a119 Author: Lior Halphon <LIJI32@gmail.com> Date: Sun, 30 Oct 2022 14:42:54 +0200 Add Joy-Con pairing interface Diffstat:
| M | Cocoa/GBApp.m | 13 | +++++++++---- |
| A | Cocoa/GBJoyConManager.h | 13 | +++++++++++++ |
| A | Cocoa/GBJoyConManager.m | 251 | +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ |
| M | Cocoa/GBPreferencesWindow.h | 1 | + |
| M | Cocoa/GBPreferencesWindow.m | 13 | +++++++++++++ |
| A | Cocoa/GBTintedImageCell.h | 5 | +++++ |
| A | Cocoa/GBTintedImageCell.m | 20 | ++++++++++++++++++++ |
| A | Cocoa/JoyConCombinedTemplate.png | 0 | |
| A | Cocoa/JoyConCombinedTemplate@2x.png | 0 | |
| A | Cocoa/JoyConLeftTemplate.png | 0 | |
| A | Cocoa/JoyConLeftTemplate@2x.png | 0 | |
| A | Cocoa/JoyConRightTemplate.png | 0 | |
| A | Cocoa/JoyConRightTemplate@2x.png | 0 | |
| M | Cocoa/Preferences.xib | 138 | ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++- |
| M | JoyKit/JOYController.h | 14 | +++++++++++--- |
| M | JoyKit/JOYController.m | 77 | ++++++++++++++++++++++++++++++++++++++++++----------------------------------- |
16 files changed, 502 insertions(+), 43 deletions(-)
diff --git a/Cocoa/GBApp.m b/Cocoa/GBApp.m @@ -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"]) { diff --git a/Cocoa/GBJoyConManager.h b/Cocoa/GBJoyConManager.h @@ -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 + diff --git a/Cocoa/GBJoyConManager.m b/Cocoa/GBJoyConManager.m @@ -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 diff --git a/Cocoa/GBPreferencesWindow.h b/Cocoa/GBPreferencesWindow.h @@ -38,4 +38,5 @@ @property IBOutlet NSWindow *paletteEditor; @property IBOutlet NSButton *joystickMBC7Checkbox; @property IBOutlet NSButton *mouseMBC7Checkbox; +@property IBOutlet NSWindow *joyconsSheet; @end diff --git a/Cocoa/GBPreferencesWindow.m b/Cocoa/GBPreferencesWindow.m @@ -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 diff --git a/Cocoa/GBTintedImageCell.h b/Cocoa/GBTintedImageCell.h @@ -0,0 +1,5 @@ +#import <Cocoa/Cocoa.h> + +@interface GBTintedImageCell : NSImageCell +@property NSColor *tint; +@end diff --git a/Cocoa/GBTintedImageCell.m b/Cocoa/GBTintedImageCell.m @@ -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 diff --git a/Cocoa/JoyConCombinedTemplate.png b/Cocoa/JoyConCombinedTemplate.png Binary files differ. diff --git a/Cocoa/JoyConCombinedTemplate@2x.png b/Cocoa/JoyConCombinedTemplate@2x.png Binary files differ. diff --git a/Cocoa/JoyConLeftTemplate.png b/Cocoa/JoyConLeftTemplate.png Binary files differ. diff --git a/Cocoa/JoyConLeftTemplate@2x.png b/Cocoa/JoyConLeftTemplate@2x.png Binary files differ. diff --git a/Cocoa/JoyConRightTemplate.png b/Cocoa/JoyConRightTemplate.png Binary files differ. diff --git a/Cocoa/JoyConRightTemplate@2x.png b/Cocoa/JoyConRightTemplate@2x.png Binary files differ. diff --git a/Cocoa/Preferences.xib b/Cocoa/Preferences.xib @@ -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"/> diff --git a/JoyKit/JOYController.h b/JoyKit/JOYController.h @@ -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 diff --git a/JoyKit/JOYController.m b/JoyKit/JOYController.m @@ -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
This webpage is intended to be an accessible preview of this repository. To get a fuller picture, clone it and use the git CLI.