Files
ts3java/ts3-client/swing/src/main/java/com/ts3client/ui/ServerTab.java
ericek111 8f385b9343 Keep each server's session state in core
What a server tab knows beyond its views - the connection lifecycle,
title and status, the identity used, the microphone/speaker/away and
commander flags, chat sending and the channel lookups - moves from the
Swing ServerTab and ServerTabSelfState into core's ServerSession, so
another frontend gets the same behaviour without copying it.

ServerSession also takes over what the Swing event bridge decided on its
own: clearing the model on disconnect, naming the tab after the server,
showing senders under their contact names, and the chat's system lines.
The bridge now only updates views. SelfState moves along, while its
icons stay with the tray.

A server switch no longer needs to queue behind the UI thread: the
disconnect is reported synchronously, so the session is free again by
the time it reconnects.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-25 08:09:23 +00:00

217 lines
8.2 KiB
Java

package com.ts3client.ui;
import com.ts3client.audio.AudioBackend;
import com.ts3client.config.BanReasonPresets;
import com.ts3client.config.IdentityStore;
import com.ts3client.config.Settings;
import com.ts3client.contacts.ContactStore;
import com.ts3client.net.ChannelNode;
import com.ts3client.net.ConnectionListener.ChatScope;
import com.ts3client.net.TeamspeakConnection;
import com.ts3client.session.ServerSession;
import com.ts3client.sound.SoundNotifier;
import com.ts3client.text.TsLink;
import javax.swing.JComponent;
import javax.swing.JSplitPane;
import java.awt.Component;
import java.awt.event.ComponentAdapter;
import java.awt.event.ComponentEvent;
/**
* One server connection and the views bound to it (tree, info and chat). The
* 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.
*
* <p>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 {
private final MainFrame host;
private final Settings settings;
private final ServerSession session;
private final TeamspeakConnection conn;
private final GroupIcons groupIcons;
private final ServerTabTreeActions treeActions;
private final ServerTreePanel treePanel;
private final ChatPanel chatPanel;
private final InfoPanel infoPanel = new InfoPanel();
private final JComponent component;
private JSplitPane leftColumn;
private int normalDividerSize;
private int savedDividerLocation = -1;
/** Whether the saved divider share has been applied; before that, layout is not the user's doing. */
private boolean splitRestored;
/** The one open ban list window for this server, if any. */
private BanListDialog banList;
ServerTab(MainFrame host, Settings settings, IdentityStore identities, AudioBackend audio,
SoundNotifier sounds, ContactStore contacts) {
this.host = host;
this.settings = settings;
// 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.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(session, treePanel, chatPanel, treeActions);
chatPanel.setSendHandler(this::onSendChat);
chatPanel.setLinkHandler(new ChatPanel.LinkHandler() {
@Override
public void onClientLink(TsLink.Ref ref, Component source, int x, int y) {
treeActions.handleClientLink(ref, source, x, y);
}
@Override
public void onChannelLink(TsLink.Ref ref, Component source, int x, int y) {
treeActions.handleChannelLink(ref, source, x, y);
}
});
chatPanel.setInputEnabled(false);
infoPanel.setDescriptionHandler(new InfoPanel.DescriptionHandler() {
@Override
public void showInfoTab(String html, Runnable onClose) {
chatPanel.openDescriptionTab("info", Icons.channelClientPair(), "", html, onClose);
}
@Override
public void closeInfoTab() {
chatPanel.closeNoteTab("info");
}
@Override
public void setPanelHidden(boolean hidden) {
ServerTab.this.setInfoPanelHidden(hidden);
}
});
leftColumn = new JSplitPane(JSplitPane.VERTICAL_SPLIT, treePanel, infoPanel);
leftColumn.setResizeWeight(0.68);
leftColumn.setContinuousLayout(true);
normalDividerSize = leftColumn.getDividerSize();
// The info panel keeps the height the user gave it, in this tab and the next session.
leftColumn.addComponentListener(new ComponentAdapter() {
@Override
public void componentResized(ComponentEvent e) {
if (splitRestored) return;
splitRestored = true;
leftColumn.setDividerLocation(settings.infoSplitRatio);
}
});
leftColumn.addPropertyChangeListener(JSplitPane.DIVIDER_LOCATION_PROPERTY, e -> rememberSplit());
JSplitPane split = new JSplitPane(JSplitPane.HORIZONTAL_SPLIT, leftColumn, chatPanel);
split.setResizeWeight(0.55);
split.setDividerLocation(400);
split.setContinuousLayout(true);
this.component = split;
}
// ---- accessors ----
JComponent component() {
return component;
}
ServerSession session() {
return session;
}
TeamspeakConnection connection() {
return conn;
}
ChatPanel chat() {
return chatPanel;
}
/** Opens the server's ban list, or brings the already open one forward. */
void showBanList(BanReasonPresets presets) {
if (!session.isConnected()) return;
if (banList == null || !banList.isDisplayable()) {
banList = new BanListDialog(host, conn, settings, presets);
}
banList.setVisible(true);
banList.toFront();
}
/** The server's name changed or the connection went; the ban list window follows. */
void banListServerChanged(boolean connected) {
if (banList == null) return;
if (connected) banList.serverNameChanged();
else banList.connectionLost();
}
/** The contact list changed: names, colours and per-client playback follow. */
void contactsChanged() {
conn.applyContacts();
treePanel.refreshRowSizes();
treeActions.renderInfo();
}
// ---- dialogs ----
void showAvatarDialog() {
if (conn.isConnected()) new AvatarDialog(host, conn).setVisible(true);
}
// ---- chat ----
private void onSendChat(ChatPanel.Target target, int clientId, String text) {
ChatScope scope = switch (target) {
case SERVER -> ChatScope.SERVER;
case PRIVATE -> ChatScope.PRIVATE;
default -> ChatScope.CHANNEL;
};
session.sendChat(scope, clientId, text);
}
/** Opens the file repository browser for a channel, as the "Browse Files" hotkey/menu action does. */
void browseFiles(ChannelNode channel) {
treeActions.browseFiles(channel);
}
/**
* Records where the user put the divider, as a share of the column. Collapsed and
* pre-layout positions are not the user's choice, so they are left out.
*/
private void rememberSplit() {
if (!splitRestored || !infoPanel.isVisible()) return;
int span = leftColumn.getHeight() - leftColumn.getDividerSize();
if (span <= 0) return;
double ratio = leftColumn.getDividerLocation() / (double) span;
if (ratio <= 0.05 || ratio >= 0.98) return;
settings.infoSplitRatio = ratio;
}
/** Collapses the info panel to nothing while its content lives in the chat tab, or restores it. */
private void setInfoPanelHidden(boolean hidden) {
if (hidden == !infoPanel.isVisible()) return;
if (hidden) {
savedDividerLocation = leftColumn.getDividerLocation();
infoPanel.setVisible(false);
leftColumn.setDividerSize(0);
leftColumn.setDividerLocation(1.0);
} else {
infoPanel.setVisible(true);
leftColumn.setDividerSize(normalDividerSize);
if (savedDividerLocation >= 0) leftColumn.setDividerLocation(savedDividerLocation);
}
leftColumn.revalidate();
}
}