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 <noreply@anthropic.com>
This commit is contained in:
2026-08-14 07:09:21 +00:00
parent 7c86a1f166
commit 610dce1bd9
3 changed files with 38 additions and 18 deletions

View File

@@ -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<ChannelNode> children = new ArrayList<>();

View File

@@ -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;
}
}

View File

@@ -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<Icon> icons = groupIcons.iconsOf(
model.serverGroupsOf(cl.serverGroupIds), model.channelGroup(cl.channelGroupId));
List<Icon> 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<Icon> 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 + "]";