From 610dce1bd963059647a13fd4e9170d650c7e5597 Mon Sep 17 00:00:00 2001 From: ericek111 Date: Fri, 14 Aug 2026 07:09:21 +0000 Subject: [PATCH] Show each channel's own icon in the tree Channels carry a channel_icon_id just like groups do; track it on ChannelNode from the channel list and the create/edit events, and paint it in the same right-aligned strip that already carries a client's group icons. Spacers keep their bare look. Co-Authored-By: Claude Opus 5 --- .../java/com/ts3client/net/ChannelNode.java | 2 ++ .../ts3client/net/TeamspeakConnection.java | 20 ++++++++--- .../com/ts3client/ui/ServerTreePanel.java | 34 ++++++++++++------- 3 files changed, 38 insertions(+), 18 deletions(-) diff --git a/ts3-client/core/src/main/java/com/ts3client/net/ChannelNode.java b/ts3-client/core/src/main/java/com/ts3client/net/ChannelNode.java index b8e1ff2..c586f2e 100644 --- a/ts3-client/core/src/main/java/com/ts3client/net/ChannelNode.java +++ b/ts3-client/core/src/main/java/com/ts3client/net/ChannelNode.java @@ -15,6 +15,8 @@ public final class ChannelNode { public boolean hasPassword; public boolean permanent; public int maxClients = -1; + /** Id of the channel's custom icon in the server's file repository, or 0 for none. */ + public long iconId; /** Populated when the tree is rebuilt. */ public final List children = new ArrayList<>(); diff --git a/ts3-client/core/src/main/java/com/ts3client/net/TeamspeakConnection.java b/ts3-client/core/src/main/java/com/ts3client/net/TeamspeakConnection.java index 9dcefd1..0d6d39c 100644 --- a/ts3-client/core/src/main/java/com/ts3client/net/TeamspeakConnection.java +++ b/ts3-client/core/src/main/java/com/ts3client/net/TeamspeakConnection.java @@ -316,6 +316,10 @@ public final class TeamspeakConnection implements TS3Listener { node.permanent = ch.isPermanent(); node.topic = ch.getTopic() == null ? "" : ch.getTopic(); node.maxClients = ch.getMaxClients(); + // channellist omits the icon on some servers; keep what events gave us. + if (ch.get("channel_icon_id") != null) { + node.iconId = safeLong(ch.get("channel_icon_id")); + } } } } catch (Exception e) { @@ -607,7 +611,8 @@ public final class TeamspeakConnection implements TS3Listener { int pid = safeInt(e, "cpid"); if (pid == 0) pid = safeInt(e, "pid"); int order = safeInt(e, "channel_order"); - model.putChannel(cid, name, pid, order); + ChannelNode node = model.putChannel(cid, name, pid, order); + node.iconId = safeLong(e, "channel_icon_id"); model.relinkChannel(cid, pid, order); ui.onModelChanged(); } @@ -625,6 +630,7 @@ public final class TeamspeakConnection implements TS3Listener { String name = e.get("channel_name"); if (name != null) ch.name = name; if (e.get("channel_order") != null) ch.order = e.getInt("channel_order"); + if (e.get("channel_icon_id") != null) ch.iconId = safeLong(e, "channel_icon_id"); ui.onModelChanged(); } } @@ -647,7 +653,7 @@ public final class TeamspeakConnection implements TS3Listener { String name = e.get("channel_name"); int pid = safeInt(e, "cpid"); int order = safeInt(e, "channel_order"); - model.putChannel(cid, name, pid, order); + model.putChannel(cid, name, pid, order).iconId = safeLong(e, "channel_icon_id"); } @Override @@ -1047,10 +1053,14 @@ public final class TeamspeakConnection implements TS3Listener { // ---- helpers ---- private static long safeLong(BaseEvent e, String key) { + return safeLong(e.get(key)); + } + + /** @return the parsed value, or 0 when it is absent or not a number */ + private static long safeLong(String value) { try { - String v = e.get(key); - return v == null ? 0 : Long.parseLong(v.trim()); - } catch (Exception ex) { + return value == null ? 0 : Long.parseLong(value.trim()); + } catch (NumberFormatException ex) { return 0; } } diff --git a/ts3-client/swing/src/main/java/com/ts3client/ui/ServerTreePanel.java b/ts3-client/swing/src/main/java/com/ts3client/ui/ServerTreePanel.java index a54d6b1..4c6c493 100644 --- a/ts3-client/swing/src/main/java/com/ts3client/ui/ServerTreePanel.java +++ b/ts3-client/swing/src/main/java/com/ts3client/ui/ServerTreePanel.java @@ -375,10 +375,10 @@ public final class ServerTreePanel extends JScrollPane { private static final int BADGE_MARGIN = 4; /** - * Paints every visible client's group icons flush with the right edge of the - * viewport, the way TeamSpeak lines them up. Drawing them here rather than in the - * cell renderer keeps the rows' measured widths (and thus the selection highlight) - * tied to the label alone. + * Paints the icons of every visible row — a client's group icons, a channel's own + * icon — flush with the right edge of the viewport, the way TeamSpeak lines them up. + * Drawing them here rather than in the cell renderer keeps the rows' measured widths + * (and thus the selection highlight) tied to the label alone. */ private void paintBadges(Graphics g, JTree tree) { Rectangle visible = tree.getVisibleRect(); @@ -390,11 +390,7 @@ public final class ServerTreePanel extends JScrollPane { TreePath path = tree.getPathForRow(row); Object obj = ((DefaultMutableTreeNode) path.getLastPathComponent()).getUserObject(); - if (!(obj instanceof ClientEntry)) continue; - ClientEntry cl = (ClientEntry) obj; - - List icons = groupIcons.iconsOf( - model.serverGroupsOf(cl.serverGroupIds), model.channelGroup(cl.channelGroupId)); + List icons = badgesOf(obj); if (icons.isEmpty()) continue; GroupIcons.Row strip = new GroupIcons.Row(icons); @@ -403,6 +399,21 @@ public final class ServerTreePanel extends JScrollPane { } } + /** The icon strip a row shows on its right, empty when it has none (yet). */ + private List badgesOf(Object node) { + if (node instanceof ClientEntry) { + ClientEntry cl = (ClientEntry) node; + return groupIcons.iconsOf( + model.serverGroupsOf(cl.serverGroupIds), model.channelGroup(cl.channelGroupId)); + } + if (node instanceof ChannelNode) { + ChannelNode ch = (ChannelNode) node; + Icon icon = Spacers.isSpacer(ch.name) ? null : groupIcons.icon(ch.iconId); + if (icon != null) return List.of(icon); + } + return List.of(); + } + /** * Draws where the drop will land: an insertion line between rows, or an outline * around the row that will receive the dragged node. @@ -564,10 +575,7 @@ public final class ServerTreePanel extends JScrollPane { } else if (obj instanceof ClientEntry) { ClientEntry cl = (ClientEntry) obj; String label = cl.nickname; - boolean hasIcons = !groupIcons.iconsOf( - model.serverGroupsOf(cl.serverGroupIds), - model.channelGroup(cl.channelGroupId)).isEmpty(); - if (!hasIcons) { + if (badgesOf(cl).isEmpty()) { // No icons (yet): fall back to naming the primary group inline. String primaryGroup = model.primaryServerGroupName(cl.serverGroupIds); if (primaryGroup != null) label += " [" + primaryGroup + "]";