package com.ts3client.ui; import com.ts3client.net.ChannelNode; import com.ts3client.net.ClientEntry; import com.ts3client.net.Group; import com.ts3client.net.IconRepository; import com.ts3client.net.ServerModel; import com.ts3client.text.BBCode; import javax.swing.BorderFactory; import javax.swing.JEditorPane; import javax.swing.JScrollPane; import java.io.File; import java.util.List; /** * Read-only detail view for the currently selected channel or client, mirroring * the TeamSpeak 3 info box: channel topic/description, or a client's server and * channel groups, platform and version. */ public final class InfoPanel extends JScrollPane { /** Moves the whole info panel's content out to a persistent chat tab. */ public interface DescriptionHandler { /** Shows/updates {@code html} in the info chat tab, creating it if needed. */ void showInfoTab(String html, Runnable onClose); /** Closes the info chat tab (the user chose to show details in the panel again). */ void closeInfoTab(); /** Hides (or restores) the panel itself, freeing its space, while its content lives in the chat tab. */ void setPanelHidden(boolean hidden); } private static final String TOGGLE_HREF = "app:toggle-info-tab"; private final JEditorPane pane = HtmlStyles.pane("font-family:sans-serif; font-size:11px;"); private DescriptionHandler descriptionHandler; private ChannelNode shownChannel; private ClientEntry shownClient; private ServerModel shownModel; private IconRepository shownIcons; private boolean inChatTab; public InfoPanel() { pane.setBorder(BorderFactory.createEmptyBorder(6, 8, 6, 8)); pane.addHyperlinkListener(e -> { if (e.getEventType() != javax.swing.event.HyperlinkEvent.EventType.ACTIVATED) return; if (TOGGLE_HREF.equals(e.getDescription())) { inChatTab = !inChatTab; if (!inChatTab && descriptionHandler != null) descriptionHandler.closeInfoTab(); render(); } else { Links.open(e.getDescription(), this); } }); setViewportView(pane); HtmlStyles.fillViewport(pane); clear(); } @Override public void updateUI() { super.updateUI(); setBorder(BorderFactory.createLineBorder(Theme.border())); } public void clear() { shownChannel = null; shownClient = null; inChatTab = false; render(); } public void setDescriptionHandler(DescriptionHandler handler) { this.descriptionHandler = handler; } /** Runs when the user closes the info chat tab; shows details in the panel again. */ private void onInfoTabClosed() { inChatTab = false; render(); } public void showChannel(ChannelNode ch) { shownChannel = ch; shownClient = null; render(); } public void showClient(ClientEntry cl, ServerModel model, IconRepository icons) { shownClient = cl; shownChannel = null; shownModel = model; shownIcons = icons; render(); } private void render() { boolean selected = shownChannel != null || shownClient != null; boolean hide = inChatTab && selected; if (descriptionHandler != null) descriptionHandler.setPanelHidden(hide); if (hide) { descriptionHandler.showInfoTab(body(), this::onInfoTabClosed); } else { setHtml(body() + (selected ? toggleLink(true) : "")); } } private String body() { if (shownChannel != null) return channelBody(shownChannel); if (shownClient != null) return clientBody(shownClient, shownModel, shownIcons); return "Select a channel or client to see details."; } private static String channelBody(ChannelNode ch) { StringBuilder sb = new StringBuilder(); sb.append(heading(esc(ch.name))); row(sb, "Type", ch.permanent ? "Permanent" : "Temporary"); if (ch.maxClients >= 0) row(sb, "Max clients", Integer.toString(ch.maxClients)); if (ch.hasPassword) row(sb, "Password", "protected"); if (!ch.topic.isEmpty()) row(sb, "Topic", esc(ch.topic)); sb.append("
"); if (ch.description != null && !ch.description.isEmpty()) { sb.append("
").append(multiline(ch.description)).append("
"); } else if (ch.descriptionLoaded) { sb.append("No description."); } else { sb.append("Loading description…"); } return sb.toString(); } private static String clientBody(ClientEntry cl, ServerModel model, IconRepository icons) { StringBuilder sb = new StringBuilder(); sb.append(heading(esc(cl.nickname) + (cl.self ? " (you)" : ""))); List serverGroups = model.serverGroupsOf(cl.serverGroupIds); if (serverGroups.isEmpty()) { List names = model.serverGroupNames(cl.serverGroupIds); row(sb, "Server groups", names.isEmpty() ? "—" : esc(String.join(", ", names))); } else { StringBuilder groups = new StringBuilder(); for (Group g : serverGroups) { if (groups.length() > 0) groups.append(", "); groups.append(iconTag(g, icons)).append(esc(g.name)); } row(sb, "Server groups", groups.toString()); } Group channelGroup = model.channelGroup(cl.channelGroupId); row(sb, "Channel group", channelGroup != null ? iconTag(channelGroup, icons) + esc(channelGroup.name) : "#" + cl.channelGroupId); if (cl.talkPower != 0) row(sb, "Talk power", Integer.toString(cl.talkPower)); if (!cl.platform.isEmpty()) row(sb, "Platform", esc(cl.platform)); if (!cl.version.isEmpty()) row(sb, "Version", esc(cl.version)); if (cl.away) row(sb, "Status", "Away"); if (!cl.inputHardware) row(sb, "Microphone", "disabled"); else if (cl.inputMuted) row(sb, "Microphone", "muted"); if (!cl.outputHardware) row(sb, "Speakers", "disabled"); else if (cl.outputMuted) row(sb, "Speakers", "muted"); if (!cl.uniqueId.isEmpty()) row(sb, "Unique ID", esc(cl.uniqueId)); if (cl.description != null && !cl.description.isEmpty()) { sb.append("
").append(multiline(cl.description)).append("
"); } return sb.toString(); } /** The link that toggles between showing details here and in a chat tab. */ private static String toggleLink(boolean toChat) { return "
" + (toChat ? "Show in chat tab" : "Show here") + "
"; } private void setHtml(String body) { pane.setText("" + body + ""); pane.setCaretPosition(0); } /** * An inline {@code } for a group's icon, or nothing while the icon is missing * or still downloading. Swing's HTML renderer only loads images by URL, so the icon * is served from the repository's on-disk cache. */ private static String iconTag(Group group, IconRepository icons) { if (group.iconId == 0) return ""; File file = icons.file(group.iconId); if (file == null) return ""; return " "; } private static String heading(String text) { return "
" + text + "
"; } private static void row(StringBuilder sb, String label, String value) { sb.append("
") .append(label).append(": ").append(value).append("
"); } /** Descriptions may carry BBCode markup. */ private static String multiline(String text) { return BBCode.toHtml(text); } private static String esc(String s) { return BBCode.escape(s); } }