diff --git a/ts3-client/core/src/main/java/com/ts3client/session/SelfState.java b/ts3-client/core/src/main/java/com/ts3client/session/SelfState.java new file mode 100644 index 0000000..20c11c5 --- /dev/null +++ b/ts3-client/core/src/main/java/com/ts3client/session/SelfState.java @@ -0,0 +1,29 @@ +package com.ts3client.session; + +/** + * The state of the local client on one server, as the tree shows it for any other + * client — the states are listed in the order the official client gives them + * priority, the strongest one first. + */ +public enum SelfState { + + DISCONNECTED("Not connected"), + DEAFENED("Speakers muted"), + MIC_MUTED("Microphone muted"), + MIC_LOCAL_MUTED("Microphone locally muted"), + AWAY("Away"), + COMMANDER_TALKING("Talking (channel commander)"), + COMMANDER("Channel commander"), + TALKING("Talking"), + IDLE("Connected"); + + private final String label; + + SelfState(String label) { + this.label = label; + } + + public String label() { + return label; + } +} diff --git a/ts3-client/core/src/main/java/com/ts3client/session/ServerSession.java b/ts3-client/core/src/main/java/com/ts3client/session/ServerSession.java new file mode 100644 index 0000000..8c54500 --- /dev/null +++ b/ts3-client/core/src/main/java/com/ts3client/session/ServerSession.java @@ -0,0 +1,457 @@ +package com.ts3client.session; + +import com.ts3client.audio.AudioBackend; +import com.ts3client.config.IdentityEntry; +import com.ts3client.config.IdentityStore; +import com.ts3client.config.Settings; +import com.ts3client.contacts.ContactStore; +import com.ts3client.net.ChannelNode; +import com.ts3client.net.ClientEntry; +import com.ts3client.net.ConnectionListener; +import com.ts3client.net.ServerModel; +import com.ts3client.net.TeamspeakConnection; +import com.ts3client.sound.SoundNotifier; + +import java.util.ArrayList; +import java.util.List; + +/** + * One server connection and the local client's state on it, independent of any UI: the + * connection lifecycle, the microphone/speaker/away flags, chat sending and channel + * lookups. A frontend keeps one per server tab and hears from it through {@link Listener}. + * + *
Listener callbacks arrive on connection threads, so frontends marshal them onto their
+ * UI thread. The getters may be called from any thread.
+ */
+public final class ServerSession {
+
+ /** Everything a {@link ConnectionListener} hears, plus what the session adds. */
+ public interface Listener extends ConnectionListener {
+
+ /**
+ * The session's title, status, connecting state or self flags changed. Also
+ * delivers what {@link #onStatus} does, so frontends usually only need this one.
+ */
+ void onSessionChanged();
+
+ /** A line for the chat's own log: connection progress, errors, pokes. */
+ void onSystemMessage(String text);
+
+ /**
+ * The local client sent a chat message.
+ *
+ * @param peerClientId the recipient of a private message, else 0
+ * @param peerName the recipient's display name for a private message, else empty
+ */
+ void onOwnChat(ChatScope scope, int peerClientId, String peerName, String text);
+ }
+
+ private final Settings settings;
+ private final IdentityStore identities;
+ private final Listener listener;
+ private final TeamspeakConnection conn;
+
+ private volatile String title = "New connection";
+ private volatile String status = "Not connected";
+ private volatile boolean connecting;
+ /** Identity used for the current connection, so it can be saved into a bookmark. */
+ private volatile String identityId = "";
+
+ private volatile boolean micMuted;
+ private volatile boolean micLocalMuted;
+ private volatile boolean deafened;
+ private volatile boolean away;
+ /** Away message currently published, empty when away carries no message. */
+ private volatile String awayMessage = "";
+ private volatile boolean commander;
+
+ public ServerSession(Settings settings, IdentityStore identities, AudioBackend audio,
+ SoundNotifier sounds, ContactStore contacts, Listener listener) {
+ this.settings = settings;
+ this.identities = identities;
+ this.listener = listener;
+ this.conn = new TeamspeakConnection(settings, audio, new Events(), sounds, contacts);
+ }
+
+ public TeamspeakConnection connection() {
+ return conn;
+ }
+
+ /** The server's name once known, the address before that. */
+ public String title() {
+ return title;
+ }
+
+ public String status() {
+ return status;
+ }
+
+ public boolean isConnected() {
+ return conn.isConnected();
+ }
+
+ /** Connected or still connecting — i.e. this session is not free for a new server. */
+ public boolean isBusy() {
+ return connecting || conn.isConnected();
+ }
+
+ public String address() {
+ return conn.getServerHost();
+ }
+
+ public int port() {
+ return conn.getServerPort();
+ }
+
+ public String identityId() {
+ return identityId;
+ }
+
+ // ---- connection lifecycle ----
+
+ /**
+ * @param identityId identity to use, or empty for the default one
+ * @param channel channel path to join on connect, or empty for the default channel
+ * @param channelPassword password for that channel, if any
+ */
+ public void connect(String address, int port, String nickname, String password, String identityId,
+ String channel, String channelPassword) {
+ synchronized (this) {
+ if (isBusy()) return;
+ connecting = true;
+ }
+ title = address + ":" + port;
+ listener.onSystemMessage("Connecting to " + address + ":" + port
+ + (channel == null || channel.isBlank() ? "" : " (channel \"" + channel + "\")") + " …");
+ setStatus("Loading identity…");
+
+ // Resolving may have to generate a first identity, which takes a while.
+ new Thread(() -> {
+ final IdentityEntry entry;
+ try {
+ entry = identities.resolve(settings, identityId);
+ } catch (Exception e) {
+ connecting = false;
+ reportError("Could not load identity: " + e.getMessage());
+ return;
+ }
+ this.identityId = entry.getId();
+ listener.onSystemMessage("Using identity \"" + entry.getName() + "\".");
+ conn.connect(address, port, nickname, password, entry.getIdentity(), channel, channelPassword);
+ }, "identity-resolve").start();
+ }
+
+ /**
+ * Replaces this session's connection: leaves the server it is on, then connects to the
+ * new one in its place — what left-clicking a bookmark does.
+ */
+ public void switchTo(String address, int port, String nickname, String password, String identityId,
+ String channel, String channelPassword) {
+ if (!isBusy()) {
+ connect(address, port, nickname, password, identityId, channel, channelPassword);
+ return;
+ }
+ new Thread(() -> {
+ // Reports the disconnect before returning, which clears the connecting flag.
+ conn.disconnectBlocking("Switching servers");
+ connect(address, port, nickname, password, identityId, channel, channelPassword);
+ }, "ts3j-switch-server").start();
+ }
+
+ public void disconnect() {
+ if (conn.isConnected()) conn.disconnect();
+ }
+
+ /** Synchronous teardown for shutdown paths, so the server sees us leave. */
+ public void shutdown() {
+ if (conn.isConnected()) conn.disconnectBlocking("Leaving");
+ }
+
+ /** Releases the background resources of a session that is being thrown away. */
+ public void dispose() {
+ conn.getIcons().shutdown();
+ }
+
+ // ---- self state ----
+
+ public boolean isMicMuted() {
+ return micMuted;
+ }
+
+ public boolean isMicLocalMuted() {
+ return micLocalMuted;
+ }
+
+ public boolean isDeafened() {
+ return deafened;
+ }
+
+ public boolean isAway() {
+ return away;
+ }
+
+ public String awayMessage() {
+ return awayMessage;
+ }
+
+ public boolean isCommander() {
+ return commander;
+ }
+
+ public void setMicMuted(boolean muted) {
+ micMuted = muted;
+ conn.setMicMuted(muted);
+ listener.onSessionChanged();
+ }
+
+ /** TS3's "Local Mic Mute": silences capture without publishing a status change. */
+ public void setMicLocalMuted(boolean muted) {
+ micLocalMuted = muted;
+ conn.setMicLocalMuted(muted);
+ listener.onSessionChanged();
+ }
+
+ public void setDeafened(boolean deaf) {
+ deafened = deaf;
+ conn.setDeafened(deaf);
+ listener.onSessionChanged();
+ }
+
+ /**
+ * @param message the away message, or null to keep the one already set — the
+ * message outlives coming back, so toggling away again restores it
+ */
+ public void setAway(boolean away, String message) {
+ this.away = away;
+ if (message != null) this.awayMessage = message;
+ conn.setAway(away, awayMessage);
+ listener.onSessionChanged();
+ }
+
+ public void setCommander(boolean commander) {
+ this.commander = commander;
+ conn.setChannelCommander(commander);
+ listener.onSessionChanged();
+ }
+
+ /** Hands the capture device to (or takes it from) this connection. */
+ public void setMicrophoneActive(boolean active) {
+ conn.setMicrophoneActive(active);
+ }
+
+ public void setNickname(String nickname) {
+ if (conn.isConnected()) conn.setNickname(nickname);
+ }
+
+ /** What the local client looks like on this server. */
+ public SelfState selfState() {
+ if (!conn.isConnected()) return SelfState.DISCONNECTED;
+ if (deafened) return SelfState.DEAFENED;
+ if (micMuted) return SelfState.MIC_MUTED;
+ if (micLocalMuted) return SelfState.MIC_LOCAL_MUTED;
+ if (away) return SelfState.AWAY;
+ ClientEntry self = self();
+ boolean talking = self != null && self.talking;
+ if (commander) return talking ? SelfState.COMMANDER_TALKING : SelfState.COMMANDER;
+ return talking ? SelfState.TALKING : SelfState.IDLE;
+ }
+
+ /** Clears all flags on a fresh connection; the server starts us out clean, so nothing to publish. */
+ private void resetSelfState() {
+ micMuted = false;
+ micLocalMuted = false;
+ deafened = false;
+ away = false;
+ awayMessage = "";
+ commander = false;
+ }
+
+ private ClientEntry self() {
+ return conn.getModel().getClient(conn.getSelfClientId());
+ }
+
+ // ---- chat ----
+
+ /**
+ * Sends a chat message and reports it back through {@link Listener#onOwnChat}.
+ *
+ * @param clientId the recipient of a private message; ignored otherwise
+ */
+ public void sendChat(ConnectionListener.ChatScope scope, int clientId, String text) {
+ if (!conn.isConnected()) {
+ listener.onSystemMessage("Not connected.");
+ return;
+ }
+ switch (scope) {
+ case SERVER -> {
+ conn.sendServerMessage(text);
+ listener.onOwnChat(scope, 0, "", text);
+ }
+ case PRIVATE -> {
+ conn.sendPrivateMessage(clientId, text);
+ listener.onOwnChat(scope, clientId, peerName(clientId), text);
+ }
+ default -> {
+ conn.sendChannelMessage(text);
+ listener.onOwnChat(scope, 0, "", text);
+ }
+ }
+ }
+
+ private String peerName(int clientId) {
+ ClientEntry c = conn.getModel().getClient(clientId);
+ return c != null ? conn.displayName(c) : "Client " + clientId;
+ }
+
+ // ---- channels ----
+
+ /** The channel we are in, or null when not connected. */
+ public ChannelNode currentChannel() {
+ if (!conn.isConnected()) return null;
+ ClientEntry self = self();
+ return self == null ? null : conn.getModel().getChannel(self.channelId);
+ }
+
+ /** Path of the channel we are in, or empty when not connected. */
+ public String currentChannelPath() {
+ if (!conn.isConnected()) return "";
+ ClientEntry self = self();
+ return self == null ? "" : conn.getModel().channelPath(self.channelId);
+ }
+
+ /** Joins the channel at a "/"-separated path, as the hotkey action names it. */
+ public void joinChannelPath(String path) {
+ if (!conn.isConnected() || path == null || path.isBlank()) return;
+ ChannelNode target = conn.getModel().findChannelByPath(path);
+ if (target != null) conn.joinChannel(target.id, null);
+ }
+
+ /** Every channel of this server as a "/"-separated path, in the tree's own order. */
+ public List ServerTab itself owns the connection, the panels and this tab's identity
- * (title/status); the mic/away/deafen flags live in {@link ServerTabSelfState},
- * tree context-menu actions and selection in {@link ServerTabTreeActions}, and
- * {@link com.ts3client.net.ConnectionListener} callbacks in {@link ServerTabConnectionEvents}.
+ * The connection and the client's own state on it live in the {@link ServerSession};
+ * ServerTab owns the panels, tree context-menu actions and selection live in
+ * {@link ServerTabTreeActions}, and session callbacks in {@link ServerTabConnectionEvents}.
*/
-final class ServerTab implements ServerTabConnectionEvents.Listener {
+final class ServerTab {
private final MainFrame host;
private final Settings settings;
- private final IdentityStore identities;
+ private final ServerSession session;
private final TeamspeakConnection conn;
private final GroupIcons groupIcons;
- private final ServerTabConnectionEvents events;
- private final ServerTabSelfState selfState;
private final ServerTabTreeActions treeActions;
private final ServerTreePanel treePanel;
private final ChatPanel chatPanel;
@@ -53,14 +46,6 @@ final class ServerTab implements ServerTabConnectionEvents.Listener {
/** Whether the saved divider share has been applied; before that, layout is not the user's doing. */
private boolean splitRestored;
- /** Label shown in the tab bar: the server name once known, the address before that. */
- private String title = "New connection";
- private String status = "Not connected";
- private volatile boolean connecting;
-
- /** Identity used for the current connection, so it can be saved into a bookmark. */
- private String identityId = "";
-
/** The one open ban list window for this server, if any. */
private BanListDialog banList;
@@ -68,21 +53,20 @@ final class ServerTab implements ServerTabConnectionEvents.Listener {
SoundNotifier sounds, ContactStore contacts) {
this.host = host;
this.settings = settings;
- this.identities = identities;
- // ServerTabConnectionEvents must exist before the connection (which needs a
- // listener up front), and TeamspeakConnection must exist before the tree/chat
- // panels and the other tab helpers that read from it — so wiring finishes with
- // an explicit attach() once everything is built. Nothing fires callbacks before then.
- this.events = new ServerTabConnectionEvents(host, this, this);
- this.conn = new TeamspeakConnection(settings, audio, events, sounds, contacts);
+ // ServerTabConnectionEvents must exist before the session (which needs a listener
+ // up front), and the session must exist before the tree/chat panels and the other
+ // tab helpers that read from it — so wiring finishes with an explicit attach() once
+ // everything is built. Nothing fires callbacks before then.
+ ServerTabConnectionEvents events = new ServerTabConnectionEvents(host, this, settings);
+ this.session = new ServerSession(settings, identities, audio, sounds, contacts, events);
+ this.conn = session.connection();
this.groupIcons = new GroupIcons(conn.getIcons());
- this.selfState = new ServerTabSelfState(conn);
this.chatPanel = new ChatPanel();
this.treeActions = new ServerTabTreeActions(host, this, conn, chatPanel, infoPanel, groupIcons);
this.treePanel = new ServerTreePanel(conn.getModel(), groupIcons, contacts, settings, treeActions);
treeActions.attach(treePanel);
- events.attach(conn, treePanel, chatPanel, selfState, treeActions);
+ events.attach(session, treePanel, chatPanel, treeActions);
chatPanel.setSendHandler(this::onSendChat);
chatPanel.setLinkHandler(new ChatPanel.LinkHandler() {
@@ -143,6 +127,10 @@ final class ServerTab implements ServerTabConnectionEvents.Listener {
return component;
}
+ ServerSession session() {
+ return session;
+ }
+
TeamspeakConnection connection() {
return conn;
}
@@ -151,140 +139,9 @@ final class ServerTab implements ServerTabConnectionEvents.Listener {
return chatPanel;
}
- String title() {
- return title;
- }
-
- String status() {
- return status;
- }
-
- boolean isConnected() {
- return conn.isConnected();
- }
-
- /** Connected or still connecting — i.e. this tab is not free for a new server. */
- boolean isBusy() {
- return connecting || conn.isConnected();
- }
-
- String address() {
- return conn.getServerHost();
- }
-
- int port() {
- return conn.getServerPort();
- }
-
- String identityId() {
- return identityId;
- }
-
- boolean isMicMuted() {
- return selfState.isMicMuted();
- }
-
- boolean isMicLocalMuted() {
- return selfState.isMicLocalMuted();
- }
-
- boolean isDeafened() {
- return selfState.isDeafened();
- }
-
- boolean isAway() {
- return selfState.isAway();
- }
-
- String awayMessage() {
- return selfState.awayMessage();
- }
-
- boolean isCommander() {
- return selfState.isCommander();
- }
-
- /** What the local client looks like on this server, for the tray icon. */
- SelfState selfState() {
- return selfState.compute();
- }
-
- /** Path of the channel we are in, or empty when not connected. */
- String currentChannelPath() {
- if (!conn.isConnected()) return "";
- ClientEntry self = conn.getModel().getClient(conn.getSelfClientId());
- return self == null ? "" : conn.getModel().channelPath(self.channelId);
- }
-
- // ---- connection lifecycle ----
-
- /**
- * @param identityId identity to use, or empty for the default one
- * @param channel channel path to join on connect, or empty for the default channel
- * @param channelPassword password for that channel, if any
- */
- void connect(String address, int port, String nickname, String password, String identityId,
- String channel, String channelPassword) {
- if (isBusy()) return;
- connecting = true;
- title = address + ":" + port;
- chatPanel.appendSystem("Connecting to " + address + ":" + port
- + (channel == null || channel.isBlank() ? "" : " (channel \"" + channel + "\")") + " …");
- events.onStatus("Loading identity…");
-
- // Resolving may have to generate a first identity, so keep it off the EDT.
- new Thread(() -> {
- final IdentityEntry entry;
- try {
- entry = identities.resolve(settings, identityId);
- } catch (Exception e) {
- connecting = false;
- events.onError("Could not load identity: " + e.getMessage());
- return;
- }
- SwingUtilities.invokeLater(() -> {
- this.identityId = entry.getId();
- chatPanel.appendSystem("Using identity \"" + entry.getName() + "\".");
- });
- conn.connect(address, port, nickname, password, entry.getIdentity(), channel, channelPassword);
- }, "identity-resolve").start();
- }
-
- /**
- * Replaces this tab's connection: leaves the server it is on, then connects to the
- * new one in its place — what left-clicking a bookmark does.
- */
- void switchTo(String address, int port, String nickname, String password, String identityId,
- String channel, String channelPassword) {
- if (!isBusy()) {
- connect(address, port, nickname, password, identityId, channel, channelPassword);
- return;
- }
- new Thread(() -> {
- conn.disconnectBlocking("Switching servers");
- // Queued behind the disconnect's own UI work, which clears the "connecting" flag.
- SwingUtilities.invokeLater(() ->
- connect(address, port, nickname, password, identityId, channel, channelPassword));
- }, "ts3j-switch-server").start();
- }
-
- void disconnect() {
- if (conn.isConnected()) conn.disconnect();
- }
-
- /**
- * Whether the server grants us {@code b_client_ban_list}. Until it has named its
- * permissions nothing can be resolved, so the list is offered and the server left to refuse.
- */
- boolean canViewBanList() {
- if (!conn.isConnected()) return false;
- ServerModel model = conn.getModel();
- return !model.hasPermissionNames() || model.selfPermissionValue("b_client_ban_list") > 0;
- }
-
/** Opens the server's ban list, or brings the already open one forward. */
void showBanList(BanReasonPresets presets) {
- if (!conn.isConnected()) return;
+ if (!session.isConnected()) return;
if (banList == null || !banList.isDisplayable()) {
banList = new BanListDialog(host, conn, settings, presets);
}
@@ -306,68 +163,7 @@ final class ServerTab implements ServerTabConnectionEvents.Listener {
treeActions.renderInfo();
}
- /** Releases the background resources of a tab that is being thrown away. */
- void dispose() {
- conn.getIcons().shutdown();
- }
-
- /** Synchronous teardown for shutdown paths, so the server sees us leave. */
- void shutdown() {
- if (conn.isConnected()) conn.disconnectBlocking("Leaving");
- }
-
- // ---- ServerTabConnectionEvents.Listener ----
-
- @Override
- public void setStatus(String status) {
- this.status = status;
- }
-
- @Override
- public void setTitle(String title) {
- this.title = title;
- }
-
- @Override
- public void setConnecting(boolean connecting) {
- this.connecting = connecting;
- }
-
- // ---- self state ----
-
- void setMicMuted(boolean muted) {
- selfState.setMicMuted(muted);
- }
-
- /** TS3's "Local Mic Mute": silences capture without publishing a status change. */
- void setMicLocalMuted(boolean muted) {
- selfState.setMicLocalMuted(muted);
- }
-
- void setDeafened(boolean deaf) {
- selfState.setDeafened(deaf);
- }
-
- /** Hands the capture device to (or takes it from) this connection. */
- void setMicrophoneActive(boolean active) {
- conn.setMicrophoneActive(active);
- }
-
- /**
- * @param message the away message, or null to keep the one already set — the
- * message outlives coming back, so toggling away again restores it
- */
- void setAway(boolean away, String message) {
- selfState.setAway(away, message);
- }
-
- void setCommander(boolean commander) {
- selfState.setCommander(commander);
- }
-
- void setNickname(String nickname) {
- if (conn.isConnected()) conn.setNickname(nickname);
- }
+ // ---- dialogs ----
void showAvatarDialog() {
if (conn.isConnected()) new AvatarDialog(host, conn).setVisible(true);
@@ -376,58 +172,12 @@ final class ServerTab implements ServerTabConnectionEvents.Listener {
// ---- chat ----
private void onSendChat(ChatPanel.Target target, int clientId, String text) {
- if (!conn.isConnected()) {
- chatPanel.appendSystem("Not connected.");
- return;
- }
- String me = settings.nickname + " (you)";
- int myId = conn.getSelfClientId();
- switch (target) {
- case SERVER:
- conn.sendServerMessage(text);
- chatPanel.appendServerMessage(myId, me, text);
- break;
- case PRIVATE:
- conn.sendPrivateMessage(clientId, text);
- chatPanel.appendPrivateMessage(clientId, peerName(clientId), myId, me, text);
- break;
- default:
- conn.sendChannelMessage(text);
- chatPanel.appendChannelMessage(myId, me, text);
- break;
- }
- }
-
- private String peerName(int clientId) {
- ClientEntry c = conn.getModel().getClient(clientId);
- return c != null ? conn.displayName(c) : "Client " + clientId;
- }
-
- /** Joins the channel at a "/"-separated path, as the hotkey action names it. */
- void joinChannelPath(String path) {
- if (!conn.isConnected() || path == null || path.isBlank()) return;
- ChannelNode target = conn.getModel().findChannelByPath(path);
- if (target != null) conn.joinChannel(target.id, null);
- }
-
- /** Every channel of this server as a "/"-separated path, in the tree's own order. */
- List Constructed before the {@link TeamspeakConnection} it listens to exists — the
- * connection's constructor needs a listener up front — so {@link #attach} wires the
- * rest of the tab in once everything else has been built.
+ * Constructed before the session it listens to exists — the session's constructor
+ * needs a listener up front — so {@link #attach} wires the rest of the tab in once
+ * everything else has been built.
*/
-final class ServerTabConnectionEvents implements ConnectionListener {
-
- /** The handful of {@link ServerTab} fields this class updates but can't reach directly. */
- interface Listener {
- void setStatus(String status);
-
- void setTitle(String title);
-
- void setConnecting(boolean connecting);
- }
+final class ServerTabConnectionEvents implements ServerSession.Listener {
private final MainFrame host;
private final ServerTab tab;
- private final Listener listener;
+ private final Settings settings;
private final AtomicBoolean modelChangePending = new AtomicBoolean();
+ private ServerSession session;
private TeamspeakConnection conn;
private ServerTreePanel treePanel;
private ChatPanel chatPanel;
- private ServerTabSelfState selfState;
private ServerTabTreeActions treeActions;
- ServerTabConnectionEvents(MainFrame host, ServerTab tab, Listener listener) {
+ ServerTabConnectionEvents(MainFrame host, ServerTab tab, Settings settings) {
this.host = host;
this.tab = tab;
- this.listener = listener;
+ this.settings = settings;
}
- void attach(TeamspeakConnection conn, ServerTreePanel treePanel, ChatPanel chatPanel,
- ServerTabSelfState selfState, ServerTabTreeActions treeActions) {
- this.conn = conn;
+ void attach(ServerSession session, ServerTreePanel treePanel, ChatPanel chatPanel,
+ ServerTabTreeActions treeActions) {
+ this.session = session;
+ this.conn = session.connection();
this.treePanel = treePanel;
this.chatPanel = chatPanel;
- this.selfState = selfState;
this.treeActions = treeActions;
}
- /** Name of the channel we are in, or null while that is not known yet. */
- private String currentChannelName() {
- ClientEntry self = conn.getModel().getClient(conn.getSelfClientId());
- if (self == null) return null;
- ChannelNode ch = conn.getModel().getChannel(self.channelId);
- return ch == null ? null : ch.name;
- }
-
@Override
- public void onStatus(String text) {
+ public void onSessionChanged() {
SwingUtilities.invokeLater(() -> {
- listener.setStatus(text);
+ if (session.isConnected()) tab.banListServerChanged(true);
host.tabUpdated(tab);
});
}
+ @Override
+ public void onSystemMessage(String text) {
+ SwingUtilities.invokeLater(() -> chatPanel.appendSystem(text));
+ }
+
+ @Override
+ public void onOwnChat(ChatScope scope, int peerClientId, String peerName, String text) {
+ String me = settings.nickname + " (you)";
+ int myId = conn.getSelfClientId();
+ SwingUtilities.invokeLater(() -> {
+ switch (scope) {
+ case SERVER -> chatPanel.appendServerMessage(myId, me, text);
+ case PRIVATE -> chatPanel.appendPrivateMessage(peerClientId, peerName, myId, me, text);
+ default -> chatPanel.appendChannelMessage(myId, me, text);
+ }
+ });
+ }
+
+ @Override
+ public void onStatus(String text) {
+ // Reaches the tab through onSessionChanged.
+ }
+
@Override
public void onConnected() {
SwingUtilities.invokeLater(() -> {
- listener.setConnecting(false);
treePanel.setSelfClientId(conn.getSelfClientId());
- selfState.resetOnConnect();
chatPanel.setInputEnabled(true);
- chatPanel.appendSystem("Connected.");
host.tabConnected(tab);
});
}
@@ -87,8 +90,6 @@ final class ServerTabConnectionEvents implements ConnectionListener {
@Override
public void onDisconnected(String reason) {
SwingUtilities.invokeLater(() -> {
- listener.setConnecting(false);
- conn.getModel().clear();
treePanel.showDisconnected();
treeActions.clearSelection();
chatPanel.setInputEnabled(false);
@@ -96,7 +97,6 @@ final class ServerTabConnectionEvents implements ConnectionListener {
chatPanel.closeNoteTabs();
chatPanel.setServerName(null);
chatPanel.setChannelName(null);
- chatPanel.appendSystem("Disconnected" + (reason == null || reason.isEmpty() ? "." : ": " + reason));
tab.banListServerChanged(false);
host.tabDisconnected(tab);
});
@@ -111,15 +111,10 @@ final class ServerTabConnectionEvents implements ConnectionListener {
treePanel.rebuild();
treeActions.renderInfo();
treeActions.refreshGroupsDialog();
- String name = conn.getModel().getServerName();
- if (conn.isConnected() && name != null && !name.isBlank() && !name.equals(tab.title())) {
- listener.setTitle(name);
- host.tabUpdated(tab);
- tab.banListServerChanged(true);
- }
- if (conn.isConnected()) {
- chatPanel.setServerName(name);
- chatPanel.setChannelName(currentChannelName());
+ if (session.isConnected()) {
+ ChannelNode channel = session.currentChannel();
+ chatPanel.setServerName(conn.getModel().getServerName());
+ chatPanel.setChannelName(channel == null ? null : channel.name);
}
});
}
@@ -138,20 +133,13 @@ final class ServerTabConnectionEvents implements ConnectionListener {
}
@Override
- public void onChat(ChatScope scope, int fromClientId, String fromUniqueId, String nickname, String message) {
- String fromName = conn.getContacts().displayName(fromUniqueId, nickname);
+ public void onChat(ChatScope scope, int fromClientId, String fromUniqueId, String fromName, String message) {
SwingUtilities.invokeLater(() -> {
switch (scope) {
- case PRIVATE:
- chatPanel.appendPrivateMessage(fromClientId, fromUniqueId, fromName,
- fromClientId, fromUniqueId, fromName, message);
- break;
- case SERVER:
- chatPanel.appendServerMessage(fromClientId, fromUniqueId, fromName, message);
- break;
- default:
- chatPanel.appendChannelMessage(fromClientId, fromUniqueId, fromName, message);
- break;
+ case PRIVATE -> chatPanel.appendPrivateMessage(fromClientId, fromUniqueId, fromName,
+ fromClientId, fromUniqueId, fromName, message);
+ case SERVER -> chatPanel.appendServerMessage(fromClientId, fromUniqueId, fromName, message);
+ default -> chatPanel.appendChannelMessage(fromClientId, fromUniqueId, fromName, message);
}
});
}
@@ -179,11 +167,7 @@ final class ServerTabConnectionEvents implements ConnectionListener {
@Override
public void onError(String message) {
- SwingUtilities.invokeLater(() -> {
- chatPanel.appendSystem("Error: " + message);
- listener.setStatus(message);
- host.tabUpdated(tab);
- });
+ // Reaches the chat through onSystemMessage and the tab through onSessionChanged.
}
@Override
@@ -199,7 +183,6 @@ final class ServerTabConnectionEvents implements ConnectionListener {
@Override
public void onPoke(String fromName, String message) {
SwingUtilities.invokeLater(() -> {
- chatPanel.appendSystem("You were poked by " + fromName + ": " + message);
host.selectTab(tab);
JOptionPane.showMessageDialog(host, fromName + " poked you:\n\n" + message,
"Poke", JOptionPane.INFORMATION_MESSAGE);
diff --git a/ts3-client/swing/src/main/java/com/ts3client/ui/ServerTabPane.java b/ts3-client/swing/src/main/java/com/ts3client/ui/ServerTabPane.java
index 132e0fd..98712e3 100644
--- a/ts3-client/swing/src/main/java/com/ts3client/ui/ServerTabPane.java
+++ b/ts3-client/swing/src/main/java/com/ts3client/ui/ServerTabPane.java
@@ -69,7 +69,7 @@ final class ServerTabPane extends JPanel {
updating = true;
try {
tabbed.removeAll();
- for (ServerTab tab : tabs) tabbed.addTab(tab.title(), tab.component());
+ for (ServerTab tab : tabs) tabbed.addTab(tab.session().title(), tab.component());
if (selected != null) tabbed.setSelectedIndex(tabs.indexOf(selected));
} finally {
updating = false;
@@ -121,8 +121,8 @@ final class ServerTabPane extends JPanel {
if (!tabbedMode) return;
for (int i = 0; i < tabs.size(); i++) {
ServerTab tab = tabs.get(i);
- tabbed.setTitleAt(i, tab.title());
- tabbed.setToolTipTextAt(i, tab.status());
+ tabbed.setTitleAt(i, tab.session().title());
+ tabbed.setToolTipTextAt(i, tab.session().status());
tabbed.setTabComponentAt(i, buildTabComponent(tab, tab == micTab));
}
}
@@ -138,7 +138,7 @@ final class ServerTabPane extends JPanel {
removeAll();
tabbed.removeAll();
if (wantTabs) {
- for (ServerTab tab : tabs) tabbed.addTab(tab.title(), tab.component());
+ for (ServerTab tab : tabs) tabbed.addTab(tab.session().title(), tab.component());
add(tabbed, BorderLayout.CENTER);
} else if (!tabs.isEmpty()) {
add(tabs.get(0).component(), BorderLayout.CENTER);
@@ -157,9 +157,9 @@ final class ServerTabPane extends JPanel {
cell.setOpaque(false);
cell.setBorder(new ScaledEmptyBorder(1, 0, 1, 0));
- JLabel label = new JLabel(tab.title(), hasMic ? Icons.micActiveSmall() : null, JLabel.LEADING);
+ JLabel label = new JLabel(tab.session().title(), hasMic ? Icons.micActiveSmall() : null, JLabel.LEADING);
label.setFont(Theme.uiFont());
- label.setToolTipText(hasMic ? "Speaking on this server" : tab.status());
+ label.setToolTipText(hasMic ? "Speaking on this server" : tab.session().status());
// A label with a tooltip swallows mouse events, so select the tab explicitly.
label.addMouseListener(new MouseAdapter() {
@Override
diff --git a/ts3-client/swing/src/main/java/com/ts3client/ui/ServerTabSelfState.java b/ts3-client/swing/src/main/java/com/ts3client/ui/ServerTabSelfState.java
deleted file mode 100644
index b800291..0000000
--- a/ts3-client/swing/src/main/java/com/ts3client/ui/ServerTabSelfState.java
+++ /dev/null
@@ -1,105 +0,0 @@
-package com.ts3client.ui;
-
-import com.ts3client.net.ClientEntry;
-import com.ts3client.net.TeamspeakConnection;
-
-/**
- * Mic/speaker/away/commander flags for one connection, plus forwarding them into
- * {@link TeamspeakConnection}. Split out of {@link ServerTab} because the flags
- * are reset together in one place (on (re)connect) and read from several
- * (tray icon, toolbar, menu checkmarks).
- */
-final class ServerTabSelfState {
-
- private final TeamspeakConnection conn;
-
- private boolean micMuted;
- private boolean micLocalMuted;
- private boolean deafened;
- private boolean away;
- /** Away message currently published, empty when away carries no message. */
- private String awayMessage = "";
- private boolean commander;
-
- ServerTabSelfState(TeamspeakConnection conn) {
- this.conn = conn;
- }
-
- boolean isMicMuted() {
- return micMuted;
- }
-
- boolean isMicLocalMuted() {
- return micLocalMuted;
- }
-
- boolean isDeafened() {
- return deafened;
- }
-
- boolean isAway() {
- return away;
- }
-
- String awayMessage() {
- return awayMessage;
- }
-
- boolean isCommander() {
- return commander;
- }
-
- void setMicMuted(boolean muted) {
- micMuted = muted;
- conn.setMicMuted(muted);
- }
-
- /** TS3's "Local Mic Mute": silences capture without publishing a status change. */
- void setMicLocalMuted(boolean muted) {
- micLocalMuted = muted;
- conn.setMicLocalMuted(muted);
- }
-
- void setDeafened(boolean deaf) {
- deafened = deaf;
- conn.setDeafened(deaf);
- }
-
- /**
- * @param message the away message, or null to keep the one already set — the
- * message outlives coming back, so toggling away again restores it
- */
- void setAway(boolean away, String message) {
- this.away = away;
- if (message != null) this.awayMessage = message;
- conn.setAway(away, awayMessage);
- }
-
- void setCommander(boolean commander) {
- this.commander = commander;
- conn.setChannelCommander(commander);
- }
-
- /** Clears all flags on a fresh connection; the server starts us out clean, so nothing to publish. */
- void resetOnConnect() {
- micMuted = false;
- micLocalMuted = false;
- deafened = false;
- away = false;
- awayMessage = "";
- commander = false;
- }
-
- /** What the local client looks like on this server, for the tray icon. */
- SelfState compute() {
- if (!conn.isConnected()) return SelfState.DISCONNECTED;
- if (deafened) return SelfState.DEAFENED;
- if (micMuted) return SelfState.MIC_MUTED;
- if (micLocalMuted) return SelfState.MIC_LOCAL_MUTED;
- if (away) return SelfState.AWAY;
- ClientEntry self = conn.getModel().getClient(conn.getSelfClientId());
- boolean talking = self != null && self.talking;
- if (commander) return talking ? SelfState.COMMANDER_TALKING : SelfState.COMMANDER;
- return talking ? SelfState.TALKING : SelfState.IDLE;
- }
-}
diff --git a/ts3-client/swing/src/main/java/com/ts3client/ui/TrayController.java b/ts3-client/swing/src/main/java/com/ts3client/ui/TrayController.java
index 5af22b6..66e5295 100644
--- a/ts3-client/swing/src/main/java/com/ts3client/ui/TrayController.java
+++ b/ts3-client/swing/src/main/java/com/ts3client/ui/TrayController.java
@@ -1,9 +1,11 @@
package com.ts3client.ui;
+import com.ts3client.session.SelfState;
import com.ts3client.ui.tray.AwtTray;
import com.ts3client.ui.tray.TrayBackend;
import com.ts3client.ui.tray.X11Tray;
+import javax.swing.ImageIcon;
import javax.swing.JComponent;
import javax.swing.JDialog;
import javax.swing.JMenuItem;
@@ -76,7 +78,7 @@ final class TrayController {
// -Dts3j.tray=awt|x11 pins the backend; by default the transparent one is
// tried first and AWT's picks up whatever it leaves.
String choice = System.getProperty("ts3j.tray", "auto");
- IntFunction