- Mute/deafen status now goes out through clientupdate instead of clientedit, which silently rejected client_input_muted/output_muted since they are runtime status, not editable client properties. - Publish client_input_hardware so other clients see "Microphone Disabled" instead of silence while another tab holds the capture device; track input/output hardware flags for other clients too, and show a distinct grey "disabled" icon instead of reusing the red "muted" one for both the tree and the info panel. - Implement TS3's Enable/Disable/Toggle Local Mic Mute hotkeys: they silence capture like a real mute, but never touch the published mute status or play a sound. - A speaker's "talking" indicator only ever cleared when its zero-length end-of-burst voice packet arrived; if that one UDP packet was lost, the indicator stuck until their next burst. Add a watchdog that clears it after 200ms of silence from that speaker regardless.
161 lines
6.2 KiB
Java
161 lines
6.2 KiB
Java
package com.ts3client.ui;
|
|
|
|
import com.ts3client.hotkey.Hotkey;
|
|
import com.ts3client.hotkey.HotkeyEngine;
|
|
|
|
import javax.swing.SwingUtilities;
|
|
import java.util.List;
|
|
|
|
/**
|
|
* Carries a fired hotkey out on the client.
|
|
*
|
|
* <p>Activations arrive on the input hook's thread, so everything is handed to the
|
|
* event dispatch thread first. Which connections an action reaches is the binding's
|
|
* "on active server" flag: set, only the selected tab; clear, every connected one.
|
|
*/
|
|
final class HotkeyActions implements HotkeyEngine.Handler {
|
|
|
|
/** How much one press of the master-volume hotkeys moves the slider. */
|
|
private static final double VOLUME_STEP = 0.05;
|
|
|
|
private final MainFrame frame;
|
|
/** Latched push-to-talk, driven by the "Toggle Push-to-Talk" action. */
|
|
private boolean pttLatched;
|
|
|
|
HotkeyActions(MainFrame frame) {
|
|
this.frame = frame;
|
|
}
|
|
|
|
@Override
|
|
public void onHotkey(Hotkey hotkey, boolean active) {
|
|
SwingUtilities.invokeLater(() -> perform(hotkey, active));
|
|
}
|
|
|
|
private void perform(Hotkey hotkey, boolean active) {
|
|
List<ServerTab> targets = frame.hotkeyTargets(hotkey.activeServerOnly);
|
|
switch (hotkey.action) {
|
|
case CONNECT_CURRENT_TAB -> frame.connectBookmark(hotkey.argument, false);
|
|
case CONNECT_NEW_TAB -> frame.connectBookmark(hotkey.argument, true);
|
|
case DISCONNECT_CURRENT -> {
|
|
ServerTab tab = frame.selectedTab();
|
|
if (tab != null) tab.disconnect();
|
|
}
|
|
case DISCONNECT_ALL -> {
|
|
for (ServerTab tab : frame.allTabs()) tab.disconnect();
|
|
}
|
|
|
|
case MIC_ACTIVATE -> frame.moveMicrophoneToSelectedTab();
|
|
case MIC_MUTE -> setMicMuted(targets, true);
|
|
case MIC_UNMUTE -> setMicMuted(targets, false);
|
|
case MIC_TOGGLE -> setMicMuted(targets, !anyMicMuted(targets));
|
|
|
|
case MIC_LOCAL_MUTE -> setMicLocalMuted(targets, true);
|
|
case MIC_LOCAL_UNMUTE -> setMicLocalMuted(targets, false);
|
|
case MIC_LOCAL_TOGGLE -> setMicLocalMuted(targets, !anyMicLocalMuted(targets));
|
|
|
|
case SPEAKER_MUTE -> setDeafened(targets, true);
|
|
case SPEAKER_UNMUTE -> setDeafened(targets, false);
|
|
case SPEAKER_TOGGLE -> setDeafened(targets, !anyDeafened(targets));
|
|
|
|
case AWAY_SET -> setAway(targets, true, "");
|
|
case AWAY_ONLINE -> setAway(targets, false, "");
|
|
case AWAY_TOGGLE -> setAway(targets, !anyAway(targets), "");
|
|
case AWAY_TOGGLE_WITH_MESSAGE -> setAway(targets, !anyAway(targets), hotkey.argument);
|
|
|
|
case COMMANDER_ACTIVATE -> setCommander(targets, true);
|
|
case COMMANDER_DEACTIVATE -> setCommander(targets, false);
|
|
case COMMANDER_TOGGLE -> setCommander(targets, !anyCommander(targets));
|
|
|
|
// Momentary: the engine reports the release too, so the key simply holds it open.
|
|
case PTT_ACTIVATE -> frame.setPushToTalk(active || pttLatched);
|
|
case PTT_DEACTIVATE -> {
|
|
pttLatched = false;
|
|
frame.setPushToTalk(false);
|
|
}
|
|
case PTT_TOGGLE -> {
|
|
pttLatched = !pttLatched;
|
|
frame.setPushToTalk(pttLatched);
|
|
}
|
|
|
|
case CHANNEL_SWITCH -> {
|
|
for (ServerTab tab : targets) tab.joinChannelPath(hotkey.argument);
|
|
}
|
|
case SERVER_TAB_SELECT -> frame.selectTabNumber(parseIndex(hotkey.argument));
|
|
case SERVER_TAB_NEXT -> frame.stepTab(1);
|
|
case SERVER_TAB_PREVIOUS -> frame.stepTab(-1);
|
|
|
|
case SOUND_MUTE -> frame.setSoundsMuted(true);
|
|
case SOUND_UNMUTE -> frame.setSoundsMuted(false);
|
|
case SOUND_TOGGLE -> frame.setSoundsMuted(!frame.areSoundsMuted());
|
|
|
|
case VOLUME_INCREASE -> frame.adjustMasterVolume(VOLUME_STEP);
|
|
case VOLUME_DECREASE -> frame.adjustMasterVolume(-VOLUME_STEP);
|
|
|
|
case NICKNAME_CHANGE -> frame.changeNickname(hotkey.argument);
|
|
|
|
case FILEBROWSER -> frame.browseCurrentChannel();
|
|
case SKIN_RELOAD -> frame.reloadSkin();
|
|
case BRING_TO_FRONT -> frame.bringToFront();
|
|
case SEND_TO_BACK -> frame.sendToBack();
|
|
|
|
default -> {
|
|
// Listed for completeness in the action catalogue, but not implemented here.
|
|
}
|
|
}
|
|
}
|
|
|
|
private void setMicMuted(List<ServerTab> targets, boolean muted) {
|
|
for (ServerTab tab : targets) tab.setMicMuted(muted);
|
|
frame.refreshAfterHotkey();
|
|
}
|
|
|
|
private void setMicLocalMuted(List<ServerTab> targets, boolean muted) {
|
|
for (ServerTab tab : targets) tab.setMicLocalMuted(muted);
|
|
frame.refreshAfterHotkey();
|
|
}
|
|
|
|
private void setDeafened(List<ServerTab> targets, boolean deaf) {
|
|
for (ServerTab tab : targets) tab.setDeafened(deaf);
|
|
frame.refreshAfterHotkey();
|
|
}
|
|
|
|
private void setAway(List<ServerTab> targets, boolean away, String message) {
|
|
for (ServerTab tab : targets) tab.setAway(away, message == null ? "" : message);
|
|
frame.refreshAfterHotkey();
|
|
}
|
|
|
|
private void setCommander(List<ServerTab> targets, boolean commander) {
|
|
for (ServerTab tab : targets) tab.setCommander(commander);
|
|
frame.refreshAfterHotkey();
|
|
}
|
|
|
|
private static boolean anyMicMuted(List<ServerTab> tabs) {
|
|
return tabs.stream().anyMatch(ServerTab::isMicMuted);
|
|
}
|
|
|
|
private static boolean anyMicLocalMuted(List<ServerTab> tabs) {
|
|
return tabs.stream().anyMatch(ServerTab::isMicLocalMuted);
|
|
}
|
|
|
|
private static boolean anyDeafened(List<ServerTab> tabs) {
|
|
return tabs.stream().anyMatch(ServerTab::isDeafened);
|
|
}
|
|
|
|
private static boolean anyAway(List<ServerTab> tabs) {
|
|
return tabs.stream().anyMatch(ServerTab::isAway);
|
|
}
|
|
|
|
private static boolean anyCommander(List<ServerTab> tabs) {
|
|
return tabs.stream().anyMatch(ServerTab::isCommander);
|
|
}
|
|
|
|
/** @return the 1-based tab number in the argument, or 1 when it is not a number */
|
|
private static int parseIndex(String argument) {
|
|
try {
|
|
return Math.max(1, Integer.parseInt(argument.trim()));
|
|
} catch (RuntimeException e) {
|
|
return 1;
|
|
}
|
|
}
|
|
}
|