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. * *
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(); } }