Skip to content

Commit 2deb68b

Browse files
committed
Implement hot plugging support and connect/disconnect events for InputDevices
Currently this is only supported by XInput - Reorder some methods
1 parent 3d29182 commit 2deb68b

File tree

12 files changed

+399
-243
lines changed

12 files changed

+399
-243
lines changed

src/main/java/de/gurkenlabs/input4j/AbstractInputDevicePlugin.java

Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,22 @@
11
package de.gurkenlabs.input4j;
22

33
import java.util.Collection;
4+
import java.util.concurrent.ConcurrentHashMap;
5+
import java.util.function.Consumer;
46
import java.util.logging.Logger;
57

68
public abstract class AbstractInputDevicePlugin implements InputDevicePlugin {
79
protected static final Logger log = Logger.getLogger(AbstractInputDevicePlugin.class.getPackage().getName());
10+
private final Collection<Consumer<InputDevice>> deviceConnectedListeners = ConcurrentHashMap.newKeySet();
11+
private final Collection<Consumer<InputDevice>> deviceDisconnectedListeners = ConcurrentHashMap.newKeySet();
12+
private final Collection<Runnable> devicesChangedListeners = ConcurrentHashMap.newKeySet();
813

14+
private final int hotPlugInterval;
15+
private long lastDeviceUpdate;
916
private Collection<InputDevice> devices;
1017

1118
protected AbstractInputDevicePlugin() {
19+
this.hotPlugInterval = InputDevices.configure().getHotPlugInterval();
1220
}
1321

1422
/**
@@ -25,6 +33,20 @@ public Collection<InputDevice> getAll() {
2533
return this.devices;
2634
}
2735

36+
/**
37+
* Closes the plugin and clears the collection of devices.
38+
*/
39+
@Override
40+
public void close() {
41+
if (this.devices != null) {
42+
this.devices.forEach(InputDevice::close);
43+
}
44+
45+
deviceConnectedListeners.clear();
46+
deviceDisconnectedListeners.clear();
47+
devicesChangedListeners.clear();
48+
}
49+
2850
/**
2951
* Sets the devices that are managed by this plugin.
3052
* <p>
@@ -34,5 +56,76 @@ public Collection<InputDevice> getAll() {
3456
*/
3557
protected void setDevices(Collection<InputDevice> devices) {
3658
this.devices = devices;
59+
this.lastDeviceUpdate = System.currentTimeMillis();
60+
}
61+
62+
/**
63+
* Refreshes the list of input devices.
64+
* <p>
65+
* This method needs to be called explicitly to support hot-plugging devices.
66+
* If a new device is connected or an existing device is disconnected, the list of input devices is updated accordingly.
67+
* </p>
68+
* <p>
69+
* IMPORTANT: This is a costly operation and should be called periodically to ensure that the list of input devices is up-to-date.
70+
* This method should not be called in the same interval as the polling of input devices.
71+
* </p>
72+
* <p>
73+
* This also triggers the {@link #onDeviceConnected(Consumer)} and {@link #onDeviceDisconnected(Consumer)} events when necessary.
74+
* </p>
75+
*/
76+
protected void refreshDevices() {
77+
if (this.lastDeviceUpdate == 0 || System.currentTimeMillis() - this.lastDeviceUpdate < this.hotPlugInterval) {
78+
return;
79+
}
80+
81+
this.lastDeviceUpdate = System.currentTimeMillis();
82+
final var oldDeviceIds = this.getAll().stream().map(InputDevice::getID).toList();
83+
var refreshedDevices = this.refreshInputDevices();
84+
var refreshedDeviceIds = refreshedDevices.stream().map(InputDevice::getID).toList();
85+
86+
var devicesChanged = false;
87+
// Check for disconnected devices
88+
for (var currentDeviceId : oldDeviceIds) {
89+
if (!refreshedDeviceIds.contains(currentDeviceId)) {
90+
// Device was disconnected
91+
var disconnectedDevice = this.devices.stream().filter(d -> d.getID().equals(currentDeviceId)).findFirst().orElse(null);
92+
if (disconnectedDevice != null) {
93+
this.deviceDisconnectedListeners.forEach(listener -> listener.accept(disconnectedDevice));
94+
devicesChanged = true;
95+
}
96+
}
97+
}
98+
99+
// Check for newly connected devices
100+
for (var connectedDeviceId : refreshedDeviceIds) {
101+
if (!oldDeviceIds.contains(connectedDeviceId)) {
102+
// New device connected
103+
InputDevice connectedDevice = refreshedDevices.stream().filter(d -> d.getID().equals(connectedDeviceId)).findFirst().orElse(null);
104+
if (connectedDevice != null) {
105+
this.deviceConnectedListeners.forEach(listener -> listener.accept(connectedDevice));
106+
devicesChanged = true;
107+
}
108+
}
109+
}
110+
111+
if (devicesChanged) {
112+
this.devicesChangedListeners.forEach(Runnable::run);
113+
}
114+
115+
this.setDevices(refreshedDevices);
116+
}
117+
118+
protected abstract Collection<InputDevice> refreshInputDevices();
119+
120+
public void onDevicesChanged(Runnable listener) {
121+
this.devicesChangedListeners.add(listener);
122+
}
123+
124+
public void onDeviceConnected(Consumer<InputDevice> listener) {
125+
this.deviceConnectedListeners.add(listener);
126+
}
127+
128+
public void onDeviceDisconnected(Consumer<InputDevice> listener) {
129+
this.deviceDisconnectedListeners.add(listener);
37130
}
38131
}

src/main/java/de/gurkenlabs/input4j/InputDevice.java

Lines changed: 18 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@
3232
*/
3333
public final class InputDevice implements Closeable {
3434
private final String identifier;
35-
private final String instanceName;
35+
private final String name;
3636
private final String productName;
3737
private final List<InputComponent> components = new CopyOnWriteArrayList<>();
3838
private final Collection<InputDeviceListener> listeners = ConcurrentHashMap.newKeySet();
@@ -49,14 +49,14 @@ public final class InputDevice implements Closeable {
4949
* Creates a new instance of the InputDevice class.
5050
*
5151
* @param identifier the identifier of the input device
52-
* @param instanceName the name of the instance of the input device
52+
* @param name the name of the instance of the input device
5353
* @param productName the name of the product of the input device
5454
* @param pollCallback the function to be called when polling for input data from the device
5555
* @param rumbleCallback the function to be called when setting rumble intensity
5656
*/
57-
public InputDevice(String identifier, String instanceName, String productName, Function<InputDevice, float[]> pollCallback, BiConsumer<InputDevice, float[]> rumbleCallback) {
57+
public InputDevice(String identifier, String name, String productName, Function<InputDevice, float[]> pollCallback, BiConsumer<InputDevice, float[]> rumbleCallback) {
5858
this.identifier = identifier;
59-
this.instanceName = instanceName;
59+
this.name = name;
6060
this.productName = productName;
6161
this.pollCallback = pollCallback;
6262
this.rumbleCallback = rumbleCallback;
@@ -81,8 +81,8 @@ public String getID() {
8181
*
8282
* @return the instance name
8383
*/
84-
public String getInstanceName() {
85-
return instanceName;
84+
public String getName() {
85+
return name;
8686
}
8787

8888
/**
@@ -239,14 +239,6 @@ public void rumble(float... intensity) {
239239

240240
this.rumbleCallback.accept(this, intensity);
241241
}
242-
243-
@Override
244-
public void close() {
245-
this.listeners.clear();
246-
this.buttonPressedListeners.clear();
247-
this.buttonReleasedListeners.clear();
248-
}
249-
250242
public void setAccuracy(int decimalPlaces) {
251243
if (decimalPlaces < 0) {
252244
throw new IllegalArgumentException("Decimal places must be a non-negative integer.");
@@ -255,6 +247,13 @@ public void setAccuracy(int decimalPlaces) {
255247
this.accuracyFactor = (float) Math.pow(10, Math.min(decimalPlaces, 7));
256248
}
257249

250+
@Override
251+
public void close() {
252+
this.listeners.clear();
253+
this.buttonPressedListeners.clear();
254+
this.buttonReleasedListeners.clear();
255+
}
256+
258257
/**
259258
* Checks if the input device has any input data.
260259
*
@@ -264,6 +263,11 @@ public boolean hasInputData() {
264263
return this.hasInputData;
265264
}
266265

266+
@Override
267+
public String toString() {
268+
return this.getName();
269+
}
270+
267271
public void onInputValueChanged(InputDeviceListener listener) {
268272
this.listeners.add(listener);
269273
}

src/main/java/de/gurkenlabs/input4j/InputDevicePlugin.java

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
import java.awt.*;
44
import java.io.Closeable;
55
import java.util.Collection;
6+
import java.util.function.Consumer;
67

78
/**
89
* Represents a plugin that provides input devices.
@@ -38,4 +39,10 @@ public interface InputDevicePlugin extends Closeable {
3839
* @return A collection of {@link InputDevice} objects representing the available input devices of this system.
3940
*/
4041
Collection<InputDevice> getAll();
42+
43+
void onDevicesChanged(Runnable listener);
44+
45+
void onDeviceConnected(Consumer<InputDevice> listener);
46+
47+
void onDeviceDisconnected(Consumer<InputDevice> listener);
4148
}

src/main/java/de/gurkenlabs/input4j/InputDevices.java

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,8 @@ public static InputDevicePlugin init(Frame owner) throws IOException {
5353
/**
5454
* Initializes the input device provider with the specified library.
5555
* <p>
56-
* Note: Some controllers don't support background mode which is why it can be necessary to pass a frame owner to the {@link InputDevices#init(Frame, String)} method.
56+
* Note: Some controllers don't support background mode which is why it can be necessary to pass a frame owner to the {@link InputDevices#init(Frame, String)} method.
57+
*
5758
* @param library The library to be used.
5859
* @return The initialized input device provider.
5960
* @throws IOException if the input device provider cannot be initialized.
@@ -65,7 +66,8 @@ public static InputDevicePlugin init(InputLibrary library) throws IOException {
6566
/**
6667
* Initializes the input device provider with the specified input plugin class.
6768
* <p>
68-
* Note: Some controllers don't support background mode which is why it can be necessary to pass a frame owner to the {@link InputDevices#init(Frame, String)} method.
69+
* Note: Some controllers don't support background mode which is why it can be necessary to pass a frame owner to the {@link InputDevices#init(Frame, String)} method.
70+
*
6971
* @param inputPluginClass The input plugin class to be used.
7072
* @return The initialized input device provider.
7173
* @throws IOException if the input device provider cannot be initialized.
@@ -214,16 +216,21 @@ public static final class DefaultInputConfiguration {
214216
// default polling rate in hertz (times per second)
215217
private static final int DEFAULT_POLLING_RATE = 100;
216218

219+
private static final int DEFAULT_HOTPLUG_INTERVAL = 3000;
220+
217221
private int pollingRate;
218222
private boolean pollingEnabled;
219223

220224
private int accuracy;
221225

226+
private int hotplugInterval;
227+
222228
private DefaultInputConfiguration() {
223229
this.pollingRate = DEFAULT_POLLING_RATE;
224230
this.pollingEnabled = false;
225231

226232
this.accuracy = DEFAULT_ACCURACY;
233+
this.hotplugInterval = DEFAULT_HOTPLUG_INTERVAL;
227234
}
228235

229236
/**
@@ -285,5 +292,13 @@ public boolean isPollingEnabled() {
285292
public void enablePolling(boolean pollingEnabled) {
286293
this.pollingEnabled = pollingEnabled;
287294
}
295+
296+
public int getHotPlugInterval() {
297+
return hotplugInterval;
298+
}
299+
300+
public void setHotPlugInterval(int hotplugInterval) {
301+
this.hotplugInterval = hotplugInterval;
302+
}
288303
}
289304
}

src/main/java/de/gurkenlabs/input4j/examples/ExampleEventBasedInputHandling.java

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,9 @@
99
public class ExampleEventBasedInputHandling {
1010
public static void main(String[] args) throws IOException, InterruptedException {
1111
try (var devices = InputDevices.init()) {
12+
13+
devices.onDeviceConnected(inputDevice -> System.out.println("Device connected " + inputDevice));
14+
devices.onDeviceDisconnected(inputDevice -> System.out.println("Device disconnected " + inputDevice));
1215
var device = devices.getAll().stream().findFirst().orElse(null);
1316
if (device == null) {
1417
System.out.println("No input devices found.");

src/main/java/de/gurkenlabs/input4j/examples/ExamplePollAllInputDevicesManually.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ public static void main(String[] args) throws IOException {
2727
continue;
2828
}
2929

30-
System.out.println(inputDevice.getInstanceName() + ":" + inputDevice.getComponents().stream().filter(x -> x.getData() != 0).toList());
30+
System.out.println(inputDevice.getName() + ":" + inputDevice.getComponents().stream().filter(x -> x.getData() != 0).toList());
3131

3232
// Rumble the device if the X button is pressed
3333
// handleRumble(inputDevice);
@@ -61,4 +61,4 @@ private static void handleRumble(InputDevice inputDevice) {
6161
}
6262
});
6363
}
64-
}
64+
}

0 commit comments

Comments
 (0)