Files
ts3j/ts3-client/swing/src/main/java/com/ts3client/ui/InfoPanel.java
ericek111 75ae6b89b2 Report mute/hardware status correctly, add Local Mic Mute, fix stale talking indicator
- Mute/deafen status now goes out through clientupdate instead of
  clientedit, which silently rejected client_input_muted/output_muted
  since they are runtime status, not editable client properties.
- Publish client_input_hardware so other clients see "Microphone
  Disabled" instead of silence while another tab holds the capture
  device; track input/output hardware flags for other clients too,
  and show a distinct grey "disabled" icon instead of reusing the red
  "muted" one for both the tree and the info panel.
- Implement TS3's Enable/Disable/Toggle Local Mic Mute hotkeys: they
  silence capture like a real mute, but never touch the published
  mute status or play a sound.
- A speaker's "talking" indicator only ever cleared when its
  zero-length end-of-burst voice packet arrived; if that one UDP
  packet was lost, the indicator stuck until their next burst. Add a
  watchdog that clears it after 200ms of silence from that speaker
  regardless.
2026-08-17 08:43:22 +00:00

151 lines
6.3 KiB
Java

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 {
private final JEditorPane pane = new JEditorPane();
public InfoPanel() {
javax.swing.text.html.HTMLEditorKit kit = new javax.swing.text.html.HTMLEditorKit();
javax.swing.text.html.StyleSheet css = new javax.swing.text.html.StyleSheet();
css.addStyleSheet(kit.getStyleSheet());
css.addRule("a { color:" + hex(Theme.CHAT_NAME) + "; text-decoration:none; }");
// Client references look exactly like a message author's name.
css.addRule("a." + BBCode.IDENTITY_LINK_CLASS
+ " { color:" + hex(Theme.CHAT_NAME) + "; font-weight:bold; text-decoration:none; }");
// External links are underlined so they can't be mistaken for an identity.
css.addRule("a." + BBCode.EXTERNAL_LINK_CLASS
+ " { color:" + hex(Theme.LINK) + "; text-decoration:underline; }");
kit.setStyleSheet(css);
pane.setEditorKit(kit);
pane.setEditable(false);
pane.setBackground(Theme.CHAT_BG);
pane.setBorder(BorderFactory.createEmptyBorder(6, 8, 6, 8));
pane.addHyperlinkListener(e -> {
if (e.getEventType() == javax.swing.event.HyperlinkEvent.EventType.ACTIVATED) {
Links.open(e.getDescription(), this);
}
});
setViewportView(pane);
setBorder(BorderFactory.createLineBorder(new java.awt.Color(0xD0D0D0)));
clear();
}
public void clear() {
setHtml("<i style='color:#8a8a8a'>Select a channel or client to see details.</i>");
}
public void showChannel(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("<hr>");
if (ch.description != null && !ch.description.isEmpty()) {
sb.append("<div>").append(multiline(ch.description)).append("</div>");
} else if (ch.descriptionLoaded) {
sb.append("<i style='color:#8a8a8a'>No description.</i>");
} else {
sb.append("<i style='color:#8a8a8a'>Loading description…</i>");
}
setHtml(sb.toString());
}
public void showClient(ClientEntry cl, ServerModel model, IconRepository icons) {
StringBuilder sb = new StringBuilder();
sb.append(heading(esc(cl.nickname) + (cl.self ? " <span style='color:#8a8a8a'>(you)</span>" : "")));
List<Group> serverGroups = model.serverGroupsOf(cl.serverGroupIds);
if (serverGroups.isEmpty()) {
List<String> 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("<hr><div>").append(multiline(cl.description)).append("</div>");
}
setHtml(sb.toString());
}
private void setHtml(String body) {
pane.setText("<html><body style='font-family:sans-serif;font-size:11px;color:#202020'>"
+ body + "</body></html>");
pane.setCaretPosition(0);
}
/**
* An inline {@code <img>} 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 "<img src='" + file.toURI() + "' width='14' height='14'> ";
}
private static String heading(String text) {
return "<div style='font-weight:bold;font-size:13px;margin-bottom:4px'>" + text + "</div>";
}
private static void row(StringBuilder sb, String label, String value) {
sb.append("<div style='margin:1px 0'><span style='color:#5a6b7b'>")
.append(label).append(":</span> ").append(value).append("</div>");
}
/** 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);
}
private static String hex(java.awt.Color c) {
return String.format("#%06X", c.getRGB() & 0xFFFFFF);
}
}