Move what the servers share out of the main window
Which server captures with the microphone, handing it over, push-to-talk and key presses reaching the right microphones, and away state and nickname applied everywhere were all worked out in MainFrame. They move to a Sessions class in core, next to ServerSession, where another frontend can use them; the window keeps only the tabs. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,153 @@
|
||||
package com.ts3client.session;
|
||||
|
||||
import com.ts3client.audio.VoiceInput;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.ExecutorService;
|
||||
import java.util.concurrent.Executors;
|
||||
|
||||
/**
|
||||
* Every server the client is on, and what they share. Only one of them can capture with the
|
||||
* microphone at a time, so it is handed from one to another here; the away state and the
|
||||
* nickname are the user's own, so they can be set on every server at once.
|
||||
*
|
||||
* <p>Driven from one thread, the frontend's.
|
||||
*/
|
||||
public final class Sessions {
|
||||
|
||||
private final List<ServerSession> sessions = new ArrayList<>();
|
||||
private final Runnable onMicrophoneMoved;
|
||||
private ServerSession microphoneOwner;
|
||||
/**
|
||||
* Opens and closes capture lines, which can block briefly, off the frontend's thread. One
|
||||
* thread, so quick successive hand-overs stop and start in the order they were made.
|
||||
*/
|
||||
private final ExecutorService handover = Executors.newSingleThreadExecutor(r -> {
|
||||
Thread t = new Thread(r, "mic-handover");
|
||||
t.setDaemon(true);
|
||||
return t;
|
||||
});
|
||||
|
||||
/** @param onMicrophoneMoved told whenever the microphone changes hands */
|
||||
public Sessions(Runnable onMicrophoneMoved) {
|
||||
this.onMicrophoneMoved = onMicrophoneMoved;
|
||||
}
|
||||
|
||||
public void add(ServerSession session) {
|
||||
sessions.add(session);
|
||||
}
|
||||
|
||||
/** Forgets a session that is being closed; the microphone moves on if it had it. */
|
||||
public void remove(ServerSession session) {
|
||||
sessions.remove(session);
|
||||
if (microphoneOwner == session) microphoneOwner = null;
|
||||
assignMicrophoneIfFree();
|
||||
}
|
||||
|
||||
public boolean anyConnected() {
|
||||
for (ServerSession s : sessions) {
|
||||
if (s.isConnected()) return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
// ---- the microphone ----
|
||||
|
||||
/** The session capturing with the microphone, or null when none is. */
|
||||
public ServerSession microphoneOwner() {
|
||||
return microphoneOwner;
|
||||
}
|
||||
|
||||
/** Moves the microphone to {@code session}, taking it from whoever held it. */
|
||||
public void giveMicrophoneTo(ServerSession session) {
|
||||
if (microphoneOwner == session) return;
|
||||
ServerSession previous = microphoneOwner;
|
||||
microphoneOwner = session;
|
||||
handover.execute(() -> {
|
||||
if (previous != null) previous.setMicrophoneActive(false);
|
||||
if (session != null) session.setMicrophoneActive(true);
|
||||
});
|
||||
onMicrophoneMoved.run();
|
||||
}
|
||||
|
||||
/** Gives the microphone to some connected session when nobody who could use it holds it. */
|
||||
public void assignMicrophoneIfFree() {
|
||||
if (microphoneOwner != null && sessions.contains(microphoneOwner) && microphoneOwner.isConnected()) return;
|
||||
ServerSession previous = microphoneOwner;
|
||||
microphoneOwner = null;
|
||||
for (ServerSession s : sessions) {
|
||||
if (s.isConnected()) {
|
||||
giveMicrophoneTo(s);
|
||||
return;
|
||||
}
|
||||
}
|
||||
if (previous != null) onMicrophoneMoved.run();
|
||||
}
|
||||
|
||||
public void connected(ServerSession session) {
|
||||
assignMicrophoneIfFree();
|
||||
}
|
||||
|
||||
/** A disconnected session cannot capture; the microphone moves on if it had it. */
|
||||
public void disconnected(ServerSession session) {
|
||||
if (microphoneOwner == session) microphoneOwner = null;
|
||||
assignMicrophoneIfFree();
|
||||
}
|
||||
|
||||
/** Opens the microphone while push-to-talk is held. */
|
||||
public void setPushToTalk(boolean talking) {
|
||||
VoiceInput mic = microphoneOf(microphoneOwner);
|
||||
if (mic != null) mic.setPushToTalk(talking);
|
||||
}
|
||||
|
||||
/** Tells every connected microphone about a key press, as TS3 tells all its connections. */
|
||||
public void keyPressed() {
|
||||
for (ServerSession s : sessions) {
|
||||
VoiceInput mic = microphoneOf(s);
|
||||
if (mic != null) mic.keyPressed();
|
||||
}
|
||||
}
|
||||
|
||||
private static VoiceInput microphoneOf(ServerSession session) {
|
||||
return session == null || !session.isConnected() ? null : session.connection().getMicrophone();
|
||||
}
|
||||
|
||||
// ---- the user's own state ----
|
||||
|
||||
public void setAwayEverywhere(boolean away, String message) {
|
||||
for (ServerSession s : sessions) {
|
||||
if (s.isConnected()) s.setAway(away, message);
|
||||
}
|
||||
}
|
||||
|
||||
/** @return true when every connected server has us away, and there is one */
|
||||
public boolean isAwayEverywhere() {
|
||||
boolean any = false;
|
||||
for (ServerSession s : sessions) {
|
||||
if (!s.isConnected()) continue;
|
||||
any = true;
|
||||
if (!s.isAway()) return false;
|
||||
}
|
||||
return any;
|
||||
}
|
||||
|
||||
/** The away message to suggest: {@code preferred}'s, else any server's that has one. */
|
||||
public String awayMessage(ServerSession preferred) {
|
||||
if (preferred != null && !preferred.awayMessage().isEmpty()) return preferred.awayMessage();
|
||||
for (ServerSession s : sessions) {
|
||||
if (s.isConnected() && !s.awayMessage().isEmpty()) return s.awayMessage();
|
||||
}
|
||||
return "";
|
||||
}
|
||||
|
||||
public void setNickname(String nickname) {
|
||||
for (ServerSession s : sessions) s.setNickname(nickname);
|
||||
}
|
||||
|
||||
/** Leaves every server, waiting until each has been told. */
|
||||
public void shutdown() {
|
||||
for (ServerSession s : new ArrayList<>(sessions)) s.shutdown();
|
||||
handover.shutdown();
|
||||
}
|
||||
}
|
||||
@@ -13,6 +13,8 @@ import com.ts3client.teamspeak.TeamSpeakImporter;
|
||||
import com.ts3client.contacts.ContactStore;
|
||||
import com.ts3client.net.ChannelNode;
|
||||
import com.ts3client.session.SelfState;
|
||||
import com.ts3client.session.ServerSession;
|
||||
import com.ts3client.session.Sessions;
|
||||
import com.ts3client.sound.SoundNotifier;
|
||||
import com.ts3client.sound.SoundPlayer;
|
||||
|
||||
@@ -37,8 +39,6 @@ import java.awt.event.WindowAdapter;
|
||||
import java.awt.event.WindowEvent;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.ExecutorService;
|
||||
import java.util.concurrent.Executors;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
|
||||
/**
|
||||
@@ -63,22 +63,16 @@ public final class MainFrame extends JFrame implements ServerTabPane.Listener {
|
||||
private final SoundPlayer soundPlayer;
|
||||
|
||||
private final List<ServerTab> tabs = new ArrayList<>();
|
||||
/** The tabs' sessions: who holds the microphone, and the state shared by every server. */
|
||||
private final Sessions sessions = new Sessions(() -> {
|
||||
refreshTabs();
|
||||
updateTray();
|
||||
});
|
||||
private final ServerTabPane tabPane = new ServerTabPane(this);
|
||||
|
||||
/** The tab whose views are on screen. */
|
||||
private ServerTab selected;
|
||||
private BanReasonPresets banReasonPresets;
|
||||
/** The tab that owns the capture device; null when nobody is capturing. */
|
||||
private ServerTab micTab;
|
||||
/**
|
||||
* Opens and closes capture lines, which can block briefly, off the EDT. One thread, so quick
|
||||
* successive hand-overs stop and start the microphone in the order they were made.
|
||||
*/
|
||||
private final ExecutorService micHandover = Executors.newSingleThreadExecutor(r -> {
|
||||
Thread t = new Thread(r, "mic-handover");
|
||||
t.setDaemon(true);
|
||||
return t;
|
||||
});
|
||||
|
||||
private MainMenuBar menuBar;
|
||||
private javax.swing.Timer statusTimer;
|
||||
@@ -89,8 +83,6 @@ public final class MainFrame extends JFrame implements ServerTabPane.Listener {
|
||||
private MainToolbar toolbar;
|
||||
private StatusBar statusBar;
|
||||
|
||||
private boolean pttPressed;
|
||||
|
||||
/** Global hotkeys: the bindings, the matching engine and the platform input hook. */
|
||||
private final HotkeyService hotkeys;
|
||||
|
||||
@@ -107,7 +99,7 @@ public final class MainFrame extends JFrame implements ServerTabPane.Listener {
|
||||
this.soundPlayer = audio.createSoundPlayer(settings);
|
||||
this.sounds.setPlayer(soundPlayer);
|
||||
this.hotkeys = new HotkeyService(new HotkeyActions(this), this::hotkeyArgumentChoices);
|
||||
hotkeys.addKeyPressListener(() -> SwingUtilities.invokeLater(this::keyPressed));
|
||||
hotkeys.addKeyPressListener(() -> SwingUtilities.invokeLater(sessions::keyPressed));
|
||||
contacts.addListener(() -> SwingUtilities.invokeLater(this::contactsChanged));
|
||||
|
||||
setIconImages(Icons.appImages());
|
||||
@@ -355,6 +347,7 @@ public final class MainFrame extends JFrame implements ServerTabPane.Listener {
|
||||
private ServerTab newTab() {
|
||||
ServerTab tab = new ServerTab(this, settings, identities, audio, sounds, contacts);
|
||||
tabs.add(tab);
|
||||
sessions.add(tab.session());
|
||||
tabPane.addTab(tab);
|
||||
refreshTabs();
|
||||
return tab;
|
||||
@@ -387,45 +380,27 @@ public final class MainFrame extends JFrame implements ServerTabPane.Listener {
|
||||
tabs.remove(tab);
|
||||
tab.session().dispose();
|
||||
tabPane.removeTab(tab);
|
||||
if (micTab == tab) micTab = null;
|
||||
if (selected == tab) {
|
||||
selected = null;
|
||||
selectTab(tabs.get(0));
|
||||
} else {
|
||||
selectTab(selected); // the pane rebuilt itself; restore the selection
|
||||
}
|
||||
assignMicrophoneIfFree();
|
||||
sessions.remove(tab.session());
|
||||
}
|
||||
|
||||
/** Moves the capture device to {@code tab}, taking it from whoever held it. */
|
||||
private void setMicTab(ServerTab tab) {
|
||||
if (micTab == tab) return;
|
||||
ServerTab previous = micTab;
|
||||
micTab = tab;
|
||||
refreshTabs();
|
||||
updateTray();
|
||||
micHandover.execute(() -> {
|
||||
if (previous != null) previous.session().setMicrophoneActive(false);
|
||||
if (tab != null) tab.session().setMicrophoneActive(true);
|
||||
});
|
||||
}
|
||||
|
||||
/** Gives the microphone to some connected tab when nobody holds it. */
|
||||
private void assignMicrophoneIfFree() {
|
||||
if (micTab != null && tabs.contains(micTab) && micTab.session().isConnected()) return;
|
||||
micTab = null;
|
||||
for (ServerTab t : tabs) {
|
||||
if (t.session().isConnected()) {
|
||||
setMicTab(t);
|
||||
return;
|
||||
}
|
||||
/** The tab that holds the capture device; null when nobody is capturing. */
|
||||
private ServerTab micTab() {
|
||||
ServerSession owner = sessions.microphoneOwner();
|
||||
for (ServerTab tab : tabs) {
|
||||
if (tab.session() == owner) return tab;
|
||||
}
|
||||
refreshTabs();
|
||||
return null;
|
||||
}
|
||||
|
||||
/** Repaints the tab labels (title, microphone marker). */
|
||||
private void refreshTabs() {
|
||||
tabPane.refresh(micTab);
|
||||
tabPane.refresh(micTab());
|
||||
}
|
||||
|
||||
// ---- callbacks from ServerTab ----
|
||||
@@ -448,15 +423,13 @@ public final class MainFrame extends JFrame implements ServerTabPane.Listener {
|
||||
|
||||
void tabConnected(ServerTab tab) {
|
||||
if (!tabs.contains(tab)) return;
|
||||
assignMicrophoneIfFree();
|
||||
sessions.connected(tab.session());
|
||||
tabUpdated(tab);
|
||||
}
|
||||
|
||||
void tabDisconnected(ServerTab tab) {
|
||||
if (!tabs.contains(tab)) return;
|
||||
// A disconnected tab cannot capture; hand the microphone on if it held it.
|
||||
if (micTab == tab) micTab = null;
|
||||
assignMicrophoneIfFree();
|
||||
sessions.disconnected(tab.session());
|
||||
tabUpdated(tab);
|
||||
}
|
||||
|
||||
@@ -488,23 +461,11 @@ public final class MainFrame extends JFrame implements ServerTabPane.Listener {
|
||||
|
||||
/** Opens the microphone while a push-to-talk hotkey is held. */
|
||||
void setPushToTalk(boolean talking) {
|
||||
pttPressed = talking;
|
||||
ServerTab tab = micTab;
|
||||
if (tab == null || !tab.session().isConnected() || tab.connection().getMicrophone() == null) return;
|
||||
tab.connection().getMicrophone().setPushToTalk(talking);
|
||||
}
|
||||
|
||||
/** Tells every connected microphone about a key press, as TS3 tells all its connections. */
|
||||
private void keyPressed() {
|
||||
for (ServerTab tab : tabs) {
|
||||
if (tab.session().isConnected() && tab.connection().getMicrophone() != null) {
|
||||
tab.connection().getMicrophone().keyPressed();
|
||||
}
|
||||
}
|
||||
sessions.setPushToTalk(talking);
|
||||
}
|
||||
|
||||
void moveMicrophoneToSelectedTab() {
|
||||
if (selected != null && selected.session().isConnected()) setMicTab(selected);
|
||||
if (selected != null && selected.session().isConnected()) sessions.giveMicrophoneTo(selected.session());
|
||||
updateToolbar();
|
||||
}
|
||||
|
||||
@@ -556,7 +517,7 @@ public final class MainFrame extends JFrame implements ServerTabPane.Listener {
|
||||
}
|
||||
settings.nickname = nickname.trim();
|
||||
settings.save();
|
||||
for (ServerTab tab : tabs) tab.session().setNickname(settings.nickname);
|
||||
sessions.setNickname(settings.nickname);
|
||||
}
|
||||
|
||||
void browseCurrentChannel() {
|
||||
@@ -652,7 +613,7 @@ public final class MainFrame extends JFrame implements ServerTabPane.Listener {
|
||||
ServerTab tab = (replaceCurrent && selected != null) ? selected : tabForNewConnection();
|
||||
// The first connection to come up takes the microphone; later ones are muted
|
||||
// until the user activates them.
|
||||
if (micTab == null) setMicTab(tab);
|
||||
if (sessions.microphoneOwner() == null) sessions.giveMicrophoneTo(tab.session());
|
||||
tab.session().switchTo(address, port, nickname, password, identityId, channel, channelPassword);
|
||||
}
|
||||
|
||||
@@ -694,16 +655,17 @@ public final class MainFrame extends JFrame implements ServerTabPane.Listener {
|
||||
/** The away button's drop-down: the global actions, the presets and their editor. */
|
||||
private JPopupMenu buildAwayMenu() {
|
||||
JPopupMenu menu = new JPopupMenu();
|
||||
boolean anyConnected = tabs.stream().anyMatch(t -> t.session().isConnected());
|
||||
boolean anyConnected = sessions.anyConnected();
|
||||
|
||||
JCheckBoxMenuItem globally = new JCheckBoxMenuItem("Set Globally Away", Icons.of("AWAY"), isGloballyAway());
|
||||
JCheckBoxMenuItem globally = new JCheckBoxMenuItem("Set Globally Away", Icons.of("AWAY"),
|
||||
sessions.isAwayEverywhere());
|
||||
globally.setEnabled(anyConnected);
|
||||
globally.addActionListener(e -> setAwayEverywhere(globally.isSelected(), null));
|
||||
|
||||
JMenuItem globalStatus = new JMenuItem("Set Globally Away Status…", Icons.of("EDIT"));
|
||||
globalStatus.setEnabled(anyConnected);
|
||||
globalStatus.addActionListener(e -> {
|
||||
String message = askAwayMessage(currentAwayMessage());
|
||||
String message = askAwayMessage(sessions.awayMessage(selected == null ? null : selected.session()));
|
||||
if (message != null) setAwayEverywhere(true, message);
|
||||
});
|
||||
|
||||
@@ -741,32 +703,10 @@ public final class MainFrame extends JFrame implements ServerTabPane.Listener {
|
||||
}
|
||||
|
||||
private void setAwayEverywhere(boolean away, String message) {
|
||||
for (ServerTab tab : tabs) {
|
||||
if (tab.session().isConnected()) tab.session().setAway(away, message);
|
||||
}
|
||||
sessions.setAwayEverywhere(away, message);
|
||||
updateToolbar();
|
||||
}
|
||||
|
||||
/** @return true when every connected server is marked away (and there is one) */
|
||||
private boolean isGloballyAway() {
|
||||
boolean any = false;
|
||||
for (ServerTab tab : tabs) {
|
||||
if (!tab.session().isConnected()) continue;
|
||||
any = true;
|
||||
if (!tab.session().isAway()) return false;
|
||||
}
|
||||
return any;
|
||||
}
|
||||
|
||||
/** The message to preload the prompt with: the selected tab's, else any set one. */
|
||||
private String currentAwayMessage() {
|
||||
if (selected != null && !selected.session().awayMessage().isEmpty()) return selected.session().awayMessage();
|
||||
for (ServerTab tab : tabs) {
|
||||
if (tab.session().isConnected() && !tab.session().awayMessage().isEmpty()) return tab.session().awayMessage();
|
||||
}
|
||||
return "";
|
||||
}
|
||||
|
||||
/** @return the entered message (possibly empty), or null when cancelled */
|
||||
private String askAwayMessage(String initial) {
|
||||
return (String) JOptionPane.showInputDialog(this, "Away message (optional):", "Away status",
|
||||
@@ -792,9 +732,7 @@ public final class MainFrame extends JFrame implements ServerTabPane.Listener {
|
||||
} catch (IllegalStateException ignored) {
|
||||
// Already shutting down (hook itself is running); nothing to remove.
|
||||
}
|
||||
for (ServerTab tab : new ArrayList<>(tabs)) {
|
||||
tab.session().shutdown();
|
||||
}
|
||||
sessions.shutdown();
|
||||
soundPlayer.shutdown();
|
||||
hotkeys.dispose();
|
||||
if (tray != null) tray.dispose();
|
||||
@@ -846,7 +784,7 @@ public final class MainFrame extends JFrame implements ServerTabPane.Listener {
|
||||
// Device and voice-gating changes apply to the capturing connection and the
|
||||
// playback of the visible one; the rest pick the new settings up on connect.
|
||||
SettingsDialog dlg = new SettingsDialog(this, settings, audio,
|
||||
micTab == null ? null : micTab.connection().getMicrophone(),
|
||||
sessions.microphoneOwner() == null ? null : sessions.microphoneOwner().connection().getMicrophone(),
|
||||
selected == null ? null : selected.connection().getPlayback(),
|
||||
sounds, hotkeys,
|
||||
() -> {
|
||||
@@ -889,11 +827,11 @@ public final class MainFrame extends JFrame implements ServerTabPane.Listener {
|
||||
|
||||
private void updateToolbar() {
|
||||
boolean connected = selected != null && selected.session().isConnected();
|
||||
boolean anyConnected = tabs.stream().anyMatch(t -> t.session().isConnected());
|
||||
boolean anyConnected = sessions.anyConnected();
|
||||
|
||||
boolean micMuted = connected && selected.session().isMicMuted();
|
||||
boolean deaf = connected && selected.session().isDeafened();
|
||||
boolean active = selected != null && selected == micTab;
|
||||
boolean active = selected != null && selected == micTab();
|
||||
boolean away = connected && selected.session().isAway();
|
||||
boolean commander = connected && selected.session().isCommander();
|
||||
toolbar.refresh(connected, anyConnected, micMuted, deaf, active, away);
|
||||
@@ -906,6 +844,7 @@ public final class MainFrame extends JFrame implements ServerTabPane.Listener {
|
||||
* visible one when nobody is capturing.
|
||||
*/
|
||||
private ServerTab trayTab() {
|
||||
ServerTab micTab = micTab();
|
||||
if (micTab != null && micTab.session().isConnected()) return micTab;
|
||||
return selected != null && selected.session().isConnected() ? selected : null;
|
||||
}
|
||||
@@ -933,7 +872,7 @@ public final class MainFrame extends JFrame implements ServerTabPane.Listener {
|
||||
.append(" | ").append(users).append(users == 1 ? " user" : " users");
|
||||
double ping = tab.connection().getPingMillis();
|
||||
if (ping >= 0) s.append(" | ping ").append(Math.round(ping)).append(" ms");
|
||||
if (tab != micTab) s.append(" | microphone on another tab");
|
||||
if (tab != micTab()) s.append(" | microphone on another tab");
|
||||
statusBar.setStatus(s.toString());
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user