Global hotkeys, in TeamSpeak's own shape
Bindings are captured system-wide rather than only while the window has focus: X11's RECORD extension where the X server sees every key, and a /dev/input reader as the Wayland fallback. Any key can act as a modifier, mouse buttons included, as TS3 allows. The action catalogue, its three categories and the "advanced actions" split are reverse-engineered from the original client; actions this client cannot perform are listed but greyed out. Push-to-talk becomes one of these hotkeys, so the old focus-bound pushToTalkKey setting is gone and the Voice Activation button edits that binding instead. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,94 @@
|
||||
package com.ts3client.ui;
|
||||
|
||||
import com.ts3client.hotkey.GlobalInputHook;
|
||||
import com.ts3client.hotkey.Hotkey;
|
||||
import com.ts3client.hotkey.HotkeyAction;
|
||||
import com.ts3client.hotkey.HotkeyCombo;
|
||||
import com.ts3client.hotkey.HotkeyEngine;
|
||||
import com.ts3client.hotkey.HotkeyKey;
|
||||
import com.ts3client.hotkey.Hotkeys;
|
||||
import com.ts3client.hotkey.desktop.DesktopInputHooks;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Owns the hotkey machinery for the UI: the stored bindings, the matching engine and
|
||||
* the platform input hook they are fed from. Everything the dialogs need — recording a
|
||||
* combination, naming keys, telling the user why hotkeys are dead — goes through here.
|
||||
*/
|
||||
final class HotkeyService {
|
||||
|
||||
private final Hotkeys hotkeys = Hotkeys.load();
|
||||
private final HotkeyEngine engine;
|
||||
private final GlobalInputHook hook;
|
||||
private final HotkeyArguments arguments;
|
||||
|
||||
HotkeyService(HotkeyEngine.Handler handler, HotkeyArguments arguments) {
|
||||
this.arguments = arguments;
|
||||
engine = new HotkeyEngine(hotkeys, handler);
|
||||
hook = DesktopInputHooks.start(engine);
|
||||
}
|
||||
|
||||
/** The values this action's parameter can take right now, for the action tree. */
|
||||
List<String> argumentChoices(HotkeyAction action) {
|
||||
if (arguments == null || action.argument() == HotkeyAction.Argument.NONE) return List.of();
|
||||
try {
|
||||
return arguments.choices(action);
|
||||
} catch (RuntimeException e) {
|
||||
return List.of();
|
||||
}
|
||||
}
|
||||
|
||||
List<Hotkey> all() {
|
||||
return hotkeys.all();
|
||||
}
|
||||
|
||||
void replaceAll(List<Hotkey> updated) {
|
||||
engine.releaseAll();
|
||||
hotkeys.replaceAll(updated);
|
||||
hotkeys.save();
|
||||
}
|
||||
|
||||
/** The binding for an action with no argument, or {@code null} when unbound. */
|
||||
Hotkey find(HotkeyAction action) {
|
||||
synchronized (hotkeys.all()) {
|
||||
for (Hotkey h : hotkeys.all()) {
|
||||
if (h.action == action) return h;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
boolean isRunning() {
|
||||
return hook.isRunning();
|
||||
}
|
||||
|
||||
/** One line for the options dialog: either working, or why it is not. */
|
||||
String status() {
|
||||
return hook.isRunning()
|
||||
? "Global hotkeys are active."
|
||||
: "Global hotkeys are unavailable (" + hook.unavailableReason() + ").";
|
||||
}
|
||||
|
||||
String display(HotkeyCombo combo) {
|
||||
return combo == null ? "No hotkey assigned" : combo.display(this::keyName);
|
||||
}
|
||||
|
||||
private String keyName(HotkeyKey key) {
|
||||
return hook.keyName(key);
|
||||
}
|
||||
|
||||
/** Captures the next combination instead of firing bindings; see {@link #stopRecording()}. */
|
||||
void record(HotkeyEngine.Recorder recorder) {
|
||||
engine.record(recorder);
|
||||
}
|
||||
|
||||
void stopRecording() {
|
||||
engine.stopRecording();
|
||||
}
|
||||
|
||||
void dispose() {
|
||||
engine.releaseAll();
|
||||
hook.close();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user