mirror of https://github.com/bsnes-emu/bsnes.git
Added Joypad support, closes #9
This commit is contained in:
parent
d6236b87dd
commit
8eeda02d50
|
@ -0,0 +1,8 @@
|
||||||
|
#import <AppKit/AppKit.h>
|
||||||
|
|
||||||
|
@protocol GBJoystickListener <NSObject>
|
||||||
|
|
||||||
|
- (void) joystick:(NSString *)joystick_name button: (unsigned)button changedState: (bool) state;
|
||||||
|
- (void) joystick:(NSString *)joystick_name axis: (unsigned)axis movedTo: (signed) value;
|
||||||
|
|
||||||
|
@end
|
|
@ -1,9 +1,12 @@
|
||||||
#import <Cocoa/Cocoa.h>
|
#import <Cocoa/Cocoa.h>
|
||||||
|
#import "GBJoystickListener.h"
|
||||||
|
|
||||||
@interface GBPreferencesWindow : NSWindow <NSTableViewDelegate, NSTableViewDataSource>
|
@interface GBPreferencesWindow : NSWindow <NSTableViewDelegate, NSTableViewDataSource, GBJoystickListener>
|
||||||
@property IBOutlet NSTableView *controlsTableView;
|
@property IBOutlet NSTableView *controlsTableView;
|
||||||
@property IBOutlet NSPopUpButton *graphicsFilterPopupButton;
|
@property IBOutlet NSPopUpButton *graphicsFilterPopupButton;
|
||||||
@property (strong) IBOutlet NSButton *aspectRatioCheckbox;
|
@property (strong) IBOutlet NSButton *aspectRatioCheckbox;
|
||||||
@property (strong) IBOutlet NSPopUpButton *highpassFilterPopupButton;
|
@property (strong) IBOutlet NSPopUpButton *highpassFilterPopupButton;
|
||||||
@property (strong) IBOutlet NSPopUpButton *colorCorrectionPopupButton;
|
@property (strong) IBOutlet NSPopUpButton *colorCorrectionPopupButton;
|
||||||
|
@property (strong) IBOutlet NSButton *configureJoypadButton;
|
||||||
|
@property (strong) IBOutlet NSButton *skipButton;
|
||||||
@end
|
@end
|
||||||
|
|
|
@ -7,6 +7,9 @@
|
||||||
{
|
{
|
||||||
bool is_button_being_modified;
|
bool is_button_being_modified;
|
||||||
NSInteger button_being_modified;
|
NSInteger button_being_modified;
|
||||||
|
signed joystick_configuration_state;
|
||||||
|
NSString *joystick_being_configured;
|
||||||
|
signed last_axis;
|
||||||
|
|
||||||
NSPopUpButton *_graphicsFilterPopupButton;
|
NSPopUpButton *_graphicsFilterPopupButton;
|
||||||
NSPopUpButton *_highpassFilterPopupButton;
|
NSPopUpButton *_highpassFilterPopupButton;
|
||||||
|
@ -36,6 +39,15 @@
|
||||||
return filters;
|
return filters;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
- (void)close
|
||||||
|
{
|
||||||
|
joystick_configuration_state = -1;
|
||||||
|
[self.configureJoypadButton setEnabled:YES];
|
||||||
|
[self.skipButton setEnabled:NO];
|
||||||
|
[self.configureJoypadButton setTitle:@"Configure Joypad"];
|
||||||
|
[super close];
|
||||||
|
}
|
||||||
|
|
||||||
- (NSPopUpButton *)graphicsFilterPopupButton
|
- (NSPopUpButton *)graphicsFilterPopupButton
|
||||||
{
|
{
|
||||||
return _graphicsFilterPopupButton;
|
return _graphicsFilterPopupButton;
|
||||||
|
@ -149,6 +161,108 @@
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
- (IBAction) configureJoypad:(id)sender
|
||||||
|
{
|
||||||
|
[self.configureJoypadButton setEnabled:NO];
|
||||||
|
[self.skipButton setEnabled:YES];
|
||||||
|
joystick_being_configured = nil;
|
||||||
|
[self advanceConfigurationStateMachine];
|
||||||
|
last_axis = -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
- (IBAction) skipButton:(id)sender
|
||||||
|
{
|
||||||
|
[self advanceConfigurationStateMachine];
|
||||||
|
}
|
||||||
|
|
||||||
|
- (void) advanceConfigurationStateMachine
|
||||||
|
{
|
||||||
|
joystick_configuration_state++;
|
||||||
|
if (joystick_configuration_state < GBButtonCount) {
|
||||||
|
[self.configureJoypadButton setTitle:[NSString stringWithFormat:@"Press Button for %@", GBButtonNames[joystick_configuration_state]]];
|
||||||
|
}
|
||||||
|
else if (joystick_configuration_state == GBButtonCount) {
|
||||||
|
[self.configureJoypadButton setTitle:@"Move the Analog Stick"];
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
joystick_configuration_state = -1;
|
||||||
|
[self.configureJoypadButton setEnabled:YES];
|
||||||
|
[self.skipButton setEnabled:NO];
|
||||||
|
[self.configureJoypadButton setTitle:@"Configure Joypad"];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
- (void) joystick:(NSString *)joystick_name button: (unsigned)button changedState: (bool) state
|
||||||
|
{
|
||||||
|
if (!state) return;
|
||||||
|
if (joystick_configuration_state == -1) return;
|
||||||
|
if (joystick_configuration_state == GBButtonCount) return;
|
||||||
|
if (!joystick_being_configured) {
|
||||||
|
joystick_being_configured = joystick_name;
|
||||||
|
}
|
||||||
|
else if (![joystick_being_configured isEqualToString:joystick_name]) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
NSMutableDictionary *all_mappings = [[[NSUserDefaults standardUserDefaults] dictionaryForKey:@"GBJoypadMappings"] mutableCopy];
|
||||||
|
|
||||||
|
if (!all_mappings) {
|
||||||
|
all_mappings = [[NSMutableDictionary alloc] init];
|
||||||
|
}
|
||||||
|
|
||||||
|
NSMutableDictionary *mapping = [[all_mappings objectForKey:joystick_name] mutableCopy];
|
||||||
|
|
||||||
|
if (!mapping) {
|
||||||
|
mapping = [[NSMutableDictionary alloc] init];
|
||||||
|
}
|
||||||
|
|
||||||
|
mapping[GBButtonNames[joystick_configuration_state]] = @(button);
|
||||||
|
|
||||||
|
all_mappings[joystick_name] = mapping;
|
||||||
|
[[NSUserDefaults standardUserDefaults] setObject:all_mappings forKey:@"GBJoypadMappings"];
|
||||||
|
[self advanceConfigurationStateMachine];
|
||||||
|
}
|
||||||
|
|
||||||
|
- (void) joystick:(NSString *)joystick_name axis: (unsigned)axis movedTo: (signed) value
|
||||||
|
{
|
||||||
|
if (abs(value) < 0x4000) return;
|
||||||
|
if (joystick_configuration_state != GBButtonCount) return;
|
||||||
|
if (!joystick_being_configured) {
|
||||||
|
joystick_being_configured = joystick_name;
|
||||||
|
}
|
||||||
|
else if (![joystick_being_configured isEqualToString:joystick_name]) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (last_axis == -1) {
|
||||||
|
last_axis = axis;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (axis == last_axis) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
NSMutableDictionary *all_mappings = [[[NSUserDefaults standardUserDefaults] dictionaryForKey:@"GBJoypadMappings"] mutableCopy];
|
||||||
|
|
||||||
|
if (!all_mappings) {
|
||||||
|
all_mappings = [[NSMutableDictionary alloc] init];
|
||||||
|
}
|
||||||
|
|
||||||
|
NSMutableDictionary *mapping = [[all_mappings objectForKey:joystick_name] mutableCopy];
|
||||||
|
|
||||||
|
if (!mapping) {
|
||||||
|
mapping = [[NSMutableDictionary alloc] init];
|
||||||
|
}
|
||||||
|
|
||||||
|
mapping[@"XAxis"] = @(MIN(axis, last_axis));
|
||||||
|
mapping[@"YAxis"] = @(MAX(axis, last_axis));
|
||||||
|
|
||||||
|
all_mappings[joystick_name] = mapping;
|
||||||
|
[[NSUserDefaults standardUserDefaults] setObject:all_mappings forKey:@"GBJoypadMappings"];
|
||||||
|
[self advanceConfigurationStateMachine];
|
||||||
|
}
|
||||||
|
|
||||||
- (NSButton *)aspectRatioCheckbox
|
- (NSButton *)aspectRatioCheckbox
|
||||||
{
|
{
|
||||||
return _aspectRatioCheckbox;
|
return _aspectRatioCheckbox;
|
||||||
|
|
|
@ -1,8 +1,9 @@
|
||||||
#import <Cocoa/Cocoa.h>
|
#import <Cocoa/Cocoa.h>
|
||||||
#include <Core/gb.h>
|
#include <Core/gb.h>
|
||||||
|
#import "GBJoystickListener.h"
|
||||||
#import "GBShader.h"
|
#import "GBShader.h"
|
||||||
|
|
||||||
@interface GBView : NSOpenGLView
|
@interface GBView<GBJoystickListener> : NSOpenGLView
|
||||||
- (void) flip;
|
- (void) flip;
|
||||||
- (uint32_t *) pixels;
|
- (uint32_t *) pixels;
|
||||||
@property GB_gameboy_t *gb;
|
@property GB_gameboy_t *gb;
|
||||||
|
|
|
@ -11,6 +11,7 @@
|
||||||
BOOL mouse_hidden;
|
BOOL mouse_hidden;
|
||||||
NSTrackingArea *tracking_area;
|
NSTrackingArea *tracking_area;
|
||||||
BOOL _mouseHidingEnabled;
|
BOOL _mouseHidingEnabled;
|
||||||
|
bool enableAnalog;
|
||||||
}
|
}
|
||||||
|
|
||||||
- (void) awakeFromNib
|
- (void) awakeFromNib
|
||||||
|
@ -213,6 +214,51 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
- (void) joystick:(NSString *)joystick_name button: (unsigned)button changedState: (bool) state
|
||||||
|
{
|
||||||
|
NSDictionary *mapping = [[NSUserDefaults standardUserDefaults] dictionaryForKey:@"GBJoypadMappings"][joystick_name];
|
||||||
|
|
||||||
|
for (GBButton i = 0; i < GBButtonCount; i++) {
|
||||||
|
NSNumber *mapped_button = [mapping objectForKey:GBButtonNames[i]];
|
||||||
|
if (mapped_button && [mapped_button integerValue] == button) {
|
||||||
|
switch (i) {
|
||||||
|
case GBTurbo:
|
||||||
|
GB_set_turbo_mode(_gb, state, false);
|
||||||
|
break;
|
||||||
|
|
||||||
|
default:
|
||||||
|
if (i < GB_KEY_A) {
|
||||||
|
enableAnalog = false;
|
||||||
|
}
|
||||||
|
GB_set_key_state(_gb, (GB_key_t)i, state);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
- (void) joystick:(NSString *)joystick_name axis: (unsigned)axis movedTo: (signed) value
|
||||||
|
{
|
||||||
|
NSDictionary *mapping = [[NSUserDefaults standardUserDefaults] dictionaryForKey:@"GBJoypadMappings"][joystick_name];
|
||||||
|
NSNumber *x_axis = [mapping objectForKey:@"XAxis"];
|
||||||
|
NSNumber *y_axis = [mapping objectForKey:@"YAxis"];
|
||||||
|
|
||||||
|
if (value > 0x4000 || value < -0x4000) {
|
||||||
|
enableAnalog = true;
|
||||||
|
}
|
||||||
|
if (!enableAnalog) return;
|
||||||
|
|
||||||
|
if (x_axis && [x_axis integerValue] == axis) {
|
||||||
|
GB_set_key_state(_gb, GB_KEY_LEFT, value < -0x4000);
|
||||||
|
GB_set_key_state(_gb, GB_KEY_RIGHT, value > 0x4000);
|
||||||
|
}
|
||||||
|
else if (y_axis && [y_axis integerValue] == axis) {
|
||||||
|
GB_set_key_state(_gb, GB_KEY_UP, value < -0x4000);
|
||||||
|
GB_set_key_state(_gb, GB_KEY_DOWN, value > 0x4000);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
- (BOOL)acceptsFirstResponder
|
- (BOOL)acceptsFirstResponder
|
||||||
{
|
{
|
||||||
return YES;
|
return YES;
|
||||||
|
@ -259,4 +305,5 @@
|
||||||
{
|
{
|
||||||
return _mouseHidingEnabled;
|
return _mouseHidingEnabled;
|
||||||
}
|
}
|
||||||
|
|
||||||
@end
|
@end
|
||||||
|
|
|
@ -1,8 +1,8 @@
|
||||||
<?xml version="1.0" encoding="UTF-8"?>
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
<document type="com.apple.InterfaceBuilder3.Cocoa.XIB" version="3.0" toolsVersion="13196" targetRuntime="MacOSX.Cocoa" propertyAccessControl="none" useAutolayout="YES" customObjectInstantitationMethod="direct">
|
<document type="com.apple.InterfaceBuilder3.Cocoa.XIB" version="3.0" toolsVersion="13529" targetRuntime="MacOSX.Cocoa" propertyAccessControl="none" useAutolayout="YES" customObjectInstantitationMethod="direct">
|
||||||
<dependencies>
|
<dependencies>
|
||||||
<deployment identifier="macosx"/>
|
<deployment identifier="macosx"/>
|
||||||
<plugIn identifier="com.apple.InterfaceBuilder.CocoaPlugin" version="13196"/>
|
<plugIn identifier="com.apple.InterfaceBuilder.CocoaPlugin" version="13529"/>
|
||||||
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
|
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
|
||||||
</dependencies>
|
</dependencies>
|
||||||
<objects>
|
<objects>
|
||||||
|
@ -17,14 +17,14 @@
|
||||||
<windowStyleMask key="styleMask" titled="YES" closable="YES"/>
|
<windowStyleMask key="styleMask" titled="YES" closable="YES"/>
|
||||||
<windowCollectionBehavior key="collectionBehavior" fullScreenAuxiliary="YES"/>
|
<windowCollectionBehavior key="collectionBehavior" fullScreenAuxiliary="YES"/>
|
||||||
<windowPositionMask key="initialPositionMask" leftStrut="YES" rightStrut="YES" topStrut="YES" bottomStrut="YES"/>
|
<windowPositionMask key="initialPositionMask" leftStrut="YES" rightStrut="YES" topStrut="YES" bottomStrut="YES"/>
|
||||||
<rect key="contentRect" x="196" y="240" width="292" height="426"/>
|
<rect key="contentRect" x="196" y="240" width="292" height="459"/>
|
||||||
<rect key="screenRect" x="0.0" y="0.0" width="2560" height="1417"/>
|
<rect key="screenRect" x="0.0" y="0.0" width="2560" height="1417"/>
|
||||||
<view key="contentView" id="EiT-Mj-1SZ">
|
<view key="contentView" id="EiT-Mj-1SZ">
|
||||||
<rect key="frame" x="0.0" y="0.0" width="292" height="426"/>
|
<rect key="frame" x="0.0" y="0.0" width="292" height="459"/>
|
||||||
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
|
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
|
||||||
<subviews>
|
<subviews>
|
||||||
<textField horizontalHuggingPriority="251" verticalHuggingPriority="750" fixedFrame="YES" translatesAutoresizingMaskIntoConstraints="NO" id="T91-rh-rRp">
|
<textField horizontalHuggingPriority="251" verticalHuggingPriority="750" fixedFrame="YES" translatesAutoresizingMaskIntoConstraints="NO" id="T91-rh-rRp">
|
||||||
<rect key="frame" x="18" y="389" width="256" height="17"/>
|
<rect key="frame" x="18" y="422" width="256" height="17"/>
|
||||||
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMinY="YES"/>
|
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMinY="YES"/>
|
||||||
<textFieldCell key="cell" scrollable="YES" lineBreakMode="clipping" sendsActionOnEndEditing="YES" title="Graphics Filter:" id="pXg-WY-8Q5">
|
<textFieldCell key="cell" scrollable="YES" lineBreakMode="clipping" sendsActionOnEndEditing="YES" title="Graphics Filter:" id="pXg-WY-8Q5">
|
||||||
<font key="font" metaFont="system"/>
|
<font key="font" metaFont="system"/>
|
||||||
|
@ -33,7 +33,7 @@
|
||||||
</textFieldCell>
|
</textFieldCell>
|
||||||
</textField>
|
</textField>
|
||||||
<popUpButton verticalHuggingPriority="750" fixedFrame="YES" translatesAutoresizingMaskIntoConstraints="NO" id="6pP-kK-EEC">
|
<popUpButton verticalHuggingPriority="750" fixedFrame="YES" translatesAutoresizingMaskIntoConstraints="NO" id="6pP-kK-EEC">
|
||||||
<rect key="frame" x="30" y="357" width="245" height="26"/>
|
<rect key="frame" x="30" y="390" width="245" height="26"/>
|
||||||
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMinY="YES"/>
|
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMinY="YES"/>
|
||||||
<popUpButtonCell key="cell" type="push" bezelStyle="rounded" alignment="left" lineBreakMode="truncatingTail" borderStyle="borderAndBezel" imageScaling="proportionallyDown" inset="2" id="I1w-05-lGl">
|
<popUpButtonCell key="cell" type="push" bezelStyle="rounded" alignment="left" lineBreakMode="truncatingTail" borderStyle="borderAndBezel" imageScaling="proportionallyDown" inset="2" id="I1w-05-lGl">
|
||||||
<behavior key="behavior" lightByBackground="YES" lightByGray="YES"/>
|
<behavior key="behavior" lightByBackground="YES" lightByGray="YES"/>
|
||||||
|
@ -67,7 +67,7 @@
|
||||||
</connections>
|
</connections>
|
||||||
</popUpButton>
|
</popUpButton>
|
||||||
<textField horizontalHuggingPriority="251" verticalHuggingPriority="750" fixedFrame="YES" translatesAutoresizingMaskIntoConstraints="NO" id="Wc3-2K-6CD">
|
<textField horizontalHuggingPriority="251" verticalHuggingPriority="750" fixedFrame="YES" translatesAutoresizingMaskIntoConstraints="NO" id="Wc3-2K-6CD">
|
||||||
<rect key="frame" x="18" y="335" width="256" height="17"/>
|
<rect key="frame" x="18" y="368" width="256" height="17"/>
|
||||||
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMinY="YES"/>
|
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMinY="YES"/>
|
||||||
<textFieldCell key="cell" scrollable="YES" lineBreakMode="clipping" sendsActionOnEndEditing="YES" title="Color Correction:" id="5Si-hz-EK3">
|
<textFieldCell key="cell" scrollable="YES" lineBreakMode="clipping" sendsActionOnEndEditing="YES" title="Color Correction:" id="5Si-hz-EK3">
|
||||||
<font key="font" metaFont="system"/>
|
<font key="font" metaFont="system"/>
|
||||||
|
@ -76,7 +76,7 @@
|
||||||
</textFieldCell>
|
</textFieldCell>
|
||||||
</textField>
|
</textField>
|
||||||
<popUpButton verticalHuggingPriority="750" fixedFrame="YES" translatesAutoresizingMaskIntoConstraints="NO" id="VEz-N4-uP6">
|
<popUpButton verticalHuggingPriority="750" fixedFrame="YES" translatesAutoresizingMaskIntoConstraints="NO" id="VEz-N4-uP6">
|
||||||
<rect key="frame" x="30" y="303" width="245" height="26"/>
|
<rect key="frame" x="30" y="336" width="245" height="26"/>
|
||||||
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMinY="YES"/>
|
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMinY="YES"/>
|
||||||
<popUpButtonCell key="cell" type="push" title="Disabled" bezelStyle="rounded" alignment="left" lineBreakMode="truncatingTail" state="on" borderStyle="borderAndBezel" imageScaling="proportionallyDown" inset="2" selectedItem="D2J-wV-1vu" id="fNJ-Fi-yOm">
|
<popUpButtonCell key="cell" type="push" title="Disabled" bezelStyle="rounded" alignment="left" lineBreakMode="truncatingTail" state="on" borderStyle="borderAndBezel" imageScaling="proportionallyDown" inset="2" selectedItem="D2J-wV-1vu" id="fNJ-Fi-yOm">
|
||||||
<behavior key="behavior" lightByBackground="YES" lightByGray="YES"/>
|
<behavior key="behavior" lightByBackground="YES" lightByGray="YES"/>
|
||||||
|
@ -97,7 +97,7 @@
|
||||||
</connections>
|
</connections>
|
||||||
</popUpButton>
|
</popUpButton>
|
||||||
<popUpButton verticalHuggingPriority="750" fixedFrame="YES" translatesAutoresizingMaskIntoConstraints="NO" id="T69-6N-dhT">
|
<popUpButton verticalHuggingPriority="750" fixedFrame="YES" translatesAutoresizingMaskIntoConstraints="NO" id="T69-6N-dhT">
|
||||||
<rect key="frame" x="30" y="223" width="245" height="26"/>
|
<rect key="frame" x="30" y="256" width="245" height="26"/>
|
||||||
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMinY="YES"/>
|
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMinY="YES"/>
|
||||||
<popUpButtonCell key="cell" type="push" title="Disabled (Keep DC Offset)" bezelStyle="rounded" alignment="left" lineBreakMode="truncatingTail" state="on" borderStyle="borderAndBezel" imageScaling="proportionallyDown" inset="2" selectedItem="Fgo-0S-zUG" id="om2-Bn-43B">
|
<popUpButtonCell key="cell" type="push" title="Disabled (Keep DC Offset)" bezelStyle="rounded" alignment="left" lineBreakMode="truncatingTail" state="on" borderStyle="borderAndBezel" imageScaling="proportionallyDown" inset="2" selectedItem="Fgo-0S-zUG" id="om2-Bn-43B">
|
||||||
<behavior key="behavior" lightByBackground="YES" lightByGray="YES"/>
|
<behavior key="behavior" lightByBackground="YES" lightByGray="YES"/>
|
||||||
|
@ -117,7 +117,7 @@
|
||||||
</connections>
|
</connections>
|
||||||
</popUpButton>
|
</popUpButton>
|
||||||
<button fixedFrame="YES" translatesAutoresizingMaskIntoConstraints="NO" id="Vfj-tg-7OP">
|
<button fixedFrame="YES" translatesAutoresizingMaskIntoConstraints="NO" id="Vfj-tg-7OP">
|
||||||
<rect key="frame" x="18" y="278" width="256" height="18"/>
|
<rect key="frame" x="18" y="311" width="256" height="18"/>
|
||||||
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMinY="YES"/>
|
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMinY="YES"/>
|
||||||
<buttonCell key="cell" type="check" title="Keep Aspect Ratio" bezelStyle="regularSquare" imagePosition="left" state="on" inset="2" id="lsj-rC-Eo6">
|
<buttonCell key="cell" type="check" title="Keep Aspect Ratio" bezelStyle="regularSquare" imagePosition="left" state="on" inset="2" id="lsj-rC-Eo6">
|
||||||
<behavior key="behavior" changeContents="YES" doesNotDimImage="YES" lightByContents="YES"/>
|
<behavior key="behavior" changeContents="YES" doesNotDimImage="YES" lightByContents="YES"/>
|
||||||
|
@ -128,7 +128,7 @@
|
||||||
</connections>
|
</connections>
|
||||||
</button>
|
</button>
|
||||||
<textField horizontalHuggingPriority="251" verticalHuggingPriority="750" fixedFrame="YES" translatesAutoresizingMaskIntoConstraints="NO" id="Utu-t4-cLx">
|
<textField horizontalHuggingPriority="251" verticalHuggingPriority="750" fixedFrame="YES" translatesAutoresizingMaskIntoConstraints="NO" id="Utu-t4-cLx">
|
||||||
<rect key="frame" x="18" y="201" width="256" height="17"/>
|
<rect key="frame" x="18" y="234" width="256" height="17"/>
|
||||||
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMinY="YES"/>
|
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMinY="YES"/>
|
||||||
<textFieldCell key="cell" scrollable="YES" lineBreakMode="clipping" sendsActionOnEndEditing="YES" title="Button configuration:" id="YqW-Ds-VIC">
|
<textFieldCell key="cell" scrollable="YES" lineBreakMode="clipping" sendsActionOnEndEditing="YES" title="Button configuration:" id="YqW-Ds-VIC">
|
||||||
<font key="font" metaFont="system"/>
|
<font key="font" metaFont="system"/>
|
||||||
|
@ -137,7 +137,7 @@
|
||||||
</textFieldCell>
|
</textFieldCell>
|
||||||
</textField>
|
</textField>
|
||||||
<textField horizontalHuggingPriority="251" verticalHuggingPriority="750" fixedFrame="YES" translatesAutoresizingMaskIntoConstraints="NO" id="WU3-oV-KHO">
|
<textField horizontalHuggingPriority="251" verticalHuggingPriority="750" fixedFrame="YES" translatesAutoresizingMaskIntoConstraints="NO" id="WU3-oV-KHO">
|
||||||
<rect key="frame" x="18" y="255" width="256" height="17"/>
|
<rect key="frame" x="18" y="288" width="256" height="17"/>
|
||||||
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMinY="YES"/>
|
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMinY="YES"/>
|
||||||
<textFieldCell key="cell" scrollable="YES" lineBreakMode="clipping" sendsActionOnEndEditing="YES" title="High-pass Filter:" id="YLF-RL-b2D">
|
<textFieldCell key="cell" scrollable="YES" lineBreakMode="clipping" sendsActionOnEndEditing="YES" title="High-pass Filter:" id="YLF-RL-b2D">
|
||||||
<font key="font" metaFont="system"/>
|
<font key="font" metaFont="system"/>
|
||||||
|
@ -146,7 +146,7 @@
|
||||||
</textFieldCell>
|
</textFieldCell>
|
||||||
</textField>
|
</textField>
|
||||||
<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">
|
<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="20" y="20" width="252" height="173"/>
|
<rect key="frame" x="20" y="53" width="252" height="173"/>
|
||||||
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMinY="YES"/>
|
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMinY="YES"/>
|
||||||
<clipView key="contentView" focusRingType="none" ambiguous="YES" drawsBackground="NO" id="AMs-PO-nid">
|
<clipView key="contentView" focusRingType="none" ambiguous="YES" drawsBackground="NO" id="AMs-PO-nid">
|
||||||
<rect key="frame" x="1" y="1" width="250" height="171"/>
|
<rect key="frame" x="1" y="1" width="250" height="171"/>
|
||||||
|
@ -203,15 +203,39 @@
|
||||||
<autoresizingMask key="autoresizingMask"/>
|
<autoresizingMask key="autoresizingMask"/>
|
||||||
</scroller>
|
</scroller>
|
||||||
</scrollView>
|
</scrollView>
|
||||||
|
<button verticalHuggingPriority="750" fixedFrame="YES" translatesAutoresizingMaskIntoConstraints="NO" id="d2I-jU-sLb">
|
||||||
|
<rect key="frame" x="205" y="10" width="67" height="32"/>
|
||||||
|
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMinY="YES"/>
|
||||||
|
<buttonCell key="cell" type="push" title="Skip" bezelStyle="rounded" alignment="center" enabled="NO" borderStyle="border" imageScaling="proportionallyDown" inset="2" id="sug-xy-tbw">
|
||||||
|
<behavior key="behavior" pushIn="YES" lightByBackground="YES" lightByGray="YES"/>
|
||||||
|
<font key="font" metaFont="system"/>
|
||||||
|
</buttonCell>
|
||||||
|
<connections>
|
||||||
|
<action selector="skipButton:" target="QvC-M9-y7g" id="aw8-sw-yJw"/>
|
||||||
|
</connections>
|
||||||
|
</button>
|
||||||
|
<button verticalHuggingPriority="750" fixedFrame="YES" translatesAutoresizingMaskIntoConstraints="NO" id="Qa7-Z7-yfO">
|
||||||
|
<rect key="frame" x="20" y="10" width="188" height="32"/>
|
||||||
|
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMinY="YES"/>
|
||||||
|
<buttonCell key="cell" type="push" title="Configure Joypad" bezelStyle="rounded" alignment="center" borderStyle="border" imageScaling="proportionallyDown" inset="2" id="GdK-tQ-Wim">
|
||||||
|
<behavior key="behavior" pushIn="YES" lightByBackground="YES" lightByGray="YES"/>
|
||||||
|
<font key="font" metaFont="system"/>
|
||||||
|
</buttonCell>
|
||||||
|
<connections>
|
||||||
|
<action selector="configureJoypad:" target="QvC-M9-y7g" id="IfY-Kc-PKU"/>
|
||||||
|
</connections>
|
||||||
|
</button>
|
||||||
</subviews>
|
</subviews>
|
||||||
</view>
|
</view>
|
||||||
<connections>
|
<connections>
|
||||||
<outlet property="aspectRatioCheckbox" destination="Vfj-tg-7OP" id="Yw0-xS-DBr"/>
|
<outlet property="aspectRatioCheckbox" destination="Vfj-tg-7OP" id="Yw0-xS-DBr"/>
|
||||||
<outlet property="colorCorrectionPopupButton" destination="VEz-N4-uP6" id="EO2-Vt-JFJ"/>
|
<outlet property="colorCorrectionPopupButton" destination="VEz-N4-uP6" id="EO2-Vt-JFJ"/>
|
||||||
|
<outlet property="configureJoypadButton" destination="Qa7-Z7-yfO" id="RaX-P3-oCX"/>
|
||||||
<outlet property="controlsTableView" destination="UDd-IJ-fxX" id="a1D-Md-yXv"/>
|
<outlet property="controlsTableView" destination="UDd-IJ-fxX" id="a1D-Md-yXv"/>
|
||||||
<outlet property="delegate" destination="-2" id="ASc-vN-Zbq"/>
|
<outlet property="delegate" destination="-2" id="ASc-vN-Zbq"/>
|
||||||
<outlet property="graphicsFilterPopupButton" destination="6pP-kK-EEC" id="LS7-HY-kHC"/>
|
<outlet property="graphicsFilterPopupButton" destination="6pP-kK-EEC" id="LS7-HY-kHC"/>
|
||||||
<outlet property="highpassFilterPopupButton" destination="T69-6N-dhT" id="0p6-4m-hb1"/>
|
<outlet property="highpassFilterPopupButton" destination="T69-6N-dhT" id="0p6-4m-hb1"/>
|
||||||
|
<outlet property="skipButton" destination="d2I-jU-sLb" id="udX-8K-0sK"/>
|
||||||
</connections>
|
</connections>
|
||||||
<point key="canvasLocation" x="179" y="498"/>
|
<point key="canvasLocation" x="179" y="498"/>
|
||||||
</window>
|
</window>
|
||||||
|
|
|
@ -0,0 +1,752 @@
|
||||||
|
/*
|
||||||
|
Joypad support is based on a stripped-down version of SDL's Darwin implementation
|
||||||
|
of the Joystick API, under the following license:
|
||||||
|
*/
|
||||||
|
|
||||||
|
/*
|
||||||
|
Simple DirectMedia Layer
|
||||||
|
Copyright (C) 1997-2017 Sam Lantinga <slouken@libsdl.org>
|
||||||
|
|
||||||
|
This software is provided 'as-is', without any express or implied
|
||||||
|
warranty. In no event will the authors be held liable for any damages
|
||||||
|
arising from the use of this software.
|
||||||
|
|
||||||
|
Permission is granted to anyone to use this software for any purpose,
|
||||||
|
including commercial applications, and to alter it and redistribute it
|
||||||
|
freely, subject to the following restrictions:
|
||||||
|
|
||||||
|
1. The origin of this software must not be misrepresented; you must not
|
||||||
|
claim that you wrote the original software. If you use this software
|
||||||
|
in a product, an acknowledgment in the product documentation would be
|
||||||
|
appreciated but is not required.
|
||||||
|
2. Altered source versions must be plainly marked as such, and must not be
|
||||||
|
misrepresented as being the original software.
|
||||||
|
3. This notice may not be removed or altered from any source distribution.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#include <AppKit/AppKit.h>
|
||||||
|
#include <stdint.h>
|
||||||
|
#include <stdbool.h>
|
||||||
|
#include <IOKit/hid/IOHIDLib.h>
|
||||||
|
#include "GBJoystickListener.h"
|
||||||
|
|
||||||
|
typedef signed SDL_JoystickID;
|
||||||
|
typedef struct _SDL_Joystick SDL_Joystick;
|
||||||
|
|
||||||
|
typedef struct _SDL_JoystickAxisInfo
|
||||||
|
{
|
||||||
|
int16_t initial_value; /* Initial axis state */
|
||||||
|
int16_t value; /* Current axis state */
|
||||||
|
int16_t zero; /* Zero point on the axis (-32768 for triggers) */
|
||||||
|
bool has_initial_value; /* Whether we've seen a value on the axis yet */
|
||||||
|
bool sent_initial_value; /* Whether we've sent the initial axis value */
|
||||||
|
} SDL_JoystickAxisInfo;
|
||||||
|
|
||||||
|
struct _SDL_Joystick
|
||||||
|
{
|
||||||
|
SDL_JoystickID instance_id; /* Device instance, monotonically increasing from 0 */
|
||||||
|
char *name; /* Joystick name - system dependent */
|
||||||
|
|
||||||
|
int naxes; /* Number of axis controls on the joystick */
|
||||||
|
SDL_JoystickAxisInfo *axes;
|
||||||
|
|
||||||
|
int nbuttons; /* Number of buttons on the joystick */
|
||||||
|
uint8_t *buttons; /* Current button states */
|
||||||
|
|
||||||
|
struct joystick_hwdata *hwdata; /* Driver dependent information */
|
||||||
|
|
||||||
|
int ref_count; /* Reference count for multiple opens */
|
||||||
|
|
||||||
|
bool is_game_controller;
|
||||||
|
bool force_recentering; /* SDL_TRUE if this device needs to have its state reset to 0 */
|
||||||
|
struct _SDL_Joystick *next; /* pointer to next joystick we have allocated */
|
||||||
|
};
|
||||||
|
|
||||||
|
typedef struct {
|
||||||
|
uint8_t data[16];
|
||||||
|
} SDL_JoystickGUID;
|
||||||
|
|
||||||
|
struct recElement
|
||||||
|
{
|
||||||
|
IOHIDElementRef elementRef;
|
||||||
|
IOHIDElementCookie cookie;
|
||||||
|
uint32_t usagePage, usage; /* HID usage */
|
||||||
|
SInt32 min; /* reported min value possible */
|
||||||
|
SInt32 max; /* reported max value possible */
|
||||||
|
|
||||||
|
/* runtime variables used for auto-calibration */
|
||||||
|
SInt32 minReport; /* min returned value */
|
||||||
|
SInt32 maxReport; /* max returned value */
|
||||||
|
|
||||||
|
struct recElement *pNext; /* next element in list */
|
||||||
|
};
|
||||||
|
typedef struct recElement recElement;
|
||||||
|
|
||||||
|
struct joystick_hwdata
|
||||||
|
{
|
||||||
|
IOHIDDeviceRef deviceRef; /* HIDManager device handle */
|
||||||
|
io_service_t ffservice; /* Interface for force feedback, 0 = no ff */
|
||||||
|
|
||||||
|
char product[256]; /* name of product */
|
||||||
|
uint32_t usage; /* usage page from IOUSBHID Parser.h which defines general usage */
|
||||||
|
uint32_t usagePage; /* usage within above page from IOUSBHID Parser.h which defines specific usage */
|
||||||
|
|
||||||
|
int axes; /* number of axis (calculated, not reported by device) */
|
||||||
|
int buttons; /* number of buttons (calculated, not reported by device) */
|
||||||
|
int elements; /* number of total elements (should be total of above) (calculated, not reported by device) */
|
||||||
|
|
||||||
|
recElement *firstAxis;
|
||||||
|
recElement *firstButton;
|
||||||
|
|
||||||
|
bool removed;
|
||||||
|
|
||||||
|
int instance_id;
|
||||||
|
SDL_JoystickGUID guid;
|
||||||
|
|
||||||
|
SDL_Joystick joystick;
|
||||||
|
|
||||||
|
struct joystick_hwdata *pNext; /* next device */
|
||||||
|
};
|
||||||
|
typedef struct joystick_hwdata recDevice;
|
||||||
|
|
||||||
|
#define SDL_JOYSTICK_RUNLOOP_MODE CFSTR("SDLJoystick")
|
||||||
|
|
||||||
|
/* The base object of the HID Manager API */
|
||||||
|
static IOHIDManagerRef hidman = NULL;
|
||||||
|
|
||||||
|
/* Linked list of all available devices */
|
||||||
|
static recDevice *gpDeviceList = NULL;
|
||||||
|
|
||||||
|
/* static incrementing counter for new joystick devices seen on the system. Devices should start with index 0 */
|
||||||
|
static int s_joystick_instance_id = -1;
|
||||||
|
|
||||||
|
#define SDL_JOYSTICK_AXIS_MAX 32767
|
||||||
|
|
||||||
|
void SDL_PrivateJoystickAxis(SDL_Joystick * joystick, uint8_t axis, int16_t value)
|
||||||
|
{
|
||||||
|
/* Make sure we're not getting garbage or duplicate events */
|
||||||
|
if (axis >= joystick->naxes) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!joystick->axes[axis].has_initial_value) {
|
||||||
|
joystick->axes[axis].initial_value = value;
|
||||||
|
joystick->axes[axis].value = value;
|
||||||
|
joystick->axes[axis].zero = value;
|
||||||
|
joystick->axes[axis].has_initial_value = true;
|
||||||
|
}
|
||||||
|
if (value == joystick->axes[axis].value) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!joystick->axes[axis].sent_initial_value) {
|
||||||
|
/* Make sure we don't send motion until there's real activity on this axis */
|
||||||
|
const int MAX_ALLOWED_JITTER = SDL_JOYSTICK_AXIS_MAX / 80; /* ShanWan PS3 controller needed 96 */
|
||||||
|
if (abs(value - joystick->axes[axis].value) <= MAX_ALLOWED_JITTER) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
joystick->axes[axis].sent_initial_value = true;
|
||||||
|
joystick->axes[axis].value = value; /* Just so we pass the check above */
|
||||||
|
SDL_PrivateJoystickAxis(joystick, axis, joystick->axes[axis].initial_value);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Update internal joystick state */
|
||||||
|
joystick->axes[axis].value = value;
|
||||||
|
|
||||||
|
NSResponder<GBJoystickListener> *responder = (typeof(responder)) [[NSApp keyWindow] firstResponder];
|
||||||
|
while (responder) {
|
||||||
|
if ([responder respondsToSelector:@selector(joystick:axis:movedTo:)]) {
|
||||||
|
[responder joystick:@(joystick->name) axis:axis movedTo:value];
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
responder = (typeof(responder)) [responder nextResponder];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void SDL_PrivateJoystickButton(SDL_Joystick *joystick, uint8_t button, uint8_t state)
|
||||||
|
{
|
||||||
|
|
||||||
|
/* Make sure we're not getting garbage or duplicate events */
|
||||||
|
if (button >= joystick->nbuttons) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (state == joystick->buttons[button]) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Update internal joystick state */
|
||||||
|
joystick->buttons[button] = state;
|
||||||
|
|
||||||
|
NSResponder<GBJoystickListener> *responder = (typeof(responder)) [[NSApp keyWindow] firstResponder];
|
||||||
|
while (responder) {
|
||||||
|
if ([responder respondsToSelector:@selector(joystick:button:changedState:)]) {
|
||||||
|
[responder joystick:@(joystick->name) button:button changedState:state];
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
responder = (typeof(responder)) [responder nextResponder];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static void
|
||||||
|
FreeElementList(recElement *pElement)
|
||||||
|
{
|
||||||
|
while (pElement) {
|
||||||
|
recElement *pElementNext = pElement->pNext;
|
||||||
|
free(pElement);
|
||||||
|
pElement = pElementNext;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
static recDevice *
|
||||||
|
FreeDevice(recDevice *removeDevice)
|
||||||
|
{
|
||||||
|
recDevice *pDeviceNext = NULL;
|
||||||
|
if (removeDevice) {
|
||||||
|
if (removeDevice->deviceRef) {
|
||||||
|
IOHIDDeviceUnscheduleFromRunLoop(removeDevice->deviceRef, CFRunLoopGetCurrent(), SDL_JOYSTICK_RUNLOOP_MODE);
|
||||||
|
removeDevice->deviceRef = NULL;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* save next device prior to disposing of this device */
|
||||||
|
pDeviceNext = removeDevice->pNext;
|
||||||
|
|
||||||
|
if ( gpDeviceList == removeDevice ) {
|
||||||
|
gpDeviceList = pDeviceNext;
|
||||||
|
} else {
|
||||||
|
recDevice *device = gpDeviceList;
|
||||||
|
while (device->pNext != removeDevice) {
|
||||||
|
device = device->pNext;
|
||||||
|
}
|
||||||
|
device->pNext = pDeviceNext;
|
||||||
|
}
|
||||||
|
removeDevice->pNext = NULL;
|
||||||
|
|
||||||
|
/* free element lists */
|
||||||
|
FreeElementList(removeDevice->firstAxis);
|
||||||
|
FreeElementList(removeDevice->firstButton);
|
||||||
|
|
||||||
|
free(removeDevice);
|
||||||
|
}
|
||||||
|
return pDeviceNext;
|
||||||
|
}
|
||||||
|
|
||||||
|
static SInt32
|
||||||
|
GetHIDElementState(recDevice *pDevice, recElement *pElement)
|
||||||
|
{
|
||||||
|
SInt32 value = 0;
|
||||||
|
|
||||||
|
if (pDevice && pElement) {
|
||||||
|
IOHIDValueRef valueRef;
|
||||||
|
if (IOHIDDeviceGetValue(pDevice->deviceRef, pElement->elementRef, &valueRef) == kIOReturnSuccess) {
|
||||||
|
value = (SInt32) IOHIDValueGetIntegerValue(valueRef);
|
||||||
|
|
||||||
|
/* record min and max for auto calibration */
|
||||||
|
if (value < pElement->minReport) {
|
||||||
|
pElement->minReport = value;
|
||||||
|
}
|
||||||
|
if (value > pElement->maxReport) {
|
||||||
|
pElement->maxReport = value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
|
static SInt32
|
||||||
|
GetHIDScaledCalibratedState(recDevice * pDevice, recElement * pElement, SInt32 min, SInt32 max)
|
||||||
|
{
|
||||||
|
const float deviceScale = max - min;
|
||||||
|
const float readScale = pElement->maxReport - pElement->minReport;
|
||||||
|
const SInt32 value = GetHIDElementState(pDevice, pElement);
|
||||||
|
if (readScale == 0) {
|
||||||
|
return value; /* no scaling at all */
|
||||||
|
}
|
||||||
|
return ((value - pElement->minReport) * deviceScale / readScale) + min;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
static void
|
||||||
|
JoystickDeviceWasRemovedCallback(void *ctx, IOReturn result, void *sender)
|
||||||
|
{
|
||||||
|
recDevice *device = (recDevice *) ctx;
|
||||||
|
device->removed = true;
|
||||||
|
device->deviceRef = NULL; // deviceRef was invalidated due to the remove
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
static void AddHIDElement(const void *value, void *parameter);
|
||||||
|
|
||||||
|
/* Call AddHIDElement() on all elements in an array of IOHIDElementRefs */
|
||||||
|
static void
|
||||||
|
AddHIDElements(CFArrayRef array, recDevice *pDevice)
|
||||||
|
{
|
||||||
|
const CFRange range = { 0, CFArrayGetCount(array) };
|
||||||
|
CFArrayApplyFunction(array, range, AddHIDElement, pDevice);
|
||||||
|
}
|
||||||
|
|
||||||
|
static bool
|
||||||
|
ElementAlreadyAdded(const IOHIDElementCookie cookie, const recElement *listitem) {
|
||||||
|
while (listitem) {
|
||||||
|
if (listitem->cookie == cookie) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
listitem = listitem->pNext;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* See if we care about this HID element, and if so, note it in our recDevice. */
|
||||||
|
static void
|
||||||
|
AddHIDElement(const void *value, void *parameter)
|
||||||
|
{
|
||||||
|
recDevice *pDevice = (recDevice *) parameter;
|
||||||
|
IOHIDElementRef refElement = (IOHIDElementRef) value;
|
||||||
|
const CFTypeID elementTypeID = refElement ? CFGetTypeID(refElement) : 0;
|
||||||
|
|
||||||
|
if (refElement && (elementTypeID == IOHIDElementGetTypeID())) {
|
||||||
|
const IOHIDElementCookie cookie = IOHIDElementGetCookie(refElement);
|
||||||
|
const uint32_t usagePage = IOHIDElementGetUsagePage(refElement);
|
||||||
|
const uint32_t usage = IOHIDElementGetUsage(refElement);
|
||||||
|
recElement *element = NULL;
|
||||||
|
recElement **headElement = NULL;
|
||||||
|
|
||||||
|
/* look at types of interest */
|
||||||
|
switch (IOHIDElementGetType(refElement)) {
|
||||||
|
case kIOHIDElementTypeInput_Misc:
|
||||||
|
case kIOHIDElementTypeInput_Button:
|
||||||
|
case kIOHIDElementTypeInput_Axis: {
|
||||||
|
switch (usagePage) { /* only interested in kHIDPage_GenericDesktop and kHIDPage_Button */
|
||||||
|
case kHIDPage_GenericDesktop:
|
||||||
|
switch (usage) {
|
||||||
|
case kHIDUsage_GD_X:
|
||||||
|
case kHIDUsage_GD_Y:
|
||||||
|
case kHIDUsage_GD_Z:
|
||||||
|
case kHIDUsage_GD_Rx:
|
||||||
|
case kHIDUsage_GD_Ry:
|
||||||
|
case kHIDUsage_GD_Rz:
|
||||||
|
case kHIDUsage_GD_Slider:
|
||||||
|
case kHIDUsage_GD_Dial:
|
||||||
|
case kHIDUsage_GD_Wheel:
|
||||||
|
if (!ElementAlreadyAdded(cookie, pDevice->firstAxis)) {
|
||||||
|
element = (recElement *) calloc(1, sizeof (recElement));
|
||||||
|
if (element) {
|
||||||
|
pDevice->axes++;
|
||||||
|
headElement = &(pDevice->firstAxis);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case kHIDUsage_GD_DPadUp:
|
||||||
|
case kHIDUsage_GD_DPadDown:
|
||||||
|
case kHIDUsage_GD_DPadRight:
|
||||||
|
case kHIDUsage_GD_DPadLeft:
|
||||||
|
case kHIDUsage_GD_Start:
|
||||||
|
case kHIDUsage_GD_Select:
|
||||||
|
case kHIDUsage_GD_SystemMainMenu:
|
||||||
|
if (!ElementAlreadyAdded(cookie, pDevice->firstButton)) {
|
||||||
|
element = (recElement *) calloc(1, sizeof (recElement));
|
||||||
|
if (element) {
|
||||||
|
pDevice->buttons++;
|
||||||
|
headElement = &(pDevice->firstButton);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case kHIDPage_Simulation:
|
||||||
|
switch (usage) {
|
||||||
|
case kHIDUsage_Sim_Rudder:
|
||||||
|
case kHIDUsage_Sim_Throttle:
|
||||||
|
case kHIDUsage_Sim_Accelerator:
|
||||||
|
case kHIDUsage_Sim_Brake:
|
||||||
|
if (!ElementAlreadyAdded(cookie, pDevice->firstAxis)) {
|
||||||
|
element = (recElement *) calloc(1, sizeof (recElement));
|
||||||
|
if (element) {
|
||||||
|
pDevice->axes++;
|
||||||
|
headElement = &(pDevice->firstAxis);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case kHIDPage_Button:
|
||||||
|
case kHIDPage_Consumer: /* e.g. 'pause' button on Steelseries MFi gamepads. */
|
||||||
|
if (!ElementAlreadyAdded(cookie, pDevice->firstButton)) {
|
||||||
|
element = (recElement *) calloc(1, sizeof (recElement));
|
||||||
|
if (element) {
|
||||||
|
pDevice->buttons++;
|
||||||
|
headElement = &(pDevice->firstButton);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case kIOHIDElementTypeCollection: {
|
||||||
|
CFArrayRef array = IOHIDElementGetChildren(refElement);
|
||||||
|
if (array) {
|
||||||
|
AddHIDElements(array, pDevice);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (element && headElement) { /* add to list */
|
||||||
|
recElement *elementPrevious = NULL;
|
||||||
|
recElement *elementCurrent = *headElement;
|
||||||
|
while (elementCurrent && usage >= elementCurrent->usage) {
|
||||||
|
elementPrevious = elementCurrent;
|
||||||
|
elementCurrent = elementCurrent->pNext;
|
||||||
|
}
|
||||||
|
if (elementPrevious) {
|
||||||
|
elementPrevious->pNext = element;
|
||||||
|
} else {
|
||||||
|
*headElement = element;
|
||||||
|
}
|
||||||
|
|
||||||
|
element->elementRef = refElement;
|
||||||
|
element->usagePage = usagePage;
|
||||||
|
element->usage = usage;
|
||||||
|
element->pNext = elementCurrent;
|
||||||
|
|
||||||
|
element->minReport = element->min = (SInt32) IOHIDElementGetLogicalMin(refElement);
|
||||||
|
element->maxReport = element->max = (SInt32) IOHIDElementGetLogicalMax(refElement);
|
||||||
|
element->cookie = IOHIDElementGetCookie(refElement);
|
||||||
|
|
||||||
|
pDevice->elements++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static bool
|
||||||
|
GetDeviceInfo(IOHIDDeviceRef hidDevice, recDevice *pDevice)
|
||||||
|
{
|
||||||
|
const uint16_t BUS_USB = 0x03;
|
||||||
|
const uint16_t BUS_BLUETOOTH = 0x05;
|
||||||
|
int32_t vendor = 0;
|
||||||
|
int32_t product = 0;
|
||||||
|
int32_t version = 0;
|
||||||
|
CFTypeRef refCF = NULL;
|
||||||
|
CFArrayRef array = NULL;
|
||||||
|
uint16_t *guid16 = (uint16_t *)pDevice->guid.data;
|
||||||
|
|
||||||
|
/* get usage page and usage */
|
||||||
|
refCF = IOHIDDeviceGetProperty(hidDevice, CFSTR(kIOHIDPrimaryUsagePageKey));
|
||||||
|
if (refCF) {
|
||||||
|
CFNumberGetValue(refCF, kCFNumberSInt32Type, &pDevice->usagePage);
|
||||||
|
}
|
||||||
|
if (pDevice->usagePage != kHIDPage_GenericDesktop) {
|
||||||
|
return false; /* Filter device list to non-keyboard/mouse stuff */
|
||||||
|
}
|
||||||
|
|
||||||
|
refCF = IOHIDDeviceGetProperty(hidDevice, CFSTR(kIOHIDPrimaryUsageKey));
|
||||||
|
if (refCF) {
|
||||||
|
CFNumberGetValue(refCF, kCFNumberSInt32Type, &pDevice->usage);
|
||||||
|
}
|
||||||
|
|
||||||
|
if ((pDevice->usage != kHIDUsage_GD_Joystick &&
|
||||||
|
pDevice->usage != kHIDUsage_GD_GamePad &&
|
||||||
|
pDevice->usage != kHIDUsage_GD_MultiAxisController)) {
|
||||||
|
return false; /* Filter device list to non-keyboard/mouse stuff */
|
||||||
|
}
|
||||||
|
|
||||||
|
pDevice->deviceRef = hidDevice;
|
||||||
|
|
||||||
|
/* get device name */
|
||||||
|
refCF = IOHIDDeviceGetProperty(hidDevice, CFSTR(kIOHIDProductKey));
|
||||||
|
if (!refCF) {
|
||||||
|
/* Maybe we can't get "AwesomeJoystick2000", but we can get "Logitech"? */
|
||||||
|
refCF = IOHIDDeviceGetProperty(hidDevice, CFSTR(kIOHIDManufacturerKey));
|
||||||
|
}
|
||||||
|
if ((!refCF) || (!CFStringGetCString(refCF, pDevice->product, sizeof (pDevice->product), kCFStringEncodingUTF8))) {
|
||||||
|
strlcpy(pDevice->product, "Unidentified joystick", sizeof (pDevice->product));
|
||||||
|
}
|
||||||
|
|
||||||
|
refCF = IOHIDDeviceGetProperty(hidDevice, CFSTR(kIOHIDVendorIDKey));
|
||||||
|
if (refCF) {
|
||||||
|
CFNumberGetValue(refCF, kCFNumberSInt32Type, &vendor);
|
||||||
|
}
|
||||||
|
|
||||||
|
refCF = IOHIDDeviceGetProperty(hidDevice, CFSTR(kIOHIDProductIDKey));
|
||||||
|
if (refCF) {
|
||||||
|
CFNumberGetValue(refCF, kCFNumberSInt32Type, &product);
|
||||||
|
}
|
||||||
|
|
||||||
|
refCF = IOHIDDeviceGetProperty(hidDevice, CFSTR(kIOHIDVersionNumberKey));
|
||||||
|
if (refCF) {
|
||||||
|
CFNumberGetValue(refCF, kCFNumberSInt32Type, &version);
|
||||||
|
}
|
||||||
|
|
||||||
|
memset(pDevice->guid.data, 0, sizeof(pDevice->guid.data));
|
||||||
|
|
||||||
|
if (vendor && product) {
|
||||||
|
*guid16++ = BUS_USB;
|
||||||
|
*guid16++ = 0;
|
||||||
|
*guid16++ = vendor;
|
||||||
|
*guid16++ = 0;
|
||||||
|
*guid16++ = product;
|
||||||
|
*guid16++ = 0;
|
||||||
|
*guid16++ = version;
|
||||||
|
*guid16++ = 0;
|
||||||
|
} else {
|
||||||
|
*guid16++ = BUS_BLUETOOTH;
|
||||||
|
*guid16++ = 0;
|
||||||
|
strlcpy((char*)guid16, pDevice->product, sizeof(pDevice->guid.data) - 4);
|
||||||
|
}
|
||||||
|
|
||||||
|
array = IOHIDDeviceCopyMatchingElements(hidDevice, NULL, kIOHIDOptionsTypeNone);
|
||||||
|
if (array) {
|
||||||
|
AddHIDElements(array, pDevice);
|
||||||
|
CFRelease(array);
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
static bool
|
||||||
|
JoystickAlreadyKnown(IOHIDDeviceRef ioHIDDeviceObject)
|
||||||
|
{
|
||||||
|
recDevice *i;
|
||||||
|
for (i = gpDeviceList; i != NULL; i = i->pNext) {
|
||||||
|
if (i->deviceRef == ioHIDDeviceObject) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
void
|
||||||
|
SDL_SYS_JoystickUpdate(SDL_Joystick * joystick)
|
||||||
|
{
|
||||||
|
recDevice *device = joystick->hwdata;
|
||||||
|
recElement *element;
|
||||||
|
SInt32 value;
|
||||||
|
int i;
|
||||||
|
|
||||||
|
if (!device) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (device->removed) { /* device was unplugged; ignore it. */
|
||||||
|
if (joystick->hwdata) {
|
||||||
|
joystick->force_recentering = true;
|
||||||
|
joystick->hwdata = NULL;
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
element = device->firstAxis;
|
||||||
|
i = 0;
|
||||||
|
while (element) {
|
||||||
|
value = GetHIDScaledCalibratedState(device, element, -32768, 32767);
|
||||||
|
SDL_PrivateJoystickAxis(joystick, i, value);
|
||||||
|
element = element->pNext;
|
||||||
|
++i;
|
||||||
|
}
|
||||||
|
|
||||||
|
element = device->firstButton;
|
||||||
|
i = 0;
|
||||||
|
while (element) {
|
||||||
|
value = GetHIDElementState(device, element);
|
||||||
|
if (value > 1) { /* handle pressure-sensitive buttons */
|
||||||
|
value = 1;
|
||||||
|
}
|
||||||
|
SDL_PrivateJoystickButton(joystick, i, value);
|
||||||
|
element = element->pNext;
|
||||||
|
++i;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static void
|
||||||
|
JoystickDeviceWasAddedCallback(void *ctx, IOReturn res, void *sender, IOHIDDeviceRef ioHIDDeviceObject)
|
||||||
|
{
|
||||||
|
recDevice *device;
|
||||||
|
int device_index = 0;
|
||||||
|
io_service_t ioservice;
|
||||||
|
|
||||||
|
if (res != kIOReturnSuccess) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (JoystickAlreadyKnown(ioHIDDeviceObject)) {
|
||||||
|
return; /* IOKit sent us a duplicate. */
|
||||||
|
}
|
||||||
|
|
||||||
|
device = (recDevice *) calloc(1, sizeof(recDevice));
|
||||||
|
|
||||||
|
if (!device) {
|
||||||
|
abort();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!GetDeviceInfo(ioHIDDeviceObject, device)) {
|
||||||
|
free(device);
|
||||||
|
return; /* not a device we care about, probably. */
|
||||||
|
}
|
||||||
|
|
||||||
|
SDL_Joystick *joystick = &device->joystick;
|
||||||
|
|
||||||
|
joystick->instance_id = device->instance_id;
|
||||||
|
joystick->hwdata = device;
|
||||||
|
joystick->name = device->product;
|
||||||
|
|
||||||
|
joystick->naxes = device->axes;
|
||||||
|
joystick->nbuttons = device->buttons;
|
||||||
|
|
||||||
|
if (joystick->naxes > 0) {
|
||||||
|
joystick->axes = (SDL_JoystickAxisInfo *) calloc(joystick->naxes, sizeof(SDL_JoystickAxisInfo));
|
||||||
|
}
|
||||||
|
if (joystick->nbuttons > 0) {
|
||||||
|
joystick->buttons = (uint8_t *) calloc(joystick->nbuttons, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Get notified when this device is disconnected. */
|
||||||
|
IOHIDDeviceRegisterRemovalCallback(ioHIDDeviceObject, JoystickDeviceWasRemovedCallback, device);
|
||||||
|
IOHIDDeviceScheduleWithRunLoop(ioHIDDeviceObject, CFRunLoopGetCurrent(), SDL_JOYSTICK_RUNLOOP_MODE);
|
||||||
|
|
||||||
|
/* Allocate an instance ID for this device */
|
||||||
|
device->instance_id = ++s_joystick_instance_id;
|
||||||
|
|
||||||
|
/* We have to do some storage of the io_service_t for SDL_HapticOpenFromJoystick */
|
||||||
|
ioservice = IOHIDDeviceGetService(ioHIDDeviceObject);
|
||||||
|
|
||||||
|
/* Add device to the end of the list */
|
||||||
|
if ( !gpDeviceList ) {
|
||||||
|
gpDeviceList = device;
|
||||||
|
} else {
|
||||||
|
recDevice *curdevice;
|
||||||
|
|
||||||
|
curdevice = gpDeviceList;
|
||||||
|
while ( curdevice->pNext ) {
|
||||||
|
++device_index;
|
||||||
|
curdevice = curdevice->pNext;
|
||||||
|
}
|
||||||
|
curdevice->pNext = device;
|
||||||
|
++device_index; /* bump by one since we counted by pNext. */
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static bool
|
||||||
|
ConfigHIDManager(CFArrayRef matchingArray)
|
||||||
|
{
|
||||||
|
CFRunLoopRef runloop = CFRunLoopGetCurrent();
|
||||||
|
|
||||||
|
if (IOHIDManagerOpen(hidman, kIOHIDOptionsTypeNone) != kIOReturnSuccess) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
IOHIDManagerSetDeviceMatchingMultiple(hidman, matchingArray);
|
||||||
|
IOHIDManagerRegisterDeviceMatchingCallback(hidman, JoystickDeviceWasAddedCallback, NULL);
|
||||||
|
IOHIDManagerScheduleWithRunLoop(hidman, runloop, SDL_JOYSTICK_RUNLOOP_MODE);
|
||||||
|
|
||||||
|
while (CFRunLoopRunInMode(SDL_JOYSTICK_RUNLOOP_MODE,0,TRUE) == kCFRunLoopRunHandledSource) {
|
||||||
|
/* no-op. Callback fires once per existing device. */
|
||||||
|
}
|
||||||
|
|
||||||
|
/* future hotplug events will come through SDL_JOYSTICK_RUNLOOP_MODE now. */
|
||||||
|
|
||||||
|
return true; /* good to go. */
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
static CFDictionaryRef
|
||||||
|
CreateHIDDeviceMatchDictionary(const UInt32 page, const UInt32 usage, int *okay)
|
||||||
|
{
|
||||||
|
CFDictionaryRef retval = NULL;
|
||||||
|
CFNumberRef pageNumRef = CFNumberCreate(kCFAllocatorDefault, kCFNumberIntType, &page);
|
||||||
|
CFNumberRef usageNumRef = CFNumberCreate(kCFAllocatorDefault, kCFNumberIntType, &usage);
|
||||||
|
const void *keys[2] = { (void *) CFSTR(kIOHIDDeviceUsagePageKey), (void *) CFSTR(kIOHIDDeviceUsageKey) };
|
||||||
|
const void *vals[2] = { (void *) pageNumRef, (void *) usageNumRef };
|
||||||
|
|
||||||
|
if (pageNumRef && usageNumRef) {
|
||||||
|
retval = CFDictionaryCreate(kCFAllocatorDefault, keys, vals, 2, &kCFTypeDictionaryKeyCallBacks, &kCFTypeDictionaryValueCallBacks);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (pageNumRef) {
|
||||||
|
CFRelease(pageNumRef);
|
||||||
|
}
|
||||||
|
if (usageNumRef) {
|
||||||
|
CFRelease(usageNumRef);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!retval) {
|
||||||
|
*okay = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
return retval;
|
||||||
|
}
|
||||||
|
|
||||||
|
static bool
|
||||||
|
CreateHIDManager(void)
|
||||||
|
{
|
||||||
|
bool retval = false;
|
||||||
|
int okay = 1;
|
||||||
|
const void *vals[] = {
|
||||||
|
(void *) CreateHIDDeviceMatchDictionary(kHIDPage_GenericDesktop, kHIDUsage_GD_Joystick, &okay),
|
||||||
|
(void *) CreateHIDDeviceMatchDictionary(kHIDPage_GenericDesktop, kHIDUsage_GD_GamePad, &okay),
|
||||||
|
(void *) CreateHIDDeviceMatchDictionary(kHIDPage_GenericDesktop, kHIDUsage_GD_MultiAxisController, &okay),
|
||||||
|
};
|
||||||
|
const size_t numElements = sizeof(vals) / sizeof(vals[0]);
|
||||||
|
CFArrayRef array = okay ? CFArrayCreate(kCFAllocatorDefault, vals, numElements, &kCFTypeArrayCallBacks) : NULL;
|
||||||
|
size_t i;
|
||||||
|
|
||||||
|
for (i = 0; i < numElements; i++) {
|
||||||
|
if (vals[i]) {
|
||||||
|
CFRelease((CFTypeRef) vals[i]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (array) {
|
||||||
|
hidman = IOHIDManagerCreate(kCFAllocatorDefault, kIOHIDOptionsTypeNone);
|
||||||
|
if (hidman != NULL) {
|
||||||
|
retval = ConfigHIDManager(array);
|
||||||
|
}
|
||||||
|
CFRelease(array);
|
||||||
|
}
|
||||||
|
|
||||||
|
return retval;
|
||||||
|
}
|
||||||
|
|
||||||
|
void SDL_JoystickRun(void)
|
||||||
|
{
|
||||||
|
recDevice *device = gpDeviceList;
|
||||||
|
while (device) {
|
||||||
|
if (device->removed) {
|
||||||
|
device = FreeDevice(device);
|
||||||
|
} else {
|
||||||
|
SDL_SYS_JoystickUpdate(&device->joystick);
|
||||||
|
device = device->pNext;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* run this after the checks above so we don't set device->removed and delete the device before
|
||||||
|
SDL_SYS_JoystickUpdate can run to clean up the SDL_Joystick object that owns this device */
|
||||||
|
while (CFRunLoopRunInMode(SDL_JOYSTICK_RUNLOOP_MODE,0,TRUE) == kCFRunLoopRunHandledSource) {
|
||||||
|
/* no-op. Pending callbacks will fire in CFRunLoopRunInMode(). */
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void __attribute__((constructor)) SDL_SYS_JoystickInit(void)
|
||||||
|
{
|
||||||
|
if (!CreateHIDManager()) {
|
||||||
|
fprintf(stderr, "Joystick: Couldn't initialize HID Manager");
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
[[NSRunLoop mainRunLoop] addTimer:
|
||||||
|
[NSTimer timerWithTimeInterval:1/120.0 repeats:YES block:^(NSTimer * _Nonnull timer) {
|
||||||
|
SDL_JoystickRun();
|
||||||
|
}] forMode:NSDefaultRunLoopMode];
|
||||||
|
}
|
||||||
|
}
|
2
Makefile
2
Makefile
|
@ -175,7 +175,7 @@ $(BIN)/SameBoy.app: $(BIN)/SameBoy.app/Contents/MacOS/SameBoy \
|
||||||
|
|
||||||
$(BIN)/SameBoy.app/Contents/MacOS/SameBoy: $(CORE_OBJECTS) $(COCOA_OBJECTS)
|
$(BIN)/SameBoy.app/Contents/MacOS/SameBoy: $(CORE_OBJECTS) $(COCOA_OBJECTS)
|
||||||
-@$(MKDIR) -p $(dir $@)
|
-@$(MKDIR) -p $(dir $@)
|
||||||
$(CC) $^ -o $@ $(LDFLAGS) -framework OpenGL -framework AudioUnit -framework AVFoundation -framework CoreVideo -framework CoreMedia
|
$(CC) $^ -o $@ $(LDFLAGS) -framework OpenGL -framework AudioUnit -framework AVFoundation -framework CoreVideo -framework CoreMedia -framework IOKit
|
||||||
ifeq ($(CONF), release)
|
ifeq ($(CONF), release)
|
||||||
strip $@
|
strip $@
|
||||||
endif
|
endif
|
||||||
|
|
Loading…
Reference in New Issue