Show server and channel group icons in the tree

Download the icons a server advertises for its groups over the file
transfer channel, cache them under the config directory, and draw them as
a badge strip on each client's row, right-aligned against the tree's
visible edge like the TeamSpeak client does. Groups without a custom icon
fall back to the bundled default set.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-08-14 07:06:23 +00:00
parent 0333b92ce5
commit 7c86a1f166
18 changed files with 597 additions and 38 deletions

View File

@@ -0,0 +1,101 @@
package com.ts3client.ui;
import com.ts3client.net.Group;
import com.ts3client.net.IconRepository;
import javax.swing.Icon;
import javax.swing.ImageIcon;
import java.awt.Component;
import java.awt.Graphics;
import java.awt.Image;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
/**
* Turns the {@link IconRepository}'s icon bytes into Swing icons for one server's
* groups, caching the decoded images. Icons that still have to be downloaded simply
* do not render yet; the repository repaints the view once they arrive.
*/
public final class GroupIcons {
/** Tree rows are 16&times;16 like TeamSpeak's own icon set. */
private static final int SIZE = 16;
private final IconRepository repository;
private final Map<Long, ImageIcon> decoded = new ConcurrentHashMap<>();
public GroupIcons(IconRepository repository) {
this.repository = repository;
}
/** @return the group's icon, or {@code null} if it has none or it is not loaded yet */
public ImageIcon iconOf(Group group) {
if (group == null || group.iconId == 0) return null;
return icon(group.iconId);
}
public ImageIcon icon(long iconId) {
ImageIcon cached = decoded.get(iconId);
if (cached != null) return cached;
byte[] data = repository.get(iconId);
if (data == null) return null;
ImageIcon icon = new ImageIcon(data);
if (icon.getIconWidth() <= 0) return null;
if (icon.getIconWidth() != SIZE || icon.getIconHeight() != SIZE) {
icon = new ImageIcon(icon.getImage().getScaledInstance(SIZE, SIZE, Image.SCALE_SMOOTH));
}
decoded.put(iconId, icon);
return icon;
}
/** Collects the icons of a client's server groups plus its channel group, in display order. */
public List<Icon> iconsOf(List<Group> serverGroups, Group channelGroup) {
List<Icon> icons = new ArrayList<>();
for (Group g : serverGroups) {
ImageIcon icon = iconOf(g);
if (icon != null) icons.add(icon);
}
ImageIcon channelIcon = iconOf(channelGroup);
if (channelIcon != null) icons.add(channelIcon);
return icons;
}
/** Lays several icons out in a row, so a single tree cell can show a badge strip. */
public static final class Row implements Icon {
private static final int GAP = 2;
private final List<Icon> icons;
public Row(List<Icon> icons) {
this.icons = icons;
}
@Override
public void paintIcon(Component c, Graphics g, int x, int y) {
int offset = x;
for (Icon icon : icons) {
icon.paintIcon(c, g, offset, y + (getIconHeight() - icon.getIconHeight()) / 2);
offset += icon.getIconWidth() + GAP;
}
}
@Override
public int getIconWidth() {
int width = 0;
for (Icon icon : icons) width += icon.getIconWidth() + GAP;
return Math.max(0, width - GAP);
}
@Override
public int getIconHeight() {
int height = 0;
for (Icon icon : icons) height = Math.max(height, icon.getIconHeight());
return height;
}
}
}