package com.ts3client.ui; import com.ts3client.net.ConnectionStats; import com.ts3client.net.TeamspeakConnection; import javax.swing.BorderFactory; import javax.swing.Box; import javax.swing.BoxLayout; import javax.swing.JButton; import javax.swing.JDialog; import javax.swing.JLabel; import javax.swing.JPanel; import javax.swing.JTabbedPane; import javax.swing.SwingUtilities; import java.awt.BorderLayout; import java.awt.Color; import java.awt.Dimension; import java.awt.Frame; import java.awt.GridBagConstraints; import java.awt.GridBagLayout; import java.awt.Insets; import java.awt.event.WindowAdapter; import java.awt.event.WindowEvent; import java.text.DecimalFormat; /** * Live "Client Connection Info" window, reachable from a client's right-click * menu. Mirrors the TeamSpeak 3 dialog: a summary header (address, version, * platform, idle/connected time, ping, filetransfer) above a * Total / Speech / Keep Alive / Control tab strip, each tab showing * that category's packet loss, packet and byte totals and live bandwidth. * *
Polls the {@link TeamspeakConnection} once a second while open; for the * local client the figures update live from local counters, for a remote client * they refresh from the server's connection report. */ public final class ConnectionInfoDialog extends JDialog { private static final int REFRESH_MS = 1000; private final TeamspeakConnection conn; private final int clientId; private final javax.swing.Timer timer; private final java.util.concurrent.atomic.AtomicBoolean inFlight = new java.util.concurrent.atomic.AtomicBoolean(); private final JLabel addressValue = value(); private final JLabel versionValue = value(); private final JLabel platformValue = value(); private final JLabel idleValue = value(); private final JLabel connectedValue = value(); private final JLabel pingValue = value(); private final JLabel filetransferValue = value(); private final KindTab totalTab = new KindTab(); private final KindTab speechTab = new KindTab(); private final KindTab keepAliveTab = new KindTab(); private final KindTab controlTab = new KindTab(); public ConnectionInfoDialog(Frame owner, TeamspeakConnection conn, int clientId, String nickname) { super(owner, "Connection Info — " + nickname, false); this.conn = conn; this.clientId = clientId; JPanel content = new JPanel(new BorderLayout(0, 10)); content.setBackground(Theme.WINDOW_BG); content.setBorder(BorderFactory.createEmptyBorder(12, 14, 12, 14)); content.add(buildSummary(), BorderLayout.NORTH); content.add(buildTabs(), BorderLayout.CENTER); content.add(buildButtons(), BorderLayout.SOUTH); setContentPane(content); pack(); setLocationRelativeTo(owner); addWindowListener(new WindowAdapter() { @Override public void windowClosed(WindowEvent e) { timer.stop(); } }); timer = new javax.swing.Timer(REFRESH_MS, e -> refresh()); timer.setInitialDelay(0); timer.start(); } // ---- construction ---- private JPanel buildSummary() { JPanel grid = new JPanel(new GridBagLayout()); grid.setBackground(Theme.WINDOW_BG); int row = 0; addRow(grid, row++, "Address", addressValue); addRow(grid, row++, "Client version", versionValue); addRow(grid, row++, "Platform", platformValue); addRow(grid, row++, "Ping", pingValue); addRow(grid, row++, "Idle time", idleValue); addRow(grid, row++, "Connected", connectedValue); addRow(grid, row, "Filetransfer (↑/↓)", filetransferValue); return grid; } private JTabbedPane buildTabs() { JTabbedPane tabs = new JTabbedPane(); tabs.setFont(Theme.UI_FONT); tabs.setBackground(Theme.WINDOW_BG); tabs.addTab("Total", totalTab); tabs.addTab("Speech", speechTab); tabs.addTab("Keep Alive", keepAliveTab); tabs.addTab("Control", controlTab); return tabs; } private JPanel buildButtons() { JPanel bar = new JPanel(); bar.setLayout(new BoxLayout(bar, BoxLayout.X_AXIS)); bar.setBackground(Theme.WINDOW_BG); bar.add(Box.createHorizontalGlue()); JButton close = new JButton("Close"); close.addActionListener(e -> dispose()); bar.add(close); return bar; } private static JLabel value() { JLabel l = new JLabel("—"); l.setFont(Theme.UI_FONT); l.setForeground(Theme.TREE_TEXT); return l; } private static void addRow(JPanel grid, int row, String label, JLabel valueLabel) { GridBagConstraints lc = new GridBagConstraints(); lc.gridx = 0; lc.gridy = row; lc.anchor = GridBagConstraints.WEST; lc.insets = new Insets(2, 0, 2, 16); JLabel key = new JLabel(label); key.setFont(Theme.UI_FONT); key.setForeground(new Color(0x5A6B7B)); grid.add(key, lc); GridBagConstraints vc = new GridBagConstraints(); vc.gridx = 1; vc.gridy = row; vc.weightx = 1; vc.anchor = GridBagConstraints.WEST; vc.fill = GridBagConstraints.HORIZONTAL; vc.insets = new Insets(2, 0, 2, 0); grid.add(valueLabel, vc); } // ---- live update ---- private void refresh() { if (!conn.isConnected()) { timer.stop(); return; } if (!inFlight.compareAndSet(false, true)) return; // a previous poll is still running conn.requestConnectionInfo(clientId, stats -> SwingUtilities.invokeLater(() -> { inFlight.set(false); apply(stats); })); } private void apply(ConnectionStats s) { if (!isDisplayable()) return; addressValue.setText(s.self ? "This computer" : orDash(s.ip)); versionValue.setText(orDash(s.version)); platformValue.setText(orDash(s.platform)); pingValue.setText(formatPing(s.pingMs, s.pingDeviationMs)); idleValue.setText(formatDuration(s.idleTimeMs)); connectedValue.setText(formatDuration(s.connectedTimeMs)); filetransferValue.setText(formatRate(s.filetransferBandwidthSent) + " / " + formatRate(s.filetransferBandwidthReceived)); totalTab.update(totalSample(s)); speechTab.update(kindSample(s, ConnectionStats.Kind.SPEECH)); keepAliveTab.update(kindSample(s, ConnectionStats.Kind.KEEPALIVE)); controlTab.update(kindSample(s, ConnectionStats.Kind.CONTROL)); growToFit(); } /** * The window is first packed around placeholder text; once real (longer) * values arrive it may need more room. Grow to fit, but never shrink, so the * window doesn't jitter as counters tick up each second. */ private void growToFit() { Dimension pref = getContentPane().getPreferredSize(); Dimension have = getContentPane().getSize(); if (pref.width > have.width || pref.height > have.height) { pack(); } } private static Sample totalSample(ConnectionStats s) { Sample sample = new Sample(); sample.packetLoss = s.packetLoss; sample.packetsSent = s.packetsSentTotal; sample.packetsReceived = s.packetsReceivedTotal; sample.bytesSent = s.bytesSentTotal; sample.bytesReceived = s.bytesReceivedTotal; sample.bwSentSecond = s.bandwidthSentLastSecond; sample.bwReceivedSecond = s.bandwidthReceivedLastSecond; sample.bwSentMinute = s.bandwidthSentLastMinute; sample.bwReceivedMinute = s.bandwidthReceivedLastMinute; return sample; } private static Sample kindSample(ConnectionStats s, ConnectionStats.Kind kind) { Sample sample = new Sample(); ConnectionStats.KindStats k = s.kind(kind); if (k == null) return sample; // all fields stay unknown (-1) sample.packetLoss = k.packetLoss; sample.packetsSent = k.packetsSent; sample.packetsReceived = k.packetsReceived; sample.bytesSent = k.bytesSent; sample.bytesReceived = k.bytesReceived; sample.bwSentSecond = k.bandwidthSentLastSecond; sample.bwReceivedSecond = k.bandwidthReceivedLastSecond; sample.bwSentMinute = k.bandwidthSentLastMinute; sample.bwReceivedMinute = k.bandwidthReceivedLastMinute; return sample; } // ---- one category tab ---- /** Numeric snapshot fed to a {@link KindTab}; -1 means "unknown". */ private static final class Sample { double packetLoss = -1; long packetsSent = -1; long packetsReceived = -1; long bytesSent = -1; long bytesReceived = -1; long bwSentSecond = -1; long bwReceivedSecond = -1; long bwSentMinute = -1; long bwReceivedMinute = -1; } private static final class KindTab extends JPanel { private final JLabel packetLoss = value(); private final JLabel packets = value(); private final JLabel bytes = value(); private final JLabel bandwidthSecond = value(); private final JLabel bandwidthMinute = value(); KindTab() { super(new GridBagLayout()); setBackground(Theme.WINDOW_BG); setBorder(BorderFactory.createEmptyBorder(10, 12, 10, 12)); int row = 0; addRow(this, row++, "Packet loss", packetLoss); addRow(this, row++, "Packets (↑/↓)", packets); addRow(this, row++, "Transferred (↑/↓)", bytes); addRow(this, row++, "Bandwidth · last second (↑/↓)", bandwidthSecond); addRow(this, row, "Bandwidth · last minute (↑/↓)", bandwidthMinute); } void update(Sample s) { packetLoss.setText(formatLoss(s.packetLoss)); packets.setText(formatCount(s.packetsSent) + " / " + formatCount(s.packetsReceived)); bytes.setText(formatBytes(s.bytesSent) + " / " + formatBytes(s.bytesReceived)); bandwidthSecond.setText(formatRate(s.bwSentSecond) + " / " + formatRate(s.bwReceivedSecond)); bandwidthMinute.setText(formatRate(s.bwSentMinute) + " / " + formatRate(s.bwReceivedMinute)); } } // ---- formatting ---- private static final DecimalFormat GROUPED = new DecimalFormat("#,##0"); private static String orDash(String s) { return (s == null || s.isEmpty()) ? "—" : s; } private static String formatPing(double ms, double deviationMs) { if (ms < 0) return "—"; String base = Math.round(ms) + " ms"; if (deviationMs > 0) base += " (± " + Math.round(deviationMs) + " ms)"; return base; } private static String formatLoss(double fraction) { if (fraction < 0) return "—"; return new DecimalFormat("0.00").format(fraction * 100.0) + " %"; } private static String formatCount(long n) { return n < 0 ? "—" : GROUPED.format(n); } private static String formatBytes(long bytes) { if (bytes < 0) return "—"; if (bytes < 1024) return bytes + " B"; double kib = bytes / 1024.0; if (kib < 1024) return new DecimalFormat("0.0").format(kib) + " KiB"; double mib = kib / 1024.0; if (mib < 1024) return new DecimalFormat("0.00").format(mib) + " MiB"; return new DecimalFormat("0.00").format(mib / 1024.0) + " GiB"; } private static String formatRate(long bytesPerSecond) { return bytesPerSecond < 0 ? "—" : formatBytes(bytesPerSecond) + "/s"; } private static String formatDuration(long ms) { if (ms < 0) return "—"; long totalSeconds = ms / 1000; long days = totalSeconds / 86400; long hours = (totalSeconds % 86400) / 3600; long minutes = (totalSeconds % 3600) / 60; long seconds = totalSeconds % 60; StringBuilder sb = new StringBuilder(); if (days > 0) sb.append(days).append("d "); if (days > 0 || hours > 0) sb.append(hours).append("h "); if (days > 0 || hours > 0 || minutes > 0) sb.append(minutes).append("m "); sb.append(seconds).append("s"); return sb.toString(); } }