From e5b607fc29e0092e2b9248d1091730c9563e8aa6 Mon Sep 17 00:00:00 2001 From: ericek111 Date: Tue, 18 Aug 2026 13:37:58 +0000 Subject: [PATCH] Split ServerTab into focused collaborator classes Extract mic/speaker/away/commander state handling into ServerTabSelfState, ServerTreePanel.Actions delegation (context menus, drag-drop, link clicks) into ServerTabTreeActions, and ConnectionListener event marshaling onto the EDT into ServerTabConnectionEvents. ServerTab keeps its full public API (636 -> 377 lines); no behavior change. Co-Authored-By: Claude Sonnet 5 --- .../main/java/com/ts3client/ui/ServerTab.java | 371 +++--------------- .../ui/ServerTabConnectionEvents.java | 160 ++++++++ .../com/ts3client/ui/ServerTabSelfState.java | 106 +++++ .../ts3client/ui/ServerTabTreeActions.java | 202 ++++++++++ 4 files changed, 524 insertions(+), 315 deletions(-) create mode 100644 ts3-client/swing/src/main/java/com/ts3client/ui/ServerTabConnectionEvents.java create mode 100644 ts3-client/swing/src/main/java/com/ts3client/ui/ServerTabSelfState.java create mode 100644 ts3-client/swing/src/main/java/com/ts3client/ui/ServerTabTreeActions.java diff --git a/ts3-client/swing/src/main/java/com/ts3client/ui/ServerTab.java b/ts3-client/swing/src/main/java/com/ts3client/ui/ServerTab.java index cc1aaf5..3a41f84 100644 --- a/ts3-client/swing/src/main/java/com/ts3client/ui/ServerTab.java +++ b/ts3-client/swing/src/main/java/com/ts3client/ui/ServerTab.java @@ -6,13 +6,11 @@ import com.ts3client.config.IdentityStore; import com.ts3client.config.Settings; import com.ts3client.net.ChannelNode; import com.ts3client.net.ClientEntry; -import com.ts3client.net.ConnectionListener; import com.ts3client.net.TeamspeakConnection; import com.ts3client.sound.SoundNotifier; import com.ts3client.text.TsLink; import javax.swing.JComponent; -import javax.swing.JOptionPane; import javax.swing.JSplitPane; import javax.swing.SwingUtilities; import java.util.ArrayList; @@ -24,10 +22,12 @@ import java.awt.Component; * client keeps several of these side by side; {@link MainFrame} shows one at a * time and owns the toolbar, menus and status bar that act on it. * - *

Everything that is per-server lives here: the connection, its microphone - * and speaker mute state, the away/commander flags and the chat history. + *

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}. */ -final class ServerTab implements ConnectionListener, ServerTreePanel.Actions { +final class ServerTab implements ServerTabConnectionEvents.Listener { private final MainFrame host; private final Settings settings; @@ -35,6 +35,9 @@ final class ServerTab implements ConnectionListener, ServerTreePanel.Actions { 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; private final InfoPanel infoPanel = new InfoPanel(); @@ -51,36 +54,36 @@ final class ServerTab implements ConnectionListener, ServerTreePanel.Actions { /** Identity used for the current connection, so it can be saved into a bookmark. */ private String identityId = ""; - 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; - - private Object currentSelection; - ServerTab(MainFrame host, Settings settings, IdentityStore identities, AudioBackend audio, SoundNotifier sounds) { this.host = host; this.settings = settings; this.identities = identities; - this.conn = new TeamspeakConnection(settings, audio, this, sounds); + + // 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); this.groupIcons = new GroupIcons(conn.getIcons()); - this.treePanel = new ServerTreePanel(conn.getModel(), groupIcons, this); + this.selfState = new ServerTabSelfState(conn); this.chatPanel = new ChatPanel(); + this.treeActions = new ServerTabTreeActions(host, this, conn, chatPanel, infoPanel); + this.treePanel = new ServerTreePanel(conn.getModel(), groupIcons, treeActions); + treeActions.attach(treePanel); + events.attach(conn, treePanel, chatPanel, selfState, treeActions); chatPanel.setSendHandler(this::onSendChat); chatPanel.setLinkHandler(new ChatPanel.LinkHandler() { @Override public void onClientLink(TsLink.Ref ref, Component source, int x, int y) { - ServerTab.this.onClientLink(ref, source, x, y); + treeActions.handleClientLink(ref, source, x, y); } @Override public void onChannelLink(TsLink.Ref ref, Component source, int x, int y) { - ServerTab.this.onChannelLink(ref, source, x, y); + treeActions.handleChannelLink(ref, source, x, y); } }); chatPanel.setInputEnabled(false); @@ -158,40 +161,32 @@ final class ServerTab implements ConnectionListener, ServerTreePanel.Actions { } boolean isMicMuted() { - return micMuted; + return selfState.isMicMuted(); } boolean isMicLocalMuted() { - return micLocalMuted; + return selfState.isMicLocalMuted(); } boolean isDeafened() { - return deafened; + return selfState.isDeafened(); } boolean isAway() { - return away; + return selfState.isAway(); } String awayMessage() { - return awayMessage; + return selfState.awayMessage(); } boolean isCommander() { - return commander; + return selfState.isCommander(); } /** What the local client looks like on this server, for the tray icon. */ 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 = 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; + return selfState.compute(); } /** Path of the channel we are in, or empty when not connected. */ @@ -215,8 +210,7 @@ final class ServerTab implements ConnectionListener, ServerTreePanel.Actions { title = address + ":" + port; chatPanel.appendSystem("Connecting to " + address + ":" + port + (channel == null || channel.isBlank() ? "" : " (channel \"" + channel + "\")") + " …"); - onStatus("Loading identity…"); - host.tabUpdated(this); + events.onStatus("Loading identity…"); // Resolving may have to generate a first identity, so keep it off the EDT. new Thread(() -> { @@ -225,7 +219,7 @@ final class ServerTab implements ConnectionListener, ServerTreePanel.Actions { entry = identities.resolve(settings, identityId); } catch (Exception e) { connecting = false; - onError("Could not load identity: " + e.getMessage()); + events.onError("Could not load identity: " + e.getMessage()); return; } SwingUtilities.invokeLater(() -> { @@ -250,23 +244,36 @@ final class ServerTab implements ConnectionListener, ServerTreePanel.Actions { 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) { - micMuted = muted; - conn.setMicMuted(muted); + selfState.setMicMuted(muted); } /** TS3's "Local Mic Mute": silences capture without publishing a status change. */ void setMicLocalMuted(boolean muted) { - micLocalMuted = muted; - conn.setMicLocalMuted(muted); + selfState.setMicLocalMuted(muted); } void setDeafened(boolean deaf) { - deafened = deaf; - conn.setDeafened(deaf); - if (deaf) micMuted = true; + selfState.setDeafened(deaf); } /** Hands the capture device to (or takes it from) this connection. */ @@ -279,14 +286,11 @@ final class ServerTab implements ConnectionListener, ServerTreePanel.Actions { * 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); + selfState.setAway(away, message); } void setCommander(boolean commander) { - this.commander = commander; - conn.setChannelCommander(commander); + selfState.setCommander(commander); } void setNickname(String nickname) { @@ -318,33 +322,6 @@ final class ServerTab implements ConnectionListener, ServerTreePanel.Actions { } } - /** A client link in the chat log was clicked: show the same menu as the tree does. */ - private void onClientLink(TsLink.Ref ref, Component source, int x, int y) { - ClientEntry client = conn.getModel().getClient(ref.id); - // The id is only valid for the session the link was made in; fall back to - // the unique id (and finally the nickname) so older links still resolve. - if (client == null || (!ref.uniqueId.isEmpty() && !ref.uniqueId.equals(client.uniqueId))) { - ClientEntry byUid = ref.uniqueId.isEmpty() ? null - : conn.getModel().findClientByUniqueId(ref.uniqueId); - if (byUid == null && !ref.name.isEmpty()) byUid = conn.getModel().findClientByName(ref.name); - if (byUid != null) client = byUid; - } - if (client == null) { - chatPanel.appendSystem("That client is no longer on the server."); - return; - } - ClientMenu.build(client, client.id == conn.getSelfClientId(), this).show(source, x, y); - } - - private void onChannelLink(TsLink.Ref ref, Component source, int x, int y) { - ChannelNode channel = conn.getModel().getChannel(ref.id); - if (channel == null) { - chatPanel.appendSystem("That channel no longer exists."); - return; - } - ChannelMenu.build(channel, this).show(source, x, y); - } - private String peerName(int clientId) { ClientEntry c = conn.getModel().getClient(clientId); return c != null ? c.nickname : "Client " + clientId; @@ -377,129 +354,9 @@ final class ServerTab implements ConnectionListener, ServerTreePanel.Actions { return self == null ? null : conn.getModel().getChannel(self.channelId); } - // ---- ServerTreePanel.Actions ---- - - @Override - public void joinChannel(int channelId) { - if (conn.isConnected()) conn.joinChannel(channelId, null); - } - - @Override - public void moveClientToChannel(ClientEntry client, ChannelNode target) { - if (!conn.isConnected()) return; - if (client.id == conn.getSelfClientId()) { - conn.joinChannel(target.id, null); - } else { - conn.moveClient(client.id, target.id, null); - } - } - - @Override - public void moveChannel(ChannelNode channel, int newParentId, int orderPredecessorId) { - if (conn.isConnected()) conn.moveChannel(channel.id, newParentId, orderPredecessorId); - } - - @Override - public void openPrivateChat(ClientEntry client) { - chatPanel.openPrivateChat(client.id, client.nickname); - } - - @Override - public void pokeClient(ClientEntry client) { - String msg = JOptionPane.showInputDialog(host, "Poke message for " + client.nickname + ":", "Poke!"); - if (msg != null) conn.poke(client.id, msg); - } - - @Override - public void kickClientFromChannel(ClientEntry client) { - String reason = kickReason("Kick Client from Channel", client); - if (reason != null) conn.kickFromChannel(client.id, reason); - } - - @Override - public void kickClientFromServer(ClientEntry client) { - String reason = kickReason("Kick Client from Server", client); - if (reason != null) conn.kickFromServer(client.id, reason); - } - - private String kickReason(String title, ClientEntry client) { - if (!conn.isConnected()) return null; - return ReasonDialog.prompt(host, title, "Reason for kicking " + client.nickname + ":", - ReasonDialog.KICK_REASON_LIMIT); - } - - @Override - public void banClient(ClientEntry client) { - if (!conn.isConnected()) return; - BanDialog dialog = new BanDialog(host, client.nickname); - dialog.setVisible(true); - if (dialog.isConfirmed()) conn.banClient(client.id, dialog.getSeconds(), dialog.getReason()); - } - - @Override - public void toggleClientMute(ClientEntry client) { - if (conn.getPlayback() == null) return; - boolean now = !conn.getPlayback().isClientMuted(client.id); - conn.getPlayback().setClientMuted(client.id, now); - } - - @Override - public void findClientInTree(ClientEntry client) { - host.selectTab(this); - treePanel.selectClient(client.id); - } - - @Override - public void showConnectionInfo(ClientEntry client) { - if (!conn.isConnected()) return; - new ConnectionInfoDialog(host, conn, client.id, client.nickname).setVisible(true); - } - - @Override - public void moveClientToOwnChannel(ClientEntry client) { - if (!conn.isConnected() || client.id == conn.getSelfClientId()) return; - ClientEntry self = conn.getModel().getClient(conn.getSelfClientId()); - if (self != null) conn.moveClient(client.id, self.channelId, null); - } - - @Override - public void setChannelSubscribed(ChannelNode channel, boolean family, boolean subscribed) { - if (conn.isConnected()) conn.setChannelsSubscribed(channel.familyIds(family), subscribed); - } - - @Override - public void browseFiles(ChannelNode channel) { - if (!conn.canTransferFiles()) return; - new FileBrowserDialog(host, conn, channel).setVisible(true); - } - - @Override - public boolean isClientLocallyMuted(int clientId) { - return conn.getPlayback() != null && conn.getPlayback().isClientMuted(clientId); - } - - @Override - public void onSelectionChanged(Object userObject) { - currentSelection = userObject; - renderInfo(); - if (!conn.isConnected()) return; - if (userObject instanceof ChannelNode) { - ChannelNode ch = (ChannelNode) userObject; - if (!ch.descriptionLoaded) conn.requestChannelInfo(ch.id); - } else if (userObject instanceof ClientEntry) { - conn.requestClientInfo(((ClientEntry) userObject).id); - } - } - - private void renderInfo() { - Object sel = currentSelection; - if (sel instanceof ChannelNode) { - infoPanel.showChannel((ChannelNode) sel); - } else if (sel instanceof ClientEntry) { - infoPanel.showClient((ClientEntry) sel, conn.getModel(), conn.getIcons()); - } else { - infoPanel.clear(); - } + /** Opens the file repository browser for a channel, as the "Browse Files" hotkey/menu action does. */ + void browseFiles(ChannelNode channel) { + treeActions.browseFiles(channel); } /** Collapses the info panel to nothing while its content lives in the chat tab, or restores it. */ @@ -517,120 +374,4 @@ final class ServerTab implements ConnectionListener, ServerTreePanel.Actions { } leftColumn.revalidate(); } - - // ---- ConnectionListener (marshal to EDT) ---- - - @Override - public void onStatus(String text) { - SwingUtilities.invokeLater(() -> { - status = text; - host.tabUpdated(this); - }); - } - - @Override - public void onConnected() { - SwingUtilities.invokeLater(() -> { - connecting = false; - treePanel.setSelfClientId(conn.getSelfClientId()); - micMuted = false; - micLocalMuted = false; - deafened = false; - away = false; - awayMessage = ""; - commander = false; - chatPanel.setInputEnabled(true); - chatPanel.appendSystem("Connected."); - host.tabConnected(this); - }); - } - - @Override - public void onDisconnected(String reason) { - SwingUtilities.invokeLater(() -> { - connecting = false; - conn.getModel().clear(); - treePanel.showDisconnected(); - currentSelection = null; - infoPanel.clear(); - chatPanel.setInputEnabled(false); - chatPanel.closePrivateChats(); - chatPanel.closeNoteTabs(); - chatPanel.appendSystem("Disconnected" + (reason == null || reason.isEmpty() ? "." : ": " + reason)); - host.tabDisconnected(this); - }); - } - - @Override - public void onModelChanged() { - SwingUtilities.invokeLater(() -> { - treePanel.rebuild(); - renderInfo(); - String name = conn.getModel().getServerName(); - if (conn.isConnected() && name != null && !name.isBlank() && !name.equals(title)) { - title = name; - host.tabUpdated(this); - } - }); - } - - @Override - public void onInfoUpdated() { - SwingUtilities.invokeLater(this::renderInfo); - } - - @Override - public void onIconsUpdated() { - SwingUtilities.invokeLater(() -> { - treePanel.refreshRowSizes(); - renderInfo(); - }); - } - - @Override - public void onChat(ChatScope scope, int fromClientId, String fromName, String message) { - switch (scope) { - case PRIVATE: - chatPanel.appendPrivateMessage(fromClientId, fromName, fromClientId, fromName, message); - break; - case SERVER: - chatPanel.appendServerMessage(fromClientId, fromName, message); - break; - default: - chatPanel.appendChannelMessage(fromClientId, fromName, message); - break; - } - } - - @Override - public void onTalkStateChanged(int clientId, boolean talking) { - SwingUtilities.invokeLater(() -> { - treePanel.refreshVisual(); - if (clientId == conn.getSelfClientId()) host.selfStateChanged(this); - }); - } - - @Override - public void onError(String message) { - SwingUtilities.invokeLater(() -> { - chatPanel.appendSystem("Error: " + message); - status = message; - host.tabUpdated(this); - }); - } - - @Override - public void onServerLog(String message) { - SwingUtilities.invokeLater(() -> chatPanel.appendServerLog(message)); - } - - @Override - public void onPoke(String fromName, String message) { - SwingUtilities.invokeLater(() -> { - chatPanel.appendSystem("You were poked by " + fromName + ": " + message); - host.selectTab(this); - 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/ServerTabConnectionEvents.java b/ts3-client/swing/src/main/java/com/ts3client/ui/ServerTabConnectionEvents.java new file mode 100644 index 0000000..0545aec --- /dev/null +++ b/ts3-client/swing/src/main/java/com/ts3client/ui/ServerTabConnectionEvents.java @@ -0,0 +1,160 @@ +package com.ts3client.ui; + +import com.ts3client.net.ConnectionListener; +import com.ts3client.net.TeamspeakConnection; + +import javax.swing.JOptionPane; +import javax.swing.SwingUtilities; + +/** + * Marshals {@link ConnectionListener} callbacks onto the EDT and fans them out to + * this tab's views (tree, chat, info panel) and to {@link MainFrame}. + * + *

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. + */ +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); + } + + private final MainFrame host; + private final ServerTab tab; + private final Listener listener; + + private TeamspeakConnection conn; + private ServerTreePanel treePanel; + private ChatPanel chatPanel; + private ServerTabSelfState selfState; + private ServerTabTreeActions treeActions; + + ServerTabConnectionEvents(MainFrame host, ServerTab tab, Listener listener) { + this.host = host; + this.tab = tab; + this.listener = listener; + } + + void attach(TeamspeakConnection conn, ServerTreePanel treePanel, ChatPanel chatPanel, + ServerTabSelfState selfState, ServerTabTreeActions treeActions) { + this.conn = conn; + this.treePanel = treePanel; + this.chatPanel = chatPanel; + this.selfState = selfState; + this.treeActions = treeActions; + } + + @Override + public void onStatus(String text) { + SwingUtilities.invokeLater(() -> { + listener.setStatus(text); + host.tabUpdated(tab); + }); + } + + @Override + public void onConnected() { + SwingUtilities.invokeLater(() -> { + listener.setConnecting(false); + treePanel.setSelfClientId(conn.getSelfClientId()); + selfState.resetOnConnect(); + chatPanel.setInputEnabled(true); + chatPanel.appendSystem("Connected."); + host.tabConnected(tab); + }); + } + + @Override + public void onDisconnected(String reason) { + SwingUtilities.invokeLater(() -> { + listener.setConnecting(false); + conn.getModel().clear(); + treePanel.showDisconnected(); + treeActions.clearSelection(); + chatPanel.setInputEnabled(false); + chatPanel.closePrivateChats(); + chatPanel.closeNoteTabs(); + chatPanel.appendSystem("Disconnected" + (reason == null || reason.isEmpty() ? "." : ": " + reason)); + host.tabDisconnected(tab); + }); + } + + @Override + public void onModelChanged() { + SwingUtilities.invokeLater(() -> { + treePanel.rebuild(); + treeActions.renderInfo(); + String name = conn.getModel().getServerName(); + if (conn.isConnected() && name != null && !name.isBlank() && !name.equals(tab.title())) { + listener.setTitle(name); + host.tabUpdated(tab); + } + }); + } + + @Override + public void onInfoUpdated() { + SwingUtilities.invokeLater(treeActions::renderInfo); + } + + @Override + public void onIconsUpdated() { + SwingUtilities.invokeLater(() -> { + treePanel.refreshRowSizes(); + treeActions.renderInfo(); + }); + } + + @Override + public void onChat(ChatScope scope, int fromClientId, String fromName, String message) { + switch (scope) { + case PRIVATE: + chatPanel.appendPrivateMessage(fromClientId, fromName, fromClientId, fromName, message); + break; + case SERVER: + chatPanel.appendServerMessage(fromClientId, fromName, message); + break; + default: + chatPanel.appendChannelMessage(fromClientId, fromName, message); + break; + } + } + + @Override + public void onTalkStateChanged(int clientId, boolean talking) { + SwingUtilities.invokeLater(() -> { + treePanel.refreshVisual(); + if (clientId == conn.getSelfClientId()) host.selfStateChanged(tab); + }); + } + + @Override + public void onError(String message) { + SwingUtilities.invokeLater(() -> { + chatPanel.appendSystem("Error: " + message); + listener.setStatus(message); + host.tabUpdated(tab); + }); + } + + @Override + public void onServerLog(String message) { + SwingUtilities.invokeLater(() -> chatPanel.appendServerLog(message)); + } + + @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/ServerTabSelfState.java b/ts3-client/swing/src/main/java/com/ts3client/ui/ServerTabSelfState.java new file mode 100644 index 0000000..099d921 --- /dev/null +++ b/ts3-client/swing/src/main/java/com/ts3client/ui/ServerTabSelfState.java @@ -0,0 +1,106 @@ +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); + if (deaf) micMuted = true; + } + + /** + * @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/ServerTabTreeActions.java b/ts3-client/swing/src/main/java/com/ts3client/ui/ServerTabTreeActions.java new file mode 100644 index 0000000..4142699 --- /dev/null +++ b/ts3-client/swing/src/main/java/com/ts3client/ui/ServerTabTreeActions.java @@ -0,0 +1,202 @@ +package com.ts3client.ui; + +import com.ts3client.net.ChannelNode; +import com.ts3client.net.ClientEntry; +import com.ts3client.net.TeamspeakConnection; +import com.ts3client.text.TsLink; + +import javax.swing.JOptionPane; +import java.awt.Component; + +/** + * Context-menu and drag/drop actions for one server's tree, plus tracking which + * node is selected so the info panel stays in sync. Also handles client/channel + * links clicked in the chat log, which open the same menus as the tree does. + * + *

Constructed before the {@link ServerTreePanel} it drives exists — the tree's + * constructor needs an {@link ServerTreePanel.Actions} up front — so {@link #attach} + * wires the tree back in once it has been built. + */ +final class ServerTabTreeActions implements ServerTreePanel.Actions { + + private final MainFrame host; + private final ServerTab tab; + private final TeamspeakConnection conn; + private final ChatPanel chatPanel; + private final InfoPanel infoPanel; + private ServerTreePanel treePanel; + + private Object currentSelection; + + ServerTabTreeActions(MainFrame host, ServerTab tab, TeamspeakConnection conn, + ChatPanel chatPanel, InfoPanel infoPanel) { + this.host = host; + this.tab = tab; + this.conn = conn; + this.chatPanel = chatPanel; + this.infoPanel = infoPanel; + } + + void attach(ServerTreePanel treePanel) { + this.treePanel = treePanel; + } + + /** Refreshes the info panel for whatever is currently selected (or clears it). */ + void renderInfo() { + Object sel = currentSelection; + if (sel instanceof ChannelNode) { + infoPanel.showChannel((ChannelNode) sel); + } else if (sel instanceof ClientEntry) { + infoPanel.showClient((ClientEntry) sel, conn.getModel(), conn.getIcons()); + } else { + infoPanel.clear(); + } + } + + /** Drops the selection on disconnect, since the model it points into is gone. */ + void clearSelection() { + currentSelection = null; + infoPanel.clear(); + } + + /** A client link in the chat log was clicked: show the same menu as the tree does. */ + void handleClientLink(TsLink.Ref ref, Component source, int x, int y) { + ClientEntry client = conn.getModel().getClient(ref.id); + // The id is only valid for the session the link was made in; fall back to + // the unique id (and finally the nickname) so older links still resolve. + if (client == null || (!ref.uniqueId.isEmpty() && !ref.uniqueId.equals(client.uniqueId))) { + ClientEntry byUid = ref.uniqueId.isEmpty() ? null + : conn.getModel().findClientByUniqueId(ref.uniqueId); + if (byUid == null && !ref.name.isEmpty()) byUid = conn.getModel().findClientByName(ref.name); + if (byUid != null) client = byUid; + } + if (client == null) { + chatPanel.appendSystem("That client is no longer on the server."); + return; + } + ClientMenu.build(client, client.id == conn.getSelfClientId(), this).show(source, x, y); + } + + void handleChannelLink(TsLink.Ref ref, Component source, int x, int y) { + ChannelNode channel = conn.getModel().getChannel(ref.id); + if (channel == null) { + chatPanel.appendSystem("That channel no longer exists."); + return; + } + ChannelMenu.build(channel, this).show(source, x, y); + } + + // ---- ServerTreePanel.Actions ---- + + @Override + public void joinChannel(int channelId) { + if (conn.isConnected()) conn.joinChannel(channelId, null); + } + + @Override + public void moveClientToChannel(ClientEntry client, ChannelNode target) { + if (!conn.isConnected()) return; + if (client.id == conn.getSelfClientId()) { + conn.joinChannel(target.id, null); + } else { + conn.moveClient(client.id, target.id, null); + } + } + + @Override + public void moveChannel(ChannelNode channel, int newParentId, int orderPredecessorId) { + if (conn.isConnected()) conn.moveChannel(channel.id, newParentId, orderPredecessorId); + } + + @Override + public void openPrivateChat(ClientEntry client) { + chatPanel.openPrivateChat(client.id, client.nickname); + } + + @Override + public void pokeClient(ClientEntry client) { + String msg = JOptionPane.showInputDialog(host, "Poke message for " + client.nickname + ":", "Poke!"); + if (msg != null) conn.poke(client.id, msg); + } + + @Override + public void kickClientFromChannel(ClientEntry client) { + String reason = kickReason("Kick Client from Channel", client); + if (reason != null) conn.kickFromChannel(client.id, reason); + } + + @Override + public void kickClientFromServer(ClientEntry client) { + String reason = kickReason("Kick Client from Server", client); + if (reason != null) conn.kickFromServer(client.id, reason); + } + + private String kickReason(String title, ClientEntry client) { + if (!conn.isConnected()) return null; + return ReasonDialog.prompt(host, title, "Reason for kicking " + client.nickname + ":", + ReasonDialog.KICK_REASON_LIMIT); + } + + @Override + public void banClient(ClientEntry client) { + if (!conn.isConnected()) return; + BanDialog dialog = new BanDialog(host, client.nickname); + dialog.setVisible(true); + if (dialog.isConfirmed()) conn.banClient(client.id, dialog.getSeconds(), dialog.getReason()); + } + + @Override + public void toggleClientMute(ClientEntry client) { + if (conn.getPlayback() == null) return; + boolean now = !conn.getPlayback().isClientMuted(client.id); + conn.getPlayback().setClientMuted(client.id, now); + } + + @Override + public void findClientInTree(ClientEntry client) { + host.selectTab(tab); + treePanel.selectClient(client.id); + } + + @Override + public void showConnectionInfo(ClientEntry client) { + if (!conn.isConnected()) return; + new ConnectionInfoDialog(host, conn, client.id, client.nickname).setVisible(true); + } + + @Override + public void moveClientToOwnChannel(ClientEntry client) { + if (!conn.isConnected() || client.id == conn.getSelfClientId()) return; + ClientEntry self = conn.getModel().getClient(conn.getSelfClientId()); + if (self != null) conn.moveClient(client.id, self.channelId, null); + } + + @Override + public void setChannelSubscribed(ChannelNode channel, boolean family, boolean subscribed) { + if (conn.isConnected()) conn.setChannelsSubscribed(channel.familyIds(family), subscribed); + } + + @Override + public void browseFiles(ChannelNode channel) { + if (!conn.canTransferFiles()) return; + new FileBrowserDialog(host, conn, channel).setVisible(true); + } + + @Override + public boolean isClientLocallyMuted(int clientId) { + return conn.getPlayback() != null && conn.getPlayback().isClientMuted(clientId); + } + + @Override + public void onSelectionChanged(Object userObject) { + currentSelection = userObject; + renderInfo(); + if (!conn.isConnected()) return; + if (userObject instanceof ChannelNode) { + ChannelNode ch = (ChannelNode) userObject; + if (!ch.descriptionLoaded) conn.requestChannelInfo(ch.id); + } else if (userObject instanceof ClientEntry) { + conn.requestClientInfo(((ClientEntry) userObject).id); + } + } +}