Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 9 additions & 4 deletions Docs/Switch2Kit.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,9 @@ Switch2Kit's in-process virtual gamepads. No console emulation code is changed.

Use a Dolphin application built with this option (not an ordinary upstream build).
Close other applications that are managing the same controller. In Dolphin's
Controller Settings, click **Find Switch 2 Controllers**, allow Bluetooth access,
and hold Sync on the wireless controller. Discovery lasts 60 seconds.
Controller Settings, find the **Switch 2 Controllers** section above **Common**.
Click **Find Controllers**, allow Bluetooth access, and hold Sync on the wireless
controller. Discovery lasts 60 seconds.

Choose the connected **Switch2Kit GameCube** or **Switch2Kit Pro Controller 2**
in the physical-controller dropdown beside the desired GameCube port. This selects
Expand All @@ -21,6 +22,10 @@ can be assigned to only one active Standard Controller port through this shortcu
set its old port to None before moving it. Other controller types and backends
continue to use Configure normally.

Pairing stays separate from the emulated controller type: **Standard Controller**
is what the game sees, while the adjacent dropdown selects the physical input
device. Changing the type does not start Bluetooth discovery.

In **Configure**, **Use Recommended Mapping** applies the same mapping to the
selected supported device. It is explicit: selecting a device, refreshing the list,
or reconnecting a controller does not overwrite bindings. Replacing custom buttons,
Expand All @@ -38,7 +43,7 @@ This is not wired GameCube USB-adapter mode.

### Automatic connection and recovery

Enable **Automatically connect Switch 2 controllers** in Controller Settings.
Enable **Automatically connect** in the **Switch 2 Controllers** section.
This starts listening now and saves your choice for future Dolphin launches. After
initial pairing, turn the controller on again after a long pause: Dolphin can
rediscover it without reopening settings or pressing Find. Discovery runs on the
Expand All @@ -50,7 +55,7 @@ and merely launching Dolphin does not start Bluetooth or request permission.
With it on, Dolphin starts once on the main run loop after SDL initialization.
The status distinguishes continuous listening from a finite manual search.

**Disconnect Switch 2 Controllers** stops input and discovery for the current
**Disconnect All** in that section stops input and discovery for the current
session, even with this option checked. Polling, returning to the app, resuming a
game or reopening settings cannot undo that explicit stop. Use Find or re-enable
the option to resume; the saved option still applies on the next app launch.
Expand Down
4 changes: 2 additions & 2 deletions Readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,11 +18,11 @@ These are development builds, not notarized releases. For an unverified-develope

### Connect and play

1. Turn on your Mac's Bluetooth and close other apps managing the controller, including the Switch2Kit dashboard or Cemu. In Dolphin, open **Controllers** (Controller Settings), click **Find Switch 2 Controllers**, allow Bluetooth access, and hold the controller's **Sync** button until its player lights sweep. The search lasts 60 seconds; click Find again to retry.
1. Turn on your Mac's Bluetooth and close other apps managing the controller, including the Switch2Kit dashboard or Cemu. In Dolphin, open **Controllers** (Controller Settings). In the **Switch 2 Controllers** section, click **Find Controllers**, allow Bluetooth access, and hold the controller's **Sync** button until its player lights sweep. The search lasts 60 seconds; click Find again to retry.
2. Beside the desired **GameCube port**, choose **Switch2Kit GameCube** or **Switch2Kit Pro Controller 2** in the physical-controller dropdown. Dolphin selects **Standard Controller** and applies the button, stick, trigger, and rumble mappings automatically. This is not **GameCube Adapter for Wii U** mode.
3. Open that port's **Configure** window to check button presses and releases, sticks, and triggers, then open your GameCube game. On the NSO GameCube controller, partial L/R travel and the full-click buttons are separate inputs. Pro Controller ZL/ZR are on/off and cannot reproduce an analog squeeze.

For automatic reconnection on later launches or after a long pause, enable **Automatically connect Switch 2 controllers** in Controller Settings and turn the controller on when you return. This option is off by default; otherwise use **Find Switch 2 Controllers** each session. **Disconnect Switch 2 Controllers** stops the current session without deleting mappings.
For automatic reconnection on later launches or after a long pause, enable **Automatically connect** in the **Switch 2 Controllers** section and turn the controller on when you return. This option is off by default; otherwise use **Find Controllers** each session. **Disconnect All** in that section stops the current session without deleting mappings.

For Wii games, configure an **Emulated Wii Remote** and its SDL device through Dolphin's normal Wii Remote settings; the GameCube-port shortcut above does not configure a Wii Remote or add calibrated Wii motion. Individual Joy-Con 2 halves also need normal manual bindings.

Expand Down
137 changes: 80 additions & 57 deletions Source/Core/DolphinQt/Config/ControllersPane.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,15 @@

#ifdef HAVE_SWITCH2KIT
#include <QCheckBox>
#include <QHBoxLayout>
#include <QGridLayout>
#include <QGroupBox>
#include <QLabel>
#include <QMessageBox>
#include <QPushButton>
#include <QSignalBlocker>
#include <QTimer>

#include "DolphinQt/QtUtils/NonDefaultQPushButton.h"
#include "InputCommon/ControllerInterface/SDL/Switch2Kit.h"
#endif

Expand All @@ -32,56 +34,55 @@ void ControllersPane::CreateMainLayout()

auto* const gamecube_controllers = new GamecubeControllersWidget(this);
m_wiimote_controllers = new WiimoteControllersWidget(this);
auto* const common = new CommonControllersWidget(this);

layout->addWidget(gamecube_controllers);
layout->addWidget(m_wiimote_controllers);
#ifdef HAVE_SWITCH2KIT
layout->addWidget(CreateSwitch2ControllersBox());
#endif
auto* const common = new CommonControllersWidget(this);
layout->addWidget(common);
layout->addStretch(1);
}

#ifdef HAVE_SWITCH2KIT
auto* const actions = new QHBoxLayout;
auto* const find = new QPushButton(tr("Find Switch 2 Controllers"), this);
auto* const stop = new QPushButton(tr("Disconnect Switch 2 Controllers"), this);
actions->addWidget(find);
actions->addWidget(stop);
layout->addLayout(actions);
auto* const auto_connect = new QCheckBox(tr("Automatically connect Switch 2 controllers"), this);
QGroupBox* ControllersPane::CreateSwitch2ControllersBox()
{
// Discovery manages physical input devices, not emulated controller types or port mappings.
auto* const box = new QGroupBox(tr("Switch 2 Controllers"), this);
auto* const layout = new QGridLayout(box);
layout->setVerticalSpacing(7);
layout->setColumnStretch(0, 1);

auto* const auto_connect = new QCheckBox(tr("Automatically connect"), box);
auto_connect->setAccessibleName(tr("Automatically connect Switch 2 controllers"));
auto_connect->setToolTip(
tr("Listen for available supported controllers while Dolphin is open, including after a "
"controller powers off. This uses Bluetooth and starts automatically on future launches. "
"Disconnect stops it until you use Find or restart Dolphin. Mappings are not changed."));
auto_connect->setChecked(ciface::SDL::GetSwitch2KitStatus().auto_connect);
layout->addWidget(auto_connect);
auto* const status = new QLabel(this);
tr("Listen for supported controllers while Dolphin is open, including on future launches. "
"This uses Bluetooth. Turning this off keeps connected controllers and saved mappings."));
auto* const find = new NonDefaultQPushButton(tr("Find Controllers"), box);
find->setAccessibleName(tr("Find Switch 2 controllers"));
find->setToolTip(tr("Search for supported Switch 2 controllers. Hold the controller's SYNC "
"button for first-time pairing."));
auto* const stop = new NonDefaultQPushButton(tr("Disconnect All"), box);
stop->setAccessibleName(tr("Disconnect all Switch 2 controllers"));
stop->setToolTip(tr("Disconnect all Switch 2 controllers and stop searching for this session. "
"Saved mappings and the automatic connection setting are not changed."));
layout->addWidget(auto_connect, 0, 0);
layout->addWidget(find, 0, 1);
layout->addWidget(stop, 0, 2);

auto* const status = new QLabel(box);
status->setAccessibleName(tr("Switch 2 connection status"));
status->setWordWrap(true);
layout->addWidget(status);
layout->addWidget(status, 1, 0, 1, 3);
auto* const help = new QLabel(
tr("Hold Sync to pair, then choose your GameCube or Pro controller next to a port above. "
"Recommended controls and rumble are applied for you. Configure is only needed for "
"custom mappings. No separate controller app is needed."),
this);
tr("Turn on your controller to reconnect, or hold SYNC to pair. "
"Choose it next to a GameCube port above to apply recommended controls and rumble. "
"Use Configure for custom mappings."),
box);
help->setWordWrap(true);
layout->addWidget(help);
connect(find, &QPushButton::clicked, this, [this] {
const int result = ciface::SDL::FindSwitch2Controllers();
if (result != 0)
QMessageBox::warning(this, tr("Switch 2 Controllers"),
tr("Controller discovery could not start (error %1). Check Bluetooth "
"permission and close other controller apps. If disconnecting, "
"wait for it to finish before trying again.")
.arg(result));
});
connect(stop, &QPushButton::clicked, this, [] { ciface::SDL::StopSwitch2Controllers(); });
connect(auto_connect, &QCheckBox::toggled, this, [this, auto_connect](bool enabled) {
const int result = ciface::SDL::SetSwitch2KitAutoConnect(enabled);
const QSignalBlocker blocker(auto_connect);
auto_connect->setChecked(ciface::SDL::GetSwitch2KitStatus().auto_connect);
if (result != 0)
QMessageBox::warning(this, tr("Switch 2 Controllers"),
tr("The automatic connection setting could not be saved or applied "
"(error %1). Check configuration access and Bluetooth permission. "
"The checkbox shows the saved choice; use Find to retry connection.")
.arg(result));
});
layout->addWidget(help, 2, 0, 1, 3);

const auto update_status = [status, find, stop, auto_connect] {
const auto state = ciface::SDL::GetSwitch2KitStatus();
find->setEnabled(state.available && !state.stopping);
Expand All @@ -100,27 +101,49 @@ void ControllersPane::CreateMainLayout()
else if (state.bluetooth == ciface::SDL::Switch2KitBluetooth::Unsupported)
status->setText(tr("Bluetooth is not supported on this Mac."));
else if (state.error != 0)
status->setText(tr("Controller input error %1. Use Find to retry.").arg(state.error));
status->setText(
tr("Controller input error %1. Use Find Controllers to retry.").arg(state.error));
else if (!state.running)
status->setText(tr("Switch 2 controller support is stopped. Use Find to resume."));
status->setText(tr("Disconnected. Use Find Controllers to connect."));
else if (state.scanning && state.auto_connect)
status->setText(tr("Listening for Switch 2 controllers. Turn it on to reconnect; "
"hold Sync for initial pairing. Connected: %1.")
.arg(state.controllers));
status->setText(tr("Listening for controllers. Connected: %1.").arg(state.controllers));
else if (state.scanning)
status->setText(
tr("Searching for 60 seconds: hold Sync. Connected: %1.").arg(state.controllers));
status->setText(tr("Searching for 60 seconds. Connected: %1.").arg(state.controllers));
else if (state.auto_connect)
status->setText(tr("Automatic connection is enabled. Connected Switch 2 controllers: %1.")
.arg(state.controllers));
status->setText(tr("Automatic connection enabled. Connected: %1.").arg(state.controllers));
else
status->setText(tr("Connected Switch 2 controllers: %1. Use Find to add another.")
.arg(state.controllers));
status->setText(
tr("Connected: %1. Use Find Controllers to add another.").arg(state.controllers));
};
auto* const timer = new QTimer(this);
connect(timer, &QTimer::timeout, this, update_status);
auto* const timer = new QTimer(box);
connect(timer, &QTimer::timeout, box, update_status);
timer->start(500);

connect(find, &QPushButton::clicked, box, [this, update_status] {
const int result = ciface::SDL::FindSwitch2Controllers();
update_status();
if (result != 0)
QMessageBox::warning(this, tr("Switch 2 Controllers"),
tr("Controller discovery could not start (error %1). Check Bluetooth "
"permission and close other controller apps. If disconnecting, "
"wait for it to finish before trying again.")
.arg(result));
});
connect(stop, &QPushButton::clicked, box, [update_status] {
ciface::SDL::StopSwitch2Controllers();
update_status();
});
connect(auto_connect, &QCheckBox::toggled, box, [this, update_status](bool enabled) {
const int result = ciface::SDL::SetSwitch2KitAutoConnect(enabled);
update_status();
if (result != 0)
QMessageBox::warning(this, tr("Switch 2 Controllers"),
tr("The automatic connection setting could not be saved or applied "
"(error %1). Check configuration access and Bluetooth permission. "
"The checkbox shows the saved choice; use Find Controllers to retry.")
.arg(result));
});
update_status();
#endif
layout->addStretch(1);
return box;
}
#endif
4 changes: 4 additions & 0 deletions Source/Core/DolphinQt/Config/ControllersPane.h
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@

#include <QWidget>

class QGroupBox;
class WiimoteControllersWidget;

class ControllersPane final : public QWidget
Expand All @@ -15,6 +16,9 @@ class ControllersPane final : public QWidget

private:
void CreateMainLayout();
#ifdef HAVE_SWITCH2KIT
QGroupBox* CreateSwitch2ControllersBox();
#endif

WiimoteControllersWidget* m_wiimote_controllers;
};
17 changes: 16 additions & 1 deletion Tools/test_switch2kit_autoconnect.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,9 +31,24 @@ def test_checkbox_updates_do_not_start_or_remap_controllers(self):
update = pane.split("const auto update_status =", 1)[1].split("auto* const timer", 1)[0]
self.assertIn("QSignalBlocker", update)
self.assertIn("state.scanning && state.auto_connect", update)
for action in ("FindSwitch2Controllers", "SetSwitch2KitAutoConnect", "Mapping::Apply"):
for action in ("FindSwitch2Controllers", "StopSwitch2Controllers",
"SetSwitch2KitAutoConnect", "Mapping::Apply"):
self.assertNotIn(action, update)

def test_section_initialization_does_not_change_connection_policy(self):
pane = self.read("Source/Core/DolphinQt/Config/ControllersPane.cpp")
initialization = pane.split("connect(find,", 1)[0]
for action in ("FindSwitch2Controllers(", "StopSwitch2Controllers(",
"SetSwitch2KitAutoConnect(", "StartSwitch2KitAutoConnect(", "Mapping::Apply"):
self.assertNotIn(action, initialization)

def test_user_actions_refresh_the_displayed_backend_state(self):
pane = self.read("Source/Core/DolphinQt/Config/ControllersPane.cpp")
# Reflect explicit stop and saved-setting failures before the next timer tick.
for action in ("FindSwitch2Controllers()", "StopSwitch2Controllers()",
"SetSwitch2KitAutoConnect(enabled)"):
self.assertIn(f"{action};\n update_status();", pane)

def test_polling_and_initialization_never_start_bluetooth(self):
native = self.read("Source/Core/InputCommon/ControllerInterface/SDL/Switch2Kit.cpp")
initialize = native.split("void InitializeSwitch2Kit()", 1)[1].split("int Find", 1)[0]
Expand Down
Loading