Skip to content
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -144,6 +144,7 @@ Buttons and axes follow the Xbox/standard gamepad layout:
| `Button` | `back`, `start`, `home` | 0.0 or 1.0 |
| `Button` | `leftStick`, `rightStick` | 0.0 or 1.0 |
| `Button` | `dpadUp/Down/Left/Right` | 0.0 or 1.0 |
| `Button` | `touchpad` | 0.0 or 1.0 |
| `Axis` | `leftStickX`, `leftStickY` | -1.0 to 1.0 |
| `Axis` | `rightStickX`, `rightStickY` | -1.0 to 1.0 |
| `Axis` | `leftTrigger`, `rightTrigger`| 0.0 to 1.0 |
Expand Down
2 changes: 2 additions & 0 deletions packages/gamepads/example/.gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,11 @@
*.swp
.DS_Store
.atom/
.build/
.buildlog/
.history
.svn/
.swiftpm/
migrate_working_dir/

# IntelliJ related
Expand Down
12 changes: 6 additions & 6 deletions packages/gamepads/example/.metadata
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
# This file should be version controlled and should not be manually edited.

version:
revision: "ff37bef603469fb030f2b72995ab929ccfc227f0"
revision: "44a626f4f0027bc38a46dc68aed5964b05a83c18"
channel: "stable"

project_type: app
Expand All @@ -13,11 +13,11 @@ project_type: app
migration:
platforms:
- platform: root
create_revision: ff37bef603469fb030f2b72995ab929ccfc227f0
base_revision: ff37bef603469fb030f2b72995ab929ccfc227f0
- platform: linux
create_revision: ff37bef603469fb030f2b72995ab929ccfc227f0
base_revision: ff37bef603469fb030f2b72995ab929ccfc227f0
create_revision: 44a626f4f0027bc38a46dc68aed5964b05a83c18
base_revision: 44a626f4f0027bc38a46dc68aed5964b05a83c18
- platform: macos
create_revision: 44a626f4f0027bc38a46dc68aed5964b05a83c18
base_revision: 44a626f4f0027bc38a46dc68aed5964b05a83c18

# User provided section

Expand Down
4 changes: 4 additions & 0 deletions packages/gamepads/lib/src/api/gamepad_button.dart
Original file line number Diff line number Diff line change
Expand Up @@ -54,4 +54,8 @@ enum GamepadButton {

/// D-pad right.
dpadRight,

/// The touchpad button (pressing the touchpad surface, available on
/// PlayStation DualSense and DualShock controllers).
touchpad,
}
8 changes: 8 additions & 0 deletions packages/gamepads/lib/src/mappings/macos_mapping.dart
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,14 @@ class MacosMapping extends PlatformMapping {
// SF Symbols names can vary by controller, so we use contains-based
// matching on first encounter, then cache the result.
static const _buttonPatterns = <String, GamepadButton>{
// Fixed keys from native plugin — these use GCController typed
// property names instead of SF Symbols to avoid ambiguity across
// controller types (e.g. DualSense reports capsule.portrait for
// both system buttons).
'buttonMenu': GamepadButton.start,
'buttonOptions': GamepadButton.back,
'buttonHome': GamepadButton.home,
'touchpadButton': GamepadButton.touchpad,
// Xbox-style face buttons
'a.circle': GamepadButton.a,
'b.circle': GamepadButton.b,
Expand Down
22 changes: 22 additions & 0 deletions packages/gamepads/test/mappings_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -389,6 +389,28 @@ void main() {
);
});

test('normalizes system buttons from fixed keys', () {
expect(
mapping.normalizeButton('buttonMenu', 1.0)?.button,
GamepadButton.start,
);
expect(
mapping.normalizeButton('buttonOptions', 1.0)?.button,
GamepadButton.back,
);
expect(
mapping.normalizeButton('buttonHome', 1.0)?.button,
GamepadButton.home,
);
});

test('normalizes touchpad button', () {
expect(
mapping.normalizeButton('touchpadButton', 1.0)?.button,
GamepadButton.touchpad,
);
});

test('normalizes shoulder buttons', () {
expect(
mapping.normalizeButton('l1.rectangle.roundedbottom', 1.0)?.button,
Expand Down
75 changes: 59 additions & 16 deletions packages/gamepads_darwin/macos/Classes/GamepadsDarwinPlugin.swift
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,13 @@ import Cocoa
import GameController
import FlutterMacOS

enum FixedKey: String {
case buttonMenu
case buttonOptions
case buttonHome
case touchpadButton
}

public class GamepadsDarwinPlugin: NSObject, FlutterPlugin {
let channel: FlutterMethodChannel
let gamepads = GamepadsListener()
Expand Down Expand Up @@ -29,7 +36,8 @@ public class GamepadsDarwinPlugin: NSObject, FlutterPlugin {
}

private func onGamepadEvent(gamepadId: Int, gamepad: GCExtendedGamepad, element: GCControllerElement) {
for (key, value) in getValues(element: element) {
let fixedKey = getFixedKey(gamepad: gamepad, element: element)
for (key, value) in getValues(element: element, fixedKey: fixedKey) {
let arguments: [String: Any] = [
"gamepadId": String(gamepadId),
"time": Int(getTimestamp(gamepad: gamepad)),
Expand All @@ -41,30 +49,65 @@ public class GamepadsDarwinPlugin: NSObject, FlutterPlugin {
}
}

private func getValues(element: GCControllerElement) -> [(String, Float)] {
/// Returns a fixed key name for elements whose SF Symbol names are
/// ambiguous across controller types (e.g. both DualSense system
/// buttons report "capsule.portrait"). For other elements, returns
/// nil so the caller falls back to SF Symbol names.
private func getFixedKey(gamepad: GCExtendedGamepad, element: GCControllerElement) -> FixedKey? {
if element === gamepad.buttonMenu {
return .buttonMenu
}
if let opt = gamepad.buttonOptions, element === opt {
return .buttonOptions
}
if #available(macOS 11.0, *) {
if let home = gamepad.buttonHome, element === home {
return .buttonHome
}
}
if #available(macOS 11.3, *) {
if let ds = gamepad as? GCDualSenseGamepad,
element === ds.touchpadButton {
return .touchpadButton
}
}
if #available(macOS 11.0, *) {
if let ds = gamepad as? GCDualShockGamepad,
element === ds.touchpadButton {
return .touchpadButton
}
}
return nil
}

private func getValues(element: GCControllerElement, fixedKey: FixedKey? = nil) -> [(String, Float)] {
if let element = element as? GCControllerButtonInput {
var button: String = "Unknown button"
if #available(macOS 11.0, *) {
if (element.sfSymbolsName != nil) {
button = element.sfSymbolsName!
var button: String = fixedKey?.rawValue ?? "Unknown button"
if fixedKey == nil {
if #available(macOS 11.0, *) {
if let name = element.sfSymbolsName {
button = name
}
}
}

return [(button, element.value)]
} else if let element = element as? GCControllerAxisInput {
var axis: String = "Unknown axis"
if #available(macOS 11.0, *) {
if (element.sfSymbolsName != nil) {
axis = element.sfSymbolsName!
var axis: String = fixedKey?.rawValue ?? "Unknown axis"
if fixedKey == nil {
if #available(macOS 11.0, *) {
if let name = element.sfSymbolsName {
axis = name
}
}
}
return [(axis, element.value)]
} else if let element = element as? GCControllerDirectionPad {
var directionPad: String = "Unknown direction pad"

if #available(macOS 11.0, *) {
if (element.sfSymbolsName != nil) {
directionPad = element.sfSymbolsName!
var directionPad: String = fixedKey?.rawValue ?? "Unknown direction pad"
if fixedKey == nil {
if #available(macOS 11.0, *) {
if let name = element.sfSymbolsName {
directionPad = name
}
}
}
return [
Expand Down
Loading