mirror of https://github.com/snes9xgit/snes9x.git
COnfigurable joypad controls and video options
This commit is contained in:
parent
b52cc5ee8b
commit
8117af03b1
|
@ -20,9 +20,25 @@
|
|||
|
||||
#import <Cocoa/Cocoa.h>
|
||||
|
||||
@interface AppDelegate : NSObject <NSApplicationDelegate>
|
||||
@interface AppDelegate : NSObject <NSApplicationDelegate, S9xInputDelegate>
|
||||
|
||||
- (void)setButtonCode:(S9xButtonCode)buttonCode forKeyCode:(int16)keyCode player:(int8)player;
|
||||
- (void)clearButton:(S9xButtonCode)button forPlayer:(int8)player;
|
||||
|
||||
- (NSArray<S9xJoypad *> *)listJoypads;
|
||||
- (void)setPlayer:(int8)player forVendorID:(uint32)vendorID productID:(uint32)productID index:(uint32)index;
|
||||
- (BOOL)setButton:(S9xButtonCode)button forVendorID:(uint32)vendorID productID:(uint32)productID index:(uint32)index cookie:(uint32)cookie value:(int32)value;
|
||||
- (void)clearJoypadForVendorID:(uint32)vendorID productID:(uint32)productID index:(uint32)index buttonCode:(S9xButtonCode)buttonCode;
|
||||
- (NSString *)labelForVendorID:(uint32)vendorID productID:(uint32)productID cookie:(uint32)cookie value:(int32)value;
|
||||
|
||||
- (NSString *)prefsKeyForVendorID:(uint32)vendorID productID:(uint32)productID index:(uint32)index;
|
||||
- (BOOL)getValuesFromString:(NSString *)str vendorID:(uint32 *)vendorID productID:(uint32 *)productID index:(uint32 *)index;
|
||||
|
||||
- (NSString *)prefValueForCookie:(uint32)cookie value:(int32)value;
|
||||
- (BOOL)getValuesFromString:(NSString *)str cookie:(uint32 *)cookie value:(int32 *)value;
|
||||
|
||||
- (void)setVideoMode:(int)videoMode;
|
||||
- (void)setShowFPS:(BOOL)showFPS;
|
||||
|
||||
@end
|
||||
|
||||
|
|
|
@ -39,7 +39,8 @@ static NSWindowFrameAutosaveName const kMainWindowIdentifier = @"s9xMainWindow";
|
|||
|
||||
- (void)applicationDidFinishLaunching:(NSNotification *)aNotification {
|
||||
self.s9xEngine = [S9xEngine new];
|
||||
[self setupKeyboard];
|
||||
self.s9xEngine.inputDelegate = self;
|
||||
[self setupDefaults];
|
||||
[self importRecentItems];
|
||||
|
||||
NSWindow *window = [[NSWindow alloc] initWithContentRect:s9xView.frame styleMask:NSWindowStyleMaskTitled|NSWindowStyleMaskClosable|NSWindowStyleMaskMiniaturizable|NSWindowStyleMaskResizable backing:NSBackingStoreBuffered defer:NO];
|
||||
|
@ -76,7 +77,7 @@ static NSWindowFrameAutosaveName const kMainWindowIdentifier = @"s9xMainWindow";
|
|||
// Insert code here to tear down your application
|
||||
}
|
||||
|
||||
- (void)setupKeyboard
|
||||
- (void)setupDefaults
|
||||
{
|
||||
NSUserDefaults *defaults = NSUserDefaults.standardUserDefaults;
|
||||
|
||||
|
@ -140,6 +141,49 @@ static NSWindowFrameAutosaveName const kMainWindowIdentifier = @"s9xMainWindow";
|
|||
[self setButtonCode:buttonCode forKeyCode:self.keys[control].integerValue player:player];
|
||||
}
|
||||
|
||||
for ( S9xJoypad *joypad in [self listJoypads])
|
||||
{
|
||||
NSMutableDictionary *joypadPrefs = [[defaults objectForKey:kJoypadInputPrefs] mutableCopy];
|
||||
|
||||
if (joypadPrefs == nil)
|
||||
{
|
||||
joypadPrefs = [NSMutableDictionary new];
|
||||
[defaults synchronize];
|
||||
}
|
||||
|
||||
NSString *key = [self prefsKeyForVendorID:joypad.vendorID productID:joypad.productID index:joypad.index];
|
||||
|
||||
NSMutableDictionary *devicePrefs = [joypadPrefs[key] mutableCopy];
|
||||
if (devicePrefs == nil)
|
||||
{
|
||||
devicePrefs = [NSMutableDictionary new];
|
||||
for (S9xJoypadInput *input in [self.s9xEngine getInputsForVendorID:joypad.vendorID productID:joypad.productID index:joypad.index])
|
||||
{
|
||||
devicePrefs[@(input.buttonCode).stringValue] = [self prefValueForCookie:input.cookie value:input.value];
|
||||
}
|
||||
|
||||
joypadPrefs[key] = devicePrefs;
|
||||
[defaults setObject:joypadPrefs forKey:kJoypadInputPrefs];
|
||||
[defaults synchronize];
|
||||
}
|
||||
else
|
||||
{
|
||||
[self.s9xEngine clearJoypadForVendorID:joypad.vendorID productID:joypad.productID index:joypad.index];
|
||||
for (NSString *buttonCodeString in devicePrefs)
|
||||
{
|
||||
S9xButtonCode buttonCode = (S9xButtonCode)buttonCodeString.intValue;
|
||||
NSString *str = devicePrefs[buttonCodeString];
|
||||
uint32 cookie = 0;
|
||||
int32 value = -1;
|
||||
|
||||
if ([self getValuesFromString:str cookie:&cookie value:&value])
|
||||
{
|
||||
[self setButton:buttonCode forVendorID:joypad.vendorID productID:joypad.productID index:joypad.index cookie:cookie value:value];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
[self importKeySettings];
|
||||
[self importGraphicsSettings];
|
||||
[defaults synchronize];
|
||||
|
@ -167,6 +211,133 @@ static NSWindowFrameAutosaveName const kMainWindowIdentifier = @"s9xMainWindow";
|
|||
}
|
||||
}
|
||||
|
||||
- (void)clearButton:(S9xButtonCode)button forPlayer:(int8)player
|
||||
{
|
||||
[self.s9xEngine clearButton:button forPlayer:player];
|
||||
NSMutableDictionary *keyDict = [[NSUserDefaults.standardUserDefaults objectForKey:kKeyboardPrefs] mutableCopy];
|
||||
[keyDict removeObjectForKey:@(button).stringValue];
|
||||
[NSUserDefaults.standardUserDefaults setObject:[keyDict copy] forKey:kKeyboardPrefs];
|
||||
[NSUserDefaults.standardUserDefaults synchronize];
|
||||
}
|
||||
|
||||
- (NSArray<S9xJoypad *> *)listJoypads
|
||||
{
|
||||
return [self.s9xEngine listJoypads];
|
||||
}
|
||||
|
||||
- (NSString *)prefsKeyForVendorID:(uint32)vendorID productID:(uint32)productID index:(uint32)index
|
||||
{
|
||||
return [NSString stringWithFormat:@"%@:%@:%@", @(vendorID).stringValue, @(productID).stringValue, @(index).stringValue];
|
||||
}
|
||||
|
||||
- (BOOL)getValuesFromString:(NSString *)str vendorID:(uint32 *)vendorID productID:(uint32 *)productID index:(uint32 *)index
|
||||
{
|
||||
if (vendorID == NULL || productID == NULL || index == NULL)
|
||||
{
|
||||
return NO;
|
||||
|
||||
}
|
||||
|
||||
NSArray<NSString *> *components = [str componentsSeparatedByString:@":"];
|
||||
|
||||
if (components.count != 3)
|
||||
{
|
||||
return NO;
|
||||
}
|
||||
|
||||
*vendorID = components[0].intValue;
|
||||
*productID = components[1].intValue;
|
||||
*index = components[2].intValue;
|
||||
|
||||
return YES;
|
||||
}
|
||||
|
||||
- (NSString *)prefValueForCookie:(uint32)cookie value:(int32)value
|
||||
{
|
||||
return [NSString stringWithFormat:@"%@:%@", @(cookie).stringValue, @(value).stringValue];
|
||||
}
|
||||
|
||||
- (BOOL)getValuesFromString:(NSString *)str cookie:(uint32 *)cookie value:(int32 *)value
|
||||
{
|
||||
if (cookie == NULL || value == NULL)
|
||||
{
|
||||
return NO;
|
||||
}
|
||||
|
||||
NSArray<NSString *> *components = [str componentsSeparatedByString:@":"];
|
||||
|
||||
if (components.count != 2)
|
||||
{
|
||||
return NO;
|
||||
}
|
||||
|
||||
*cookie = components.firstObject.intValue;
|
||||
*value = components.lastObject.intValue;
|
||||
|
||||
return YES;
|
||||
}
|
||||
|
||||
- (void)setPlayer:(int8)player forVendorID:(uint32)vendorID productID:(uint32)productID index:(uint32)index
|
||||
{
|
||||
int8 oldPlayer = -1;
|
||||
[self.s9xEngine setPlayer:player forVendorID:vendorID productID:productID index:index oldPlayer:&oldPlayer];
|
||||
|
||||
NSMutableDictionary *playersDict = [[NSUserDefaults.standardUserDefaults objectForKey:kJoypadPlayerPrefs] mutableCopy];
|
||||
if (playersDict == nil)
|
||||
{
|
||||
playersDict = [NSMutableDictionary new];
|
||||
}
|
||||
|
||||
if (oldPlayer >= 0 && player != oldPlayer)
|
||||
{
|
||||
[playersDict removeObjectForKey:@(oldPlayer).stringValue];
|
||||
}
|
||||
|
||||
playersDict[@(player).stringValue] = [self prefsKeyForVendorID:vendorID productID:productID index:index];
|
||||
|
||||
[NSUserDefaults.standardUserDefaults setObject:[playersDict copy] forKey:kJoypadPlayerPrefs];
|
||||
}
|
||||
|
||||
- (BOOL)setButton:(S9xButtonCode)button forVendorID:(uint32)vendorID productID:(uint32)productID index:(uint32)index cookie:(uint32)cookie value:(int32)value
|
||||
{
|
||||
S9xButtonCode oldButton = (S9xButtonCode)-1;
|
||||
BOOL result = [self.s9xEngine setButton:button forVendorID:vendorID productID:productID index:index cookie:cookie value:value oldButton:&oldButton];
|
||||
|
||||
NSMutableDictionary *prefsDict = [[NSUserDefaults.standardUserDefaults objectForKey:kJoypadInputPrefs] mutableCopy];
|
||||
NSString *key = [self prefsKeyForVendorID:vendorID productID:productID index:index];
|
||||
NSMutableDictionary *joypadDict = [prefsDict[key] mutableCopy];
|
||||
|
||||
if (result && button != oldButton)
|
||||
{
|
||||
[joypadDict removeObjectForKey:@(oldButton).stringValue];
|
||||
}
|
||||
|
||||
joypadDict[@(button).stringValue] = [self prefValueForCookie:cookie value:value];
|
||||
|
||||
prefsDict[key] = [joypadDict copy];
|
||||
|
||||
[NSUserDefaults.standardUserDefaults setObject:[prefsDict copy] forKey:kJoypadInputPrefs];
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
- (void)clearJoypadForVendorID:(uint32)vendorID productID:(uint32)productID index:(uint32)index buttonCode:(S9xButtonCode)buttonCode
|
||||
{
|
||||
[self.s9xEngine clearJoypadForVendorID:vendorID productID:productID index:index buttonCode:buttonCode];
|
||||
NSString *key = [self prefsKeyForVendorID:vendorID productID:productID index:index];
|
||||
NSMutableDictionary *joypadsDict = [[NSUserDefaults.standardUserDefaults objectForKey:kJoypadInputPrefs] mutableCopy];
|
||||
NSMutableDictionary *deviceDict = [joypadsDict[key] mutableCopy];
|
||||
[deviceDict removeObjectForKey:@(buttonCode).stringValue];
|
||||
joypadsDict[key] = deviceDict;
|
||||
[NSUserDefaults.standardUserDefaults setObject:[joypadsDict copy] forKey:kJoypadInputPrefs];
|
||||
[NSUserDefaults.standardUserDefaults synchronize];
|
||||
}
|
||||
|
||||
- (NSString *)labelForVendorID:(uint32)vendorID productID:(uint32)productID cookie:(uint32)cookie value:(int32)value
|
||||
{
|
||||
return [self.s9xEngine labelForVendorID:vendorID productID:productID cookie:cookie value:value];
|
||||
}
|
||||
|
||||
- (void)importRecentItems
|
||||
{
|
||||
const NSInteger maxRecents = 20;
|
||||
|
@ -216,12 +387,16 @@ static NSWindowFrameAutosaveName const kMainWindowIdentifier = @"s9xMainWindow";
|
|||
[NSUserDefaults.standardUserDefaults setBool:(data.length > 0 && ((char *)data.bytes)[0]) forKey:kShowFPSPref];
|
||||
}
|
||||
|
||||
[self setShowFPS:[NSUserDefaults.standardUserDefaults boolForKey:kShowFPSPref]];
|
||||
|
||||
data = [self objectForPrefOSCode:'Vmod'];
|
||||
|
||||
if ( data != nil)
|
||||
{
|
||||
[NSUserDefaults.standardUserDefaults setInteger:((data.length >= 0 && ((char *)data.bytes)[0]) ? VIDEOMODE_SMOOTH : VIDEOMODE_BLOCKY) forKey:kVideoModePref];
|
||||
}
|
||||
|
||||
[self setVideoMode:(int)[NSUserDefaults.standardUserDefaults integerForKey:kVideoModePref]];
|
||||
}
|
||||
|
||||
- (id)objectForPrefOSCode:(uint32_t)osCode
|
||||
|
@ -304,4 +479,33 @@ static NSWindowFrameAutosaveName const kMainWindowIdentifier = @"s9xMainWindow";
|
|||
[self.prefsWindowController.window makeKeyAndOrderFront:self];
|
||||
}
|
||||
|
||||
- (void)setVideoMode:(int)videoMode
|
||||
{
|
||||
[self.s9xEngine setVideoMode:videoMode];
|
||||
[NSUserDefaults.standardUserDefaults setObject:@(videoMode) forKey:kVideoModePref];
|
||||
[NSUserDefaults.standardUserDefaults synchronize];
|
||||
}
|
||||
|
||||
- (void)setShowFPS:(BOOL)showFPS
|
||||
{
|
||||
[self.s9xEngine setShowFPS:showFPS];
|
||||
[NSUserDefaults.standardUserDefaults setObject:@(showFPS) forKey:kShowFPSPref];
|
||||
[NSUserDefaults.standardUserDefaults synchronize];
|
||||
}
|
||||
|
||||
- (IBAction)resume:(id)sender
|
||||
{
|
||||
[self.s9xEngine resume];
|
||||
}
|
||||
|
||||
- (BOOL)handleInput:(S9xJoypadInput *)input fromJoypad:(S9xJoypad *)joypad
|
||||
{
|
||||
if (NSApp.keyWindow == self.prefsWindowController.window)
|
||||
{
|
||||
return [((S9xPrefsViewController *) self.prefsWindowController.contentViewController) handleInput:input fromJoypad:joypad];
|
||||
}
|
||||
|
||||
return NO;
|
||||
}
|
||||
|
||||
@end
|
||||
|
|
|
@ -95,6 +95,18 @@
|
|||
</items>
|
||||
</menu>
|
||||
</menuItem>
|
||||
<menuItem title="Emulation" id="Kjk-eO-rPI">
|
||||
<modifierMask key="keyEquivalentModifierMask"/>
|
||||
<menu key="submenu" title="Emulation" id="s2F-Pd-YT5">
|
||||
<items>
|
||||
<menuItem title="Resume" keyEquivalent="r" id="m8Y-5s-VGC">
|
||||
<connections>
|
||||
<action selector="resume:" target="-1" id="jxw-ZM-JXi"/>
|
||||
</connections>
|
||||
</menuItem>
|
||||
</items>
|
||||
</menu>
|
||||
</menuItem>
|
||||
<menuItem title="View" id="H8h-7b-M4v">
|
||||
<modifierMask key="keyEquivalentModifierMask"/>
|
||||
<menu key="submenu" title="View" id="HyV-fh-RgO">
|
||||
|
|
|
@ -20,11 +20,15 @@
|
|||
|
||||
#import <Cocoa/Cocoa.h>
|
||||
|
||||
@class S9xJoypadInput;
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
@interface S9xButtonConfigTextField : NSSearchField<NSTextFieldDelegate>
|
||||
@interface S9xButtonConfigTextField : NSSearchField<NSSearchFieldDelegate>
|
||||
|
||||
@property (nonatomic, assign) CGKeyCode keyCode;
|
||||
@property (nonatomic, strong, nullable) S9xJoypadInput *joypadInput;
|
||||
@property (nonatomic, assign) BOOL disableKeyboardInput;
|
||||
|
||||
@end
|
||||
|
||||
|
|
|
@ -30,10 +30,21 @@
|
|||
self.delegate = self;
|
||||
self.placeholderString = @"";
|
||||
[[self cell] setSearchButtonCell:nil];
|
||||
|
||||
NSButtonCell *cancelButton = [[self cell] cancelButtonCell];
|
||||
cancelButton.target = self;
|
||||
cancelButton.action = @selector(clearSearch:);
|
||||
}
|
||||
|
||||
- (void)setKeyCode:(CGKeyCode)keyCode
|
||||
{
|
||||
if (keyCode == (CGKeyCode)-1)
|
||||
{
|
||||
_keyCode = keyCode;
|
||||
self.stringValue = @"";
|
||||
return;
|
||||
}
|
||||
|
||||
NSString *stringValue = nil;
|
||||
|
||||
switch (keyCode)
|
||||
|
@ -266,11 +277,19 @@
|
|||
|
||||
- (void)keyUp:(NSEvent *)event
|
||||
{
|
||||
[self setKeyCode:event.keyCode];
|
||||
if (!self.disableKeyboardInput )
|
||||
{
|
||||
[self setKeyCode:event.keyCode];
|
||||
}
|
||||
}
|
||||
|
||||
- (void)flagsChanged:(NSEvent *)event
|
||||
{
|
||||
if (self.disableKeyboardInput)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
NSEventModifierFlags flags = event.modifierFlags;
|
||||
|
||||
if ( flags & NSEventModifierFlagShift )
|
||||
|
@ -297,8 +316,27 @@
|
|||
[self.currentEditor selectAll:self];
|
||||
}
|
||||
|
||||
- (void)clearSearch:(id)sender
|
||||
{
|
||||
self.stringValue = @"";
|
||||
|
||||
if (self.disableKeyboardInput)
|
||||
{
|
||||
self.joypadInput = nil;
|
||||
}
|
||||
else
|
||||
{
|
||||
self.keyCode = -1;
|
||||
}
|
||||
}
|
||||
|
||||
- (BOOL)control:(NSControl *)control textView:(NSTextView *)textView doCommandBySelector:(SEL)commandSelector
|
||||
{
|
||||
if (self.disableKeyboardInput)
|
||||
{
|
||||
return NO;
|
||||
}
|
||||
|
||||
if (commandSelector == @selector(insertTab:))
|
||||
{
|
||||
[self setKeyCode:kVK_Tab];
|
||||
|
@ -313,4 +351,9 @@
|
|||
return NO;
|
||||
}
|
||||
|
||||
- (BOOL)control:(NSControl *)control textShouldBeginEditing:(NSText *)fieldEditor
|
||||
{
|
||||
return !self.disableKeyboardInput;
|
||||
}
|
||||
|
||||
@end
|
||||
|
|
|
@ -21,5 +21,7 @@
|
|||
#import <Foundation/Foundation.h>
|
||||
|
||||
extern NSString * const kKeyboardPrefs;
|
||||
extern NSString * const kJoypadInputPrefs;
|
||||
extern NSString * const kJoypadPlayerPrefs;
|
||||
extern NSString * const kShowFPSPref;
|
||||
extern NSString * const kVideoModePref;
|
||||
|
|
|
@ -21,5 +21,7 @@
|
|||
#import "S9xPrefsConstants.h"
|
||||
|
||||
NSString * const kKeyboardPrefs = @"KeyboardConfig";
|
||||
NSString * const kJoypadInputPrefs = @"JoypadInputs";
|
||||
NSString * const kJoypadPlayerPrefs = @"JoypadPlayers";
|
||||
NSString * const kShowFPSPref = @"ShowFPS";
|
||||
NSString * const kVideoModePref = @"VideoMode";
|
||||
|
|
|
@ -21,7 +21,7 @@
|
|||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
@interface S9xPrefsViewController : NSViewController
|
||||
@interface S9xPrefsViewController : NSViewController<S9xInputDelegate>
|
||||
|
||||
@end
|
||||
|
||||
|
|
|
@ -36,12 +36,58 @@
|
|||
|
||||
- (void)awakeFromNib
|
||||
{
|
||||
AppDelegate *appDelegate = (AppDelegate *)NSApp.delegate;
|
||||
NSUInteger joypadIndex = 0;
|
||||
for (S9xJoypad *joypad in [appDelegate listJoypads])
|
||||
{
|
||||
NSMenuItem *item = [NSMenuItem new];
|
||||
item.title = joypad.name;
|
||||
item.tag = joypadIndex++;
|
||||
item.representedObject = joypad;
|
||||
[self.devicePopUp.menu addItem:item];
|
||||
}
|
||||
|
||||
[self selectDeviceForPlayer:0];
|
||||
|
||||
for (NSView *subview in self.view.subviews)
|
||||
{
|
||||
if ( [subview isKindOfClass:[S9xButtonConfigTextField class]] )
|
||||
{
|
||||
S9xButtonConfigTextField *field = (S9xButtonConfigTextField *)subview;
|
||||
[field addObserver:self forKeyPath:@"keyCode" options:NSKeyValueObservingOptionNew context:NULL];
|
||||
[field addObserver:self forKeyPath:@"joypadInput" options:NSKeyValueObservingOptionNew context:NULL];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
- (void)selectDeviceForPlayer:(int8_t)player
|
||||
{
|
||||
AppDelegate *appDelegate = (AppDelegate *)NSApp.delegate;
|
||||
NSString* joypadKey = [[NSUserDefaults.standardUserDefaults objectForKey:kJoypadPlayerPrefs] objectForKey:@(player).stringValue];
|
||||
|
||||
[self.devicePopUp selectItemAtIndex:0];
|
||||
|
||||
if (joypadKey != nil)
|
||||
{
|
||||
uint32 vendorID = 0;
|
||||
uint32 productID = 0;
|
||||
uint32 index = 0;
|
||||
|
||||
if ( [appDelegate getValuesFromString:joypadKey vendorID:&vendorID productID:&productID index:&index])
|
||||
{
|
||||
S9xJoypad *joypad = [S9xJoypad new];
|
||||
joypad.vendorID = vendorID;
|
||||
joypad.productID = productID;
|
||||
joypad.index = index;
|
||||
|
||||
for (NSMenuItem *item in self.devicePopUp.menu.itemArray)
|
||||
{
|
||||
if ([joypad isEqual:item.representedObject])
|
||||
{
|
||||
[self.devicePopUp selectItem:item];
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -59,9 +105,10 @@
|
|||
[self.videoModePopup selectItemAtIndex:index];
|
||||
self.showFPSCheckbox.state = [NSUserDefaults.standardUserDefaults boolForKey:kShowFPSPref];
|
||||
|
||||
NSMutableDictionary<NSNumber *, NSNumber *> *controlsDict = [NSMutableDictionary new];
|
||||
if (self.devicePopUp.selectedItem.tag < 0)
|
||||
{
|
||||
NSMutableDictionary<NSNumber *, NSNumber *> *controlsDict = [NSMutableDictionary new];
|
||||
|
||||
NSDictionary *keyboardDict = [NSUserDefaults.standardUserDefaults objectForKey:kKeyboardPrefs];
|
||||
NSInteger playerNum = self.playerPopUp.selectedItem.tag;
|
||||
|
||||
|
@ -69,46 +116,182 @@
|
|||
{
|
||||
controlsDict[@(i)] = keyboardDict[@(i + playerNum).stringValue];
|
||||
}
|
||||
}
|
||||
|
||||
for (NSView *subview in self.view.subviews)
|
||||
{
|
||||
if ( [subview isKindOfClass:[S9xButtonConfigTextField class]] )
|
||||
for (NSView *subview in self.view.subviews)
|
||||
{
|
||||
S9xButtonConfigTextField *field = (S9xButtonConfigTextField *)subview;
|
||||
|
||||
[field removeObserver:self forKeyPath:@"keyCode"];
|
||||
|
||||
NSNumber *keyCode = controlsDict[@(field.tag)];
|
||||
|
||||
if ( keyCode != nil )
|
||||
if ([subview isKindOfClass:[S9xButtonConfigTextField class]])
|
||||
{
|
||||
field.keyCode = keyCode.intValue;
|
||||
}
|
||||
else
|
||||
{
|
||||
field.stringValue = @"";
|
||||
}
|
||||
S9xButtonConfigTextField *field = (S9xButtonConfigTextField *)subview;
|
||||
|
||||
[field addObserver:self forKeyPath:@"keyCode" options:NSKeyValueObservingOptionNew context:NULL];
|
||||
[field removeObserver:self forKeyPath:@"keyCode"];
|
||||
[field removeObserver:self forKeyPath:@"joypadInput"];
|
||||
|
||||
NSNumber *keyCode = controlsDict[@(field.tag)];
|
||||
|
||||
field.joypadInput = nil;
|
||||
|
||||
if ( keyCode != nil )
|
||||
{
|
||||
field.keyCode = keyCode.intValue;
|
||||
}
|
||||
else
|
||||
{
|
||||
field.keyCode = -1;
|
||||
}
|
||||
|
||||
[field addObserver:self forKeyPath:@"keyCode" options:NSKeyValueObservingOptionNew context:NULL];
|
||||
[field addObserver:self forKeyPath:@"joypadInput" options:NSKeyValueObservingOptionNew context:NULL];
|
||||
|
||||
field.disableKeyboardInput = NO;
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
AppDelegate *appDelegate = (AppDelegate *)NSApp.delegate;
|
||||
S9xJoypad *joypad = self.devicePopUp.selectedItem.representedObject;
|
||||
NSString *joypadKey = [appDelegate prefsKeyForVendorID:joypad.vendorID productID:joypad.productID index:joypad.index];
|
||||
NSDictionary *joypadDIct = [[NSUserDefaults.standardUserDefaults objectForKey:kJoypadInputPrefs] objectForKey:joypadKey];
|
||||
|
||||
for (NSView *subview in self.view.subviews)
|
||||
{
|
||||
if ([subview isKindOfClass:[S9xButtonConfigTextField class]])
|
||||
{
|
||||
S9xButtonConfigTextField *textField = (S9xButtonConfigTextField *)subview;
|
||||
|
||||
[textField removeObserver:self forKeyPath:@"keyCode"];
|
||||
[textField removeObserver:self forKeyPath:@"joypadInput"];
|
||||
|
||||
uint32 cookie = 0;
|
||||
int32 value = 0;
|
||||
S9xButtonCode buttonCode = (S9xButtonCode)textField.tag;
|
||||
NSString *inputString = joypadDIct[@(buttonCode).stringValue];
|
||||
|
||||
textField.keyCode = -1;
|
||||
|
||||
if ([appDelegate getValuesFromString:inputString cookie:&cookie value:&value])
|
||||
{
|
||||
S9xJoypadInput *input = [S9xJoypadInput new];
|
||||
input.cookie = cookie;
|
||||
input.value = value;
|
||||
input.buttonCode = buttonCode;
|
||||
textField.joypadInput = input;
|
||||
textField.stringValue = [appDelegate labelForVendorID:joypad.vendorID productID:joypad.productID cookie:cookie value:value];
|
||||
}
|
||||
else
|
||||
{
|
||||
textField.joypadInput = nil;
|
||||
textField.stringValue = @"";
|
||||
}
|
||||
|
||||
[textField addObserver:self forKeyPath:@"keyCode" options:NSKeyValueObservingOptionNew context:NULL];
|
||||
[textField addObserver:self forKeyPath:@"joypadInput" options:NSKeyValueObservingOptionNew context:NULL];
|
||||
|
||||
textField.disableKeyboardInput = YES;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context
|
||||
{
|
||||
if ( [keyPath isEqualToString:@"keyCode"] )
|
||||
if ([keyPath isEqualToString:@"keyCode"])
|
||||
{
|
||||
S9xButtonConfigTextField *field = (S9xButtonConfigTextField *)object;
|
||||
S9xButtonCode buttonCode = (S9xButtonCode)field.tag;
|
||||
uint16_t keyCode = field.keyCode;
|
||||
int8_t player = self.playerPopUp.selectedItem.tag;
|
||||
|
||||
[((AppDelegate *) NSApp.delegate) setButtonCode:buttonCode forKeyCode:keyCode player:player];
|
||||
if (keyCode != (CGKeyCode)-1)
|
||||
{
|
||||
[((AppDelegate *) NSApp.delegate) setButtonCode:buttonCode forKeyCode:keyCode player:player];
|
||||
}
|
||||
else
|
||||
{
|
||||
[((AppDelegate *) NSApp.delegate) clearButton:buttonCode forPlayer:player];
|
||||
}
|
||||
|
||||
[NSUserDefaults.standardUserDefaults synchronize];
|
||||
|
||||
[self refresh];
|
||||
}
|
||||
else if ( [keyPath isEqualToString:@"joypadInput"])
|
||||
{
|
||||
S9xButtonConfigTextField *field = (S9xButtonConfigTextField *)object;
|
||||
S9xButtonCode buttonCode = (S9xButtonCode)field.tag;
|
||||
S9xJoypad *joypad = self.devicePopUp.selectedItem.representedObject;
|
||||
|
||||
if ([joypad isKindOfClass:[S9xJoypad class]])
|
||||
{
|
||||
S9xJoypadInput *input = field.joypadInput;
|
||||
|
||||
if (input != nil)
|
||||
{
|
||||
[((AppDelegate *)NSApp.delegate) setButton:buttonCode forVendorID:joypad.vendorID productID:joypad.productID index:joypad.index cookie:input.cookie value:input.value];
|
||||
}
|
||||
else
|
||||
{
|
||||
[((AppDelegate *)NSApp.delegate) clearJoypadForVendorID:joypad.vendorID productID:joypad.productID index:joypad.index buttonCode:buttonCode];
|
||||
}
|
||||
}
|
||||
|
||||
[NSUserDefaults.standardUserDefaults synchronize];
|
||||
[self refresh];
|
||||
}
|
||||
}
|
||||
|
||||
- (IBAction)playerDropdownChanged:(NSPopUpButton *)sender
|
||||
{
|
||||
[self selectDeviceForPlayer:sender.selectedTag];
|
||||
[self refresh];
|
||||
}
|
||||
|
||||
- (IBAction)deviceDropdownChanged:(NSPopUpButton *)sender
|
||||
{
|
||||
if (sender.selectedTag >= 0)
|
||||
{
|
||||
AppDelegate *appDelegate = (AppDelegate *)NSApp.delegate;
|
||||
S9xJoypad *joypad = sender.selectedItem.representedObject;
|
||||
[appDelegate setPlayer:self.playerPopUp.selectedTag forVendorID:joypad.vendorID productID:joypad.productID index:joypad.index];
|
||||
[NSUserDefaults.standardUserDefaults synchronize];
|
||||
}
|
||||
|
||||
[self refresh];
|
||||
}
|
||||
|
||||
- (IBAction)showFPS:(NSButton *)sender
|
||||
{
|
||||
AppDelegate *appDelegate = (AppDelegate *)NSApp.delegate;
|
||||
[appDelegate setShowFPS:sender.state == NSOnState];
|
||||
}
|
||||
|
||||
- (IBAction)setVideoMode:(NSPopUpButton *)sender
|
||||
{
|
||||
AppDelegate *appDelegate = (AppDelegate *)NSApp.delegate;
|
||||
[appDelegate setVideoMode:(int)sender.selectedTag];
|
||||
}
|
||||
|
||||
- (BOOL)handleInput:(S9xJoypadInput *)input fromJoypad:(S9xJoypad *)joypad
|
||||
{
|
||||
id firstResponder = self.view.window.firstResponder;
|
||||
|
||||
if ([firstResponder isFieldEditor])
|
||||
{
|
||||
firstResponder = [firstResponder delegate];
|
||||
}
|
||||
|
||||
if ([firstResponder respondsToSelector:@selector(setJoypadInput:)])
|
||||
{
|
||||
S9xJoypad *currentJoypad = self.devicePopUp.selectedItem.representedObject;
|
||||
|
||||
if ([joypad isEqual:currentJoypad])
|
||||
{
|
||||
[firstResponder setJoypadInput:input];
|
||||
return YES;
|
||||
}
|
||||
}
|
||||
|
||||
return NO;
|
||||
}
|
||||
|
||||
@end
|
||||
|
|
|
@ -22,25 +22,28 @@
|
|||
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMinY="YES"/>
|
||||
<subviews>
|
||||
<textField horizontalHuggingPriority="251" verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="rxW-aA-Bai">
|
||||
<rect key="frame" x="18" y="538" width="77" height="16"/>
|
||||
<textFieldCell key="cell" lineBreakMode="clipping" title="Video Mode" id="XoT-95-aIF">
|
||||
<rect key="frame" x="18" y="538" width="191" height="16"/>
|
||||
<textFieldCell key="cell" lineBreakMode="clipping" title="Video Mode (Requires Restart)" id="XoT-95-aIF">
|
||||
<font key="font" usesAppearanceFont="YES"/>
|
||||
<color key="textColor" name="labelColor" catalog="System" colorSpace="catalog"/>
|
||||
<color key="backgroundColor" name="textBackgroundColor" catalog="System" colorSpace="catalog"/>
|
||||
</textFieldCell>
|
||||
</textField>
|
||||
<popUpButton verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="qNi-B0-E49">
|
||||
<rect key="frame" x="99" y="532" width="85" height="25"/>
|
||||
<rect key="frame" x="213" y="532" width="85" height="25"/>
|
||||
<popUpButtonCell key="cell" type="push" title="Blocky" bezelStyle="rounded" alignment="left" lineBreakMode="truncatingTail" state="on" borderStyle="borderAndBezel" imageScaling="proportionallyDown" inset="2" selectedItem="Xg7-hz-RRs" id="IRO-Ia-Tlm">
|
||||
<behavior key="behavior" lightByBackground="YES" lightByGray="YES"/>
|
||||
<font key="font" metaFont="menu"/>
|
||||
<menu key="menu" id="VtI-7w-Yps">
|
||||
<items>
|
||||
<menuItem title="Blocky" state="on" id="Xg7-hz-RRs"/>
|
||||
<menuItem title="Smooth" tag="1" id="ucR-ef-Wfk"/>
|
||||
<menuItem title="Smooth" tag="2" id="ucR-ef-Wfk"/>
|
||||
</items>
|
||||
</menu>
|
||||
</popUpButtonCell>
|
||||
<connections>
|
||||
<action selector="setVideoMode:" target="-2" id="oj8-pk-Kdg"/>
|
||||
</connections>
|
||||
</popUpButton>
|
||||
<popUpButton verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="hfx-Lg-t9P">
|
||||
<rect key="frame" x="18" y="503" width="472" height="25"/>
|
||||
|
@ -60,6 +63,9 @@
|
|||
</items>
|
||||
</menu>
|
||||
</popUpButtonCell>
|
||||
<connections>
|
||||
<action selector="playerDropdownChanged:" target="-2" id="P4K-gg-QUK"/>
|
||||
</connections>
|
||||
</popUpButton>
|
||||
<textField horizontalHuggingPriority="251" verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="qMp-14-9zW">
|
||||
<rect key="frame" x="18" y="443" width="88" height="16"/>
|
||||
|
@ -211,6 +217,9 @@
|
|||
<behavior key="behavior" changeContents="YES" doesNotDimImage="YES" lightByContents="YES"/>
|
||||
<font key="font" metaFont="system"/>
|
||||
</buttonCell>
|
||||
<connections>
|
||||
<action selector="showFPS:" target="-2" id="CM2-bf-aFb"/>
|
||||
</connections>
|
||||
</button>
|
||||
<textField horizontalHuggingPriority="251" verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="TuE-fA-Shh">
|
||||
<rect key="frame" x="230" y="353" width="151" height="16"/>
|
||||
|
@ -311,6 +320,9 @@
|
|||
</items>
|
||||
</menu>
|
||||
</popUpButtonCell>
|
||||
<connections>
|
||||
<action selector="deviceDropdownChanged:" target="-2" id="oH3-xl-IZF"/>
|
||||
</connections>
|
||||
</popUpButton>
|
||||
<searchField wantsLayer="YES" verticalHuggingPriority="750" tag="1" textCompletion="NO" translatesAutoresizingMaskIntoConstraints="NO" id="5wn-WM-n7f" customClass="S9xButtonConfigTextField">
|
||||
<rect key="frame" x="112" y="410" width="100" height="22"/>
|
||||
|
|
|
@ -22,12 +22,117 @@
|
|||
#ifndef _mac_joypad_h_
|
||||
#define _mac_joypad_h_
|
||||
|
||||
#include <string>
|
||||
#include <unordered_map>
|
||||
#include <unordered_set>
|
||||
|
||||
#include "mac-controls.h"
|
||||
|
||||
struct JoypadDevice {
|
||||
uint16 vendorID;
|
||||
uint16 productID;
|
||||
uint32 index;
|
||||
|
||||
bool operator==(const struct JoypadDevice &o) const
|
||||
{
|
||||
return vendorID == o.vendorID && productID == o.productID && index == o.index;
|
||||
}
|
||||
|
||||
bool operator<(const struct JoypadDevice &o) const
|
||||
{
|
||||
return vendorID < o.vendorID || productID < o.productID || index < o.index;
|
||||
}
|
||||
};
|
||||
|
||||
struct JoypadCookie {
|
||||
struct JoypadDevice device;
|
||||
uint32 cookie;
|
||||
|
||||
JoypadCookie() {}
|
||||
|
||||
struct JoypadCookie &operator=(const struct JoypadCookie &o)
|
||||
{
|
||||
device = o.device;
|
||||
cookie = o.cookie;
|
||||
return *this;
|
||||
}
|
||||
|
||||
bool operator==(const struct JoypadCookie &o) const
|
||||
{
|
||||
return device == o.device && cookie == o.cookie;
|
||||
}
|
||||
|
||||
bool operator<(const struct JoypadCookie &o) const
|
||||
{
|
||||
return device < o.device || cookie < o.cookie;
|
||||
}
|
||||
};
|
||||
|
||||
struct JoypadCookieInfo {
|
||||
uint32 usage;
|
||||
uint32 index;
|
||||
int32 midpoint;
|
||||
int32 min;
|
||||
int32 max;
|
||||
};
|
||||
|
||||
struct JoypadInput {
|
||||
struct JoypadCookie cookie;
|
||||
int32 value;
|
||||
|
||||
bool operator==(const struct JoypadInput &o) const
|
||||
{
|
||||
return cookie == o.cookie && value == o.value;
|
||||
}
|
||||
|
||||
bool operator<(const struct JoypadInput &o) const
|
||||
{
|
||||
return cookie < o.cookie || value < o.value;
|
||||
}
|
||||
};
|
||||
|
||||
namespace std {
|
||||
template <>
|
||||
struct hash<struct JoypadDevice>
|
||||
{
|
||||
std::size_t operator()(const JoypadDevice& k) const
|
||||
{
|
||||
return k.vendorID ^ k.productID ^ k.index;
|
||||
}
|
||||
};
|
||||
|
||||
template <>
|
||||
struct hash<struct JoypadCookie>
|
||||
{
|
||||
std::size_t operator()(const JoypadCookie& k) const
|
||||
{
|
||||
return std::hash<struct JoypadDevice>()(k.device) ^ k.cookie;
|
||||
}
|
||||
};
|
||||
|
||||
template <>
|
||||
struct hash<struct JoypadInput>
|
||||
{
|
||||
std::size_t operator()(const JoypadInput& k) const
|
||||
{
|
||||
return std::hash<struct JoypadCookie>()(k.cookie) ^ k.value;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
void SetUpHID (void);
|
||||
void ReleaseHID (void);
|
||||
|
||||
void SetPlayerForJoypad(int8 playerNum, uint32 vendorID, uint32 productID, uint8 index, int8 *oldPlayerNum);
|
||||
void SetButtonCodeForJoypadControl(uint32 vendorID, uint32 productID, uint8 index, uint32 cookie, int32 value, S9xButtonCode buttonCode, bool overwrite, S9xButtonCode *oldButtonCode);
|
||||
std::unordered_set<struct JoypadDevice> ListJoypads (void);
|
||||
std::string NameForDevice(struct JoypadDevice device);
|
||||
|
||||
void SetPlayerForJoypad(int8 playerNum, uint32 vendorID, uint32 productID, uint32 index, int8 *oldPlayerNum);
|
||||
bool SetButtonCodeForJoypadControl(uint32 vendorID, uint32 productID, uint32 index, uint32 cookie, int32 value, S9xButtonCode buttonCode, bool overwrite, S9xButtonCode *oldButtonCode);
|
||||
void ClearButtonCodeForJoypad(uint32 vendorID, uint32 productID, uint32 index, S9xButtonCode buttonCode);
|
||||
|
||||
void ClearJoypad(uint32 vendorID, uint32 productID, uint32 index);
|
||||
std::unordered_map<struct JoypadInput, S9xButtonCode> GetJuypadButtons(uint32 vendorID, uint32 productID, uint32 index);
|
||||
|
||||
std::string LabelForInput(uint32 vendorID, uint32 productID, uint32 cookie, int32 value);
|
||||
|
||||
#endif
|
||||
|
|
|
@ -20,8 +20,6 @@
|
|||
|
||||
|
||||
#include <map>
|
||||
#include <unordered_map>
|
||||
#include <unordered_set>
|
||||
|
||||
#include "port.h"
|
||||
|
||||
|
@ -60,107 +58,15 @@
|
|||
typedef hu_device_t *pRecDevice;
|
||||
typedef hu_element_t *pRecElement;
|
||||
|
||||
struct JoypadDevice {
|
||||
uint16 vendorID;
|
||||
uint16 productID;
|
||||
uint32 index;
|
||||
|
||||
bool operator==(const struct JoypadDevice &o) const
|
||||
{
|
||||
return vendorID == o.vendorID && productID == o.productID && index == o.index;
|
||||
}
|
||||
|
||||
bool operator<(const struct JoypadDevice &o) const
|
||||
{
|
||||
return vendorID < o.vendorID || productID < o.productID || index < o.index;
|
||||
}
|
||||
};
|
||||
|
||||
struct JoypadCookie {
|
||||
struct JoypadDevice device;
|
||||
uint32 cookie;
|
||||
|
||||
JoypadCookie() {}
|
||||
|
||||
struct JoypadCookie &operator=(const struct JoypadCookie &o)
|
||||
{
|
||||
device = o.device;
|
||||
cookie = o.cookie;
|
||||
return *this;
|
||||
}
|
||||
|
||||
bool operator==(const struct JoypadCookie &o) const
|
||||
{
|
||||
return device == o.device && cookie == o.cookie;
|
||||
}
|
||||
|
||||
bool operator<(const struct JoypadCookie &o) const
|
||||
{
|
||||
return device < o.device || cookie < o.cookie;
|
||||
}
|
||||
};
|
||||
|
||||
struct JoypadCookieInfo {
|
||||
uint32 usage;
|
||||
uint32 index;
|
||||
int32 midpoint;
|
||||
int32 min;
|
||||
int32 max;
|
||||
};
|
||||
|
||||
struct JoypadInput {
|
||||
struct JoypadCookie cookie;
|
||||
int32 value;
|
||||
|
||||
bool operator==(const struct JoypadInput &o) const
|
||||
{
|
||||
return cookie == o.cookie && value == o.value;
|
||||
}
|
||||
|
||||
bool operator<(const struct JoypadInput &o) const
|
||||
{
|
||||
return cookie < o.cookie || value < o.value;
|
||||
}
|
||||
};
|
||||
|
||||
namespace std {
|
||||
template <>
|
||||
struct hash<struct JoypadDevice>
|
||||
{
|
||||
std::size_t operator()(const JoypadDevice& k) const
|
||||
{
|
||||
return k.vendorID ^ k.productID ^ k.index;
|
||||
}
|
||||
};
|
||||
|
||||
template <>
|
||||
struct hash<struct JoypadCookie>
|
||||
{
|
||||
std::size_t operator()(const JoypadCookie& k) const
|
||||
{
|
||||
return std::hash<struct JoypadDevice>()(k.device) ^ k.cookie;
|
||||
}
|
||||
};
|
||||
|
||||
template <>
|
||||
struct hash<struct JoypadInput>
|
||||
{
|
||||
std::size_t operator()(const JoypadInput& k) const
|
||||
{
|
||||
return std::hash<struct JoypadCookie>()(k.cookie) ^ k.value;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
std::unordered_set<JoypadDevice> allDevices;
|
||||
std::unordered_map<JoypadDevice, std::map<uint8, std::map<int8, S9xButtonCode>>> defaultAxes;
|
||||
std::unordered_map<JoypadDevice, std::map<uint8, S9xButtonCode>> defaultButtons;
|
||||
std::unordered_map<JoypadDevice, std::map<uint8, S9xButtonCode>> defaultHatValues;
|
||||
// TODO: Hook these next two up
|
||||
std::unordered_map<JoypadDevice, int8> playerNumByDevice;
|
||||
std::unordered_map<uint32, int8> deviceIndexByPort;
|
||||
std::unordered_map<JoypadCookie, JoypadCookieInfo> infoByCookie;
|
||||
std::unordered_map<JoypadInput, S9xButtonCode> buttonCodeByJoypadInput;
|
||||
std::unordered_map<JoypadDevice, std::string> namesByDevice;
|
||||
|
||||
@interface NSData (S9xHexString)
|
||||
+(id)s9x_dataWithHexString:(NSString *)hex;
|
||||
|
@ -191,6 +97,20 @@ std::unordered_map<JoypadInput, S9xButtonCode> buttonCodeByJoypadInput;
|
|||
|
||||
IOHIDManagerRef hidManager = NULL;
|
||||
|
||||
std::unordered_set<struct JoypadDevice> ListJoypads (void) {
|
||||
return allDevices;
|
||||
}
|
||||
|
||||
std::string NameForDevice(struct JoypadDevice device) {
|
||||
auto it = namesByDevice.find(device);
|
||||
if (it != namesByDevice.end())
|
||||
{
|
||||
return it->second;
|
||||
}
|
||||
|
||||
return "";
|
||||
}
|
||||
|
||||
void gamepadAction(void *inContext, IOReturn inResult, void *inSender, IOHIDValueRef v) {
|
||||
os_unfair_lock_lock(&keyLock);
|
||||
|
||||
|
@ -244,6 +164,22 @@ void gamepadAction(void *inContext, IOReturn inResult, void *inSender, IOHIDValu
|
|||
inputStruct.cookie = cookieStruct;
|
||||
inputStruct.value = (int32_t)IOHIDValueGetIntegerValue(v);
|
||||
|
||||
S9xJoypad *objcJoypad = [S9xJoypad new];
|
||||
objcJoypad.vendorID = deviceStruct.vendorID;
|
||||
objcJoypad.productID = deviceStruct.productID;
|
||||
objcJoypad.index = deviceStruct.index;
|
||||
|
||||
S9xJoypadInput *objcInput = [S9xJoypadInput new];
|
||||
objcInput.cookie = inputStruct.cookie.cookie;
|
||||
objcInput.value =inputStruct.value;
|
||||
|
||||
os_unfair_lock_unlock(&keyLock);
|
||||
if ([inputDelegate handleInput:objcInput fromJoypad:objcJoypad])
|
||||
{
|
||||
return;
|
||||
}
|
||||
os_unfair_lock_lock(&keyLock);
|
||||
|
||||
struct JoypadInput oppositeInputStruct = inputStruct;
|
||||
|
||||
if (info.min != info.max)
|
||||
|
@ -552,6 +488,7 @@ void AddDevice (IOHIDDeviceRef device)
|
|||
{
|
||||
NSNumber *vendor = (NSNumber *)IOHIDDeviceGetProperty(device, CFSTR(kIOHIDVendorIDKey));
|
||||
NSNumber *product = (NSNumber *)IOHIDDeviceGetProperty(device, CFSTR(kIOHIDProductIDKey));
|
||||
NSString *name = (NSString *)IOHIDDeviceGetProperty(device, CFSTR(kIOHIDProductKey));
|
||||
|
||||
NSMutableArray<NSDictionary *> *buttons = [NSMutableArray new];
|
||||
NSMutableArray<NSDictionary *> *axes = [NSMutableArray new];
|
||||
|
@ -570,6 +507,14 @@ void AddDevice (IOHIDDeviceRef device)
|
|||
}
|
||||
|
||||
allDevices.insert(deviceStruct);
|
||||
std::string s = std::string(name.UTF8String);
|
||||
|
||||
if (deviceStruct.index > 0)
|
||||
{
|
||||
s = s + " (" + std::to_string(deviceStruct.index + 1) + ")";
|
||||
}
|
||||
|
||||
namesByDevice[deviceStruct] = s;
|
||||
uint32_t port = ((NSNumber *)IOHIDDeviceGetProperty(device, CFSTR(kIOHIDLocationIDKey))).unsignedIntValue;
|
||||
deviceIndexByPort[port] = deviceStruct.index;
|
||||
|
||||
|
@ -662,7 +607,7 @@ void AddDevice (IOHIDDeviceRef device)
|
|||
}
|
||||
}
|
||||
|
||||
// TODO: Extend axisIndex into defaultAxes
|
||||
info.usage = axisDict[@kIOHIDElementUsageKey].intValue;
|
||||
info.index = axisIndex++;
|
||||
infoByCookie[cookie] = info;
|
||||
}
|
||||
|
@ -698,6 +643,46 @@ void AddDevice (IOHIDDeviceRef device)
|
|||
CFRelease(properties);
|
||||
}
|
||||
|
||||
void ClearJoypad(uint32 vendorID, uint32 productID, uint32 index)
|
||||
{
|
||||
struct JoypadDevice device;
|
||||
device.vendorID = vendorID;
|
||||
device.productID = productID;
|
||||
device.index = index;
|
||||
|
||||
for (auto it = buttonCodeByJoypadInput.begin(); it != buttonCodeByJoypadInput.end();)
|
||||
{
|
||||
if (it->first.cookie.device == device)
|
||||
{
|
||||
buttonCodeByJoypadInput.erase(it++);
|
||||
}
|
||||
else
|
||||
{
|
||||
++it;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
std::unordered_map<struct JoypadInput, S9xButtonCode> GetJuypadButtons(uint32 vendorID, uint32 productID, uint32 index)
|
||||
{
|
||||
struct JoypadDevice device;
|
||||
device.vendorID = vendorID;
|
||||
device.productID = productID;
|
||||
device.index = index;
|
||||
|
||||
std::unordered_map<struct JoypadInput, S9xButtonCode> joypadButtons;
|
||||
|
||||
for (auto it = buttonCodeByJoypadInput.begin(); it != buttonCodeByJoypadInput.end(); ++it)
|
||||
{
|
||||
if ( it->first.cookie.device == device)
|
||||
{
|
||||
joypadButtons[it->first] = it->second;
|
||||
}
|
||||
}
|
||||
|
||||
return joypadButtons;
|
||||
}
|
||||
|
||||
void SetUpHID (void)
|
||||
{
|
||||
hidManager = IOHIDManagerCreate(kCFAllocatorDefault, kIOHIDOptionsTypeNone);
|
||||
|
@ -776,7 +761,7 @@ void ReleaseHID (void)
|
|||
}
|
||||
}
|
||||
|
||||
void SetPlayerForJoypad(int8 playerNum, uint32 vendorID, uint32 productID, uint8 index, int8 *oldPlayerNum)
|
||||
void SetPlayerForJoypad(int8 playerNum, uint32 vendorID, uint32 productID, uint32 index, int8 *oldPlayerNum)
|
||||
{
|
||||
struct JoypadDevice device;
|
||||
device.vendorID = vendorID;
|
||||
|
@ -796,8 +781,13 @@ void SetPlayerForJoypad(int8 playerNum, uint32 vendorID, uint32 productID, uint8
|
|||
playerNumByDevice[device] = playerNum;
|
||||
}
|
||||
|
||||
void SetButtonCodeForJoypadControl(uint32 vendorID, uint32 productID, uint8 index, uint32 cookie, int32 value, S9xButtonCode buttonCode, bool overwrite, S9xButtonCode *oldButtonCode)
|
||||
bool SetButtonCodeForJoypadControl(uint32 vendorID, uint32 productID, uint32 index, uint32 cookie, int32 value, S9xButtonCode buttonCode, bool overwrite, S9xButtonCode *oldButtonCode)
|
||||
{
|
||||
if (buttonCode < 0 || buttonCode >= kNumButtons)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (oldButtonCode != NULL)
|
||||
{
|
||||
*oldButtonCode = (S9xButtonCode)-1;
|
||||
|
@ -818,11 +808,11 @@ void SetButtonCodeForJoypadControl(uint32 vendorID, uint32 productID, uint8 inde
|
|||
|
||||
if ( info.min != info.max )
|
||||
{
|
||||
if (value < info.min)
|
||||
if (value <= info.min)
|
||||
{
|
||||
value = info.min;
|
||||
}
|
||||
else if (value > info.max)
|
||||
else if (value >= info.max)
|
||||
{
|
||||
value = info.max;
|
||||
}
|
||||
|
@ -846,8 +836,195 @@ void SetButtonCodeForJoypadControl(uint32 vendorID, uint32 productID, uint8 inde
|
|||
*oldButtonCode = buttonCodeByJoypadInput[input];
|
||||
}
|
||||
|
||||
for (auto it = buttonCodeByJoypadInput.begin(); it != buttonCodeByJoypadInput.end();)
|
||||
{
|
||||
if (it->second == buttonCode && it->first.cookie.device == device)
|
||||
{
|
||||
if (overwrite)
|
||||
{
|
||||
buttonCodeByJoypadInput.erase(it++);
|
||||
}
|
||||
else
|
||||
{
|
||||
++it;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
++it;
|
||||
}
|
||||
}
|
||||
|
||||
if (overwrite)
|
||||
{
|
||||
buttonCodeByJoypadInput[input] = buttonCode;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
void ClearButtonCodeForJoypad(uint32 vendorID, uint32 productID, uint32 index, S9xButtonCode buttonCode)
|
||||
{
|
||||
struct JoypadDevice device;
|
||||
device.vendorID = vendorID;
|
||||
device.productID = productID;
|
||||
device.index = index;
|
||||
|
||||
for (auto it = buttonCodeByJoypadInput.begin(); it != buttonCodeByJoypadInput.end();)
|
||||
{
|
||||
if (it->first.cookie.device == device && it->second == buttonCode)
|
||||
{
|
||||
buttonCodeByJoypadInput.erase(it++);
|
||||
}
|
||||
else
|
||||
{
|
||||
++it;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
std::string LabelForInput(uint32 vendorID, uint32 productID, uint32 cookie, int32 value)
|
||||
{
|
||||
struct JoypadDevice deviceStruct;
|
||||
deviceStruct.productID = productID;
|
||||
deviceStruct.vendorID = vendorID;
|
||||
deviceStruct.index = 0;
|
||||
|
||||
struct JoypadCookie cookieStruct;
|
||||
cookieStruct.device = deviceStruct;
|
||||
cookieStruct.cookie = cookie;
|
||||
|
||||
auto it = infoByCookie.find(cookieStruct);
|
||||
if (it != infoByCookie.end())
|
||||
{
|
||||
auto info = it->second;
|
||||
switch(info.usage)
|
||||
{
|
||||
case kHIDUsage_GD_X:
|
||||
{
|
||||
if (value <= info.min)
|
||||
{
|
||||
return "X-";
|
||||
}
|
||||
else if (value >= info.max)
|
||||
{
|
||||
return "X+";
|
||||
}
|
||||
}
|
||||
|
||||
case kHIDUsage_GD_Y:
|
||||
{
|
||||
if (value <= info.min)
|
||||
{
|
||||
return "Y-";
|
||||
}
|
||||
else if (value >= info.max)
|
||||
{
|
||||
return "Y+";
|
||||
}
|
||||
}
|
||||
|
||||
case kHIDUsage_GD_Z:
|
||||
{
|
||||
if (value <= info.min)
|
||||
{
|
||||
return "Z-";
|
||||
}
|
||||
else if (value >= info.max)
|
||||
{
|
||||
return "Z+";
|
||||
}
|
||||
}
|
||||
|
||||
case kHIDUsage_GD_Rx:
|
||||
{
|
||||
if (value <= info.min)
|
||||
{
|
||||
return "Right X-";
|
||||
}
|
||||
else if (value >= info.max)
|
||||
{
|
||||
return "Right X+";
|
||||
}
|
||||
}
|
||||
|
||||
case kHIDUsage_GD_Ry:
|
||||
{
|
||||
if (value <= info.min)
|
||||
{
|
||||
return "Right Y-";
|
||||
}
|
||||
else if (value >= info.max)
|
||||
{
|
||||
return "Right Y+";
|
||||
}
|
||||
}
|
||||
|
||||
case kHIDUsage_GD_Rz:
|
||||
{
|
||||
if (value <= info.min)
|
||||
{
|
||||
return "Right Z-";
|
||||
}
|
||||
else if (value >= info.max)
|
||||
{
|
||||
return "Right Z+";
|
||||
}
|
||||
}
|
||||
|
||||
case kHIDUsage_GD_Hatswitch:
|
||||
{
|
||||
auto defaultIT = defaultHatValues.find(deviceStruct);
|
||||
if (defaultIT != defaultHatValues.end())
|
||||
{
|
||||
auto hatDict = defaultIT->second;
|
||||
auto hatIT = hatDict.find(value);
|
||||
if ( hatIT != hatDict.end())
|
||||
{
|
||||
switch (hatIT->second)
|
||||
{
|
||||
case kUp:
|
||||
return "D-Pad Up";
|
||||
|
||||
case kDown:
|
||||
return "D-Pad Down";
|
||||
|
||||
case kLeft:
|
||||
return "D-Pad Left";
|
||||
|
||||
case kRight:
|
||||
return "D-Pad Right";
|
||||
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (value == 1)
|
||||
{
|
||||
return "D-Pad Up";
|
||||
}
|
||||
else if (value == 2)
|
||||
{
|
||||
return "D-Pad Right";
|
||||
}
|
||||
else if (value == 4)
|
||||
{
|
||||
return "D-Pad Down";
|
||||
}
|
||||
else if (value == 8)
|
||||
{
|
||||
return "D-Pad Left";
|
||||
}
|
||||
}
|
||||
|
||||
default:
|
||||
{
|
||||
return std::string("Button " + std::to_string(info.index));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return std::to_string(cookie);
|
||||
}
|
||||
|
|
|
@ -37,5 +37,6 @@ void InitKeyboard (void);
|
|||
void DeinitKeyboard (void);
|
||||
|
||||
bool SetKeyCode(int16 keyCode, S9xButtonCode button, int8 player, int16 *oldKeyCode, S9xButtonCode *oldButton, int8 *oldPlayer);
|
||||
void ClearKeyCode(S9xButtonCode buttonCode, int8 player);
|
||||
|
||||
#endif
|
||||
|
|
|
@ -81,3 +81,20 @@ bool SetKeyCode(int16 keyCode, S9xButtonCode buttonCode, int8 player, int16 *old
|
|||
|
||||
return true;
|
||||
}
|
||||
|
||||
void ClearKeyCode(S9xButtonCode buttonCode, int8 player)
|
||||
{
|
||||
if (player < 0 || player >= MAC_MAX_PLAYERS || buttonCode < 0 || buttonCode >= kNumButtons)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
for ( int i = 0; i < MAC_NUM_KEYCODES; ++i)
|
||||
{
|
||||
struct S9xButton button = keyCodes[i];
|
||||
if (button.player == player && button.buttonCode == buttonCode)
|
||||
{
|
||||
keyCodes[i] = { -1, -1 };
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -181,8 +181,29 @@ uint64 GetMicroseconds(void);
|
|||
|
||||
void CopyPressedKeys(uint8 keys[MAC_MAX_PLAYERS][kNumButtons], uint8 gamepadButtons[MAC_MAX_PLAYERS][kNumButtons]);
|
||||
|
||||
@interface S9xJoypad : NSObject
|
||||
@property (nonatomic, assign) uint32 vendorID;
|
||||
@property (nonatomic, assign) uint32 productID;
|
||||
@property (nonatomic, assign) uint8 index;
|
||||
@property (nonatomic, copy) NSString *name;
|
||||
@end
|
||||
|
||||
@interface S9xJoypadInput : NSObject
|
||||
@property (nonatomic, assign) uint32 cookie;
|
||||
@property (nonatomic, assign) int32 value;
|
||||
@property (nonatomic, assign) S9xButtonCode buttonCode;
|
||||
@end
|
||||
|
||||
@protocol S9xInputDelegate <NSObject>
|
||||
- (BOOL)handleInput:(S9xJoypadInput *)input fromJoypad:(S9xJoypad *)joypad;
|
||||
@end
|
||||
|
||||
extern id<S9xInputDelegate> inputDelegate;
|
||||
|
||||
@interface S9xEngine : NSObject
|
||||
|
||||
@property (nonatomic, weak) id<S9xInputDelegate> inputDelegate;
|
||||
|
||||
- (void)start;
|
||||
- (void)stop;
|
||||
|
||||
|
@ -192,9 +213,22 @@ void CopyPressedKeys(uint8 keys[MAC_MAX_PLAYERS][kNumButtons], uint8 gamepadButt
|
|||
- (void)resume;
|
||||
|
||||
- (BOOL)setButton:(S9xButtonCode)button forKey:(int16)key player:(int8)player oldButton:(S9xButtonCode *)oldButton oldPlayer:(int8 *)oldPlayer oldKey:(int16 *)oldKey;
|
||||
- (void)clearButton:(S9xButtonCode)button forPlayer:(int8)player;
|
||||
|
||||
- (NSArray<S9xJoypad *> *)listJoypads;
|
||||
- (void)setPlayer:(int8)player forVendorID:(uint32)vendorID productID:(uint32)productID index:(uint32)index oldPlayer:(int8 *)oldPlayer;
|
||||
- (BOOL)setButton:(S9xButtonCode)button forVendorID:(uint32)vendorID productID:(uint32)productID index:(uint32)index cookie:(uint32)cookie value:(int32)value oldButton:(S9xButtonCode *)oldButton;
|
||||
- (void)clearJoypadForVendorID:(uint32)vendorID productID:(uint32)productID index:(uint32)index;
|
||||
- (void)clearJoypadForVendorID:(uint32)vendorID productID:(uint32)productID index:(uint32)index buttonCode:(S9xButtonCode)buttonCode;
|
||||
- (NSArray<S9xJoypadInput *> *)getInputsForVendorID:(uint32)vendorID productID:(uint32)productID index:(uint32)index;
|
||||
|
||||
- (NSString *)labelForVendorID:(uint32)vendorID productID:(uint32)productID cookie:(uint32)cookie value:(int32)value;
|
||||
|
||||
- (BOOL)loadROM:(NSURL *)fileURL;
|
||||
|
||||
- (void)setVideoMode:(int)videoMode;
|
||||
- (void)setShowFPS:(BOOL)showFPS;
|
||||
|
||||
@end
|
||||
|
||||
#endif
|
||||
|
|
147
macosx/mac-os.mm
147
macosx/mac-os.mm
|
@ -174,6 +174,8 @@ CFStringRef multiCartPath[2];
|
|||
IconRef macIconRef[118];
|
||||
#endif
|
||||
|
||||
id<S9xInputDelegate> inputDelegate = nil;
|
||||
|
||||
typedef enum
|
||||
{
|
||||
ToggleBG0,
|
||||
|
@ -2076,7 +2078,7 @@ static void ProcessInput (void)
|
|||
bool8 keys[MAC_MAX_PLAYERS][kNumButtons];
|
||||
bool8 gamepadButtons[MAC_MAX_PLAYERS][kNumButtons];
|
||||
bool8 isok, fnbtn, altbtn, tcbtn;
|
||||
static bool8 toggleff = false, lastTimeTT = false, lastTimeFn = false, ffUp = false, ffDown = false, ffUpSp = false, ffDownSp = false;
|
||||
static bool8 toggleff = false, lastTimeTT = false, lastTimeFn = false, ffUp = false, ffDown = false;
|
||||
|
||||
if (rejectinput)
|
||||
return;
|
||||
|
@ -3018,6 +3020,101 @@ void QuitWithFatalError ( NSString *message)
|
|||
pauseEmulation = false;
|
||||
}
|
||||
|
||||
- (NSArray<S9xJoypad *> *)listJoypads
|
||||
{
|
||||
os_unfair_lock_lock(&keyLock);
|
||||
NSMutableArray<S9xJoypad *> *joypads = [NSMutableArray new];
|
||||
for (auto joypadStruct : ListJoypads())
|
||||
{
|
||||
S9xJoypad *joypad = [S9xJoypad new];
|
||||
joypad.vendorID = joypadStruct.vendorID;
|
||||
joypad.productID = joypadStruct.productID;
|
||||
joypad.index = joypadStruct.index;
|
||||
joypad.name = [[NSString alloc] initWithUTF8String:NameForDevice(joypadStruct).c_str()];
|
||||
|
||||
[joypads addObject:joypad];
|
||||
}
|
||||
|
||||
[joypads sortUsingComparator:^NSComparisonResult(S9xJoypad *a, S9xJoypad *b)
|
||||
{
|
||||
NSComparisonResult result = [a.name compare:b.name];
|
||||
|
||||
if ( result == NSOrderedSame )
|
||||
{
|
||||
result = [@(a.vendorID) compare:@(b.vendorID)];
|
||||
}
|
||||
|
||||
if ( result == NSOrderedSame )
|
||||
{
|
||||
result = [@(a.productID) compare:@(b.productID)];
|
||||
}
|
||||
|
||||
if ( result == NSOrderedSame )
|
||||
{
|
||||
result = [@(a.index) compare:@(b.index)];
|
||||
}
|
||||
|
||||
return result;
|
||||
}];
|
||||
os_unfair_lock_unlock(&keyLock);
|
||||
|
||||
return joypads;
|
||||
}
|
||||
|
||||
- (void)setPlayer:(int8)player forVendorID:(uint32)vendorID productID:(uint32)productID index:(uint32)index oldPlayer:(int8 *)oldPlayer
|
||||
{
|
||||
os_unfair_lock_lock(&keyLock);
|
||||
SetPlayerForJoypad(player, vendorID, productID, index, oldPlayer);
|
||||
os_unfair_lock_unlock(&keyLock);
|
||||
}
|
||||
|
||||
- (BOOL)setButton:(S9xButtonCode)button forVendorID:(uint32)vendorID productID:(uint32)productID index:(uint32)index cookie:(uint32)cookie value:(int32)value oldButton:(S9xButtonCode *)oldButton
|
||||
{
|
||||
BOOL result = NO;
|
||||
os_unfair_lock_lock(&keyLock);
|
||||
result = SetButtonCodeForJoypadControl(vendorID, productID, index, cookie, value, button, true, oldButton);
|
||||
os_unfair_lock_unlock(&keyLock);
|
||||
return result;
|
||||
}
|
||||
|
||||
- (void)clearJoypadForVendorID:(uint32)vendorID productID:(uint32)productID index:(uint32)index
|
||||
{
|
||||
os_unfair_lock_lock(&keyLock);
|
||||
ClearJoypad(vendorID, productID, index);
|
||||
os_unfair_lock_unlock(&keyLock);
|
||||
}
|
||||
|
||||
- (void)clearJoypadForVendorID:(uint32)vendorID productID:(uint32)productID index:(uint32)index buttonCode:(S9xButtonCode)buttonCode
|
||||
{
|
||||
os_unfair_lock_lock(&keyLock);
|
||||
ClearButtonCodeForJoypad(vendorID, productID, index, buttonCode);
|
||||
os_unfair_lock_unlock(&keyLock);
|
||||
}
|
||||
|
||||
- (NSArray<S9xJoypadInput *> *)getInputsForVendorID:(uint32)vendorID productID:(uint32)productID index:(uint32)index
|
||||
{
|
||||
os_unfair_lock_lock(&keyLock);
|
||||
NSMutableArray<S9xJoypadInput *> *inputs = [NSMutableArray new];
|
||||
std::unordered_map<struct JoypadInput, S9xButtonCode> buttonCodeMap = GetJuypadButtons(vendorID, productID, index);
|
||||
for (auto it = buttonCodeMap.begin(); it != buttonCodeMap.end(); ++it)
|
||||
{
|
||||
S9xJoypadInput *input = [S9xJoypadInput new];
|
||||
input.cookie = it->first.cookie.cookie;
|
||||
input.value = it->first.value;
|
||||
input.buttonCode = it->second;
|
||||
|
||||
[inputs addObject:input];
|
||||
}
|
||||
os_unfair_lock_unlock(&keyLock);
|
||||
|
||||
return inputs;
|
||||
}
|
||||
|
||||
- (NSString *)labelForVendorID:(uint32)vendorID productID:(uint32)productID cookie:(uint32)cookie value:(int32)value
|
||||
{
|
||||
return [NSString stringWithUTF8String:LabelForInput(vendorID, productID, cookie, value).c_str()];
|
||||
}
|
||||
|
||||
- (BOOL)setButton:(S9xButtonCode)button forKey:(int16)key player:(int8)player oldButton:(S9xButtonCode *)oldButton oldPlayer:(int8 *)oldPlayer oldKey:(int16 *)oldKey
|
||||
{
|
||||
BOOL result = NO;
|
||||
|
@ -3027,6 +3124,13 @@ void QuitWithFatalError ( NSString *message)
|
|||
return result;
|
||||
}
|
||||
|
||||
- (void)clearButton:(S9xButtonCode)button forPlayer:(int8)player
|
||||
{
|
||||
os_unfair_lock_lock(&keyLock);
|
||||
ClearKeyCode(button, player);
|
||||
os_unfair_lock_unlock(&keyLock);
|
||||
}
|
||||
|
||||
- (BOOL)loadROM:(NSURL *)fileURL
|
||||
{
|
||||
if ( SNES9X_OpenCart(fileURL) )
|
||||
|
@ -3041,4 +3145,45 @@ void QuitWithFatalError ( NSString *message)
|
|||
return NO;
|
||||
}
|
||||
|
||||
- (void)setShowFPS:(BOOL)showFPS
|
||||
{
|
||||
Settings.DisplayFrameRate = showFPS;
|
||||
}
|
||||
|
||||
- (void)setVideoMode:(int)mode
|
||||
{
|
||||
os_unfair_lock_lock(&renderLock);
|
||||
videoMode = mode;
|
||||
os_unfair_lock_unlock(&renderLock);
|
||||
}
|
||||
|
||||
@dynamic inputDelegate;
|
||||
- (void)setInputDelegate:(id<S9xInputDelegate>)delegate
|
||||
{
|
||||
inputDelegate = delegate;
|
||||
}
|
||||
|
||||
- (id<S9xInputDelegate>)inputDelegate
|
||||
{
|
||||
return inputDelegate;
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
@implementation S9xJoypad
|
||||
|
||||
- (BOOL)isEqual:(id)object
|
||||
{
|
||||
if (![object isKindOfClass:[self class]])
|
||||
{
|
||||
return NO;
|
||||
}
|
||||
|
||||
S9xJoypad *other = (S9xJoypad *)object;
|
||||
return (self.vendorID == other.vendorID && self.productID == other.productID && self.index == other.index);
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
@implementation S9xJoypadInput
|
||||
@end
|
||||
|
|
Loading…
Reference in New Issue