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:
@@ -6,6 +6,7 @@ import com.ts3client.net.ServerModel;
|
||||
import com.ts3client.text.TsLink;
|
||||
|
||||
import javax.swing.DropMode;
|
||||
import javax.swing.Icon;
|
||||
import javax.swing.ImageIcon;
|
||||
import javax.swing.JComponent;
|
||||
import javax.swing.JScrollPane;
|
||||
@@ -77,11 +78,13 @@ public final class ServerTreePanel extends JScrollPane {
|
||||
private final DefaultMutableTreeNode root = new DefaultMutableTreeNode();
|
||||
private final DefaultTreeModel treeModel = new DefaultTreeModel(root);
|
||||
private final ServerModel model;
|
||||
private final GroupIcons groupIcons;
|
||||
private final Actions actions;
|
||||
private int selfClientId = -1;
|
||||
|
||||
public ServerTreePanel(ServerModel model, Actions actions) {
|
||||
public ServerTreePanel(ServerModel model, GroupIcons groupIcons, Actions actions) {
|
||||
this.model = model;
|
||||
this.groupIcons = groupIcons;
|
||||
this.actions = actions;
|
||||
root.setUserObject("Not connected");
|
||||
this.tree = new DropIndicatorTree(treeModel);
|
||||
@@ -364,11 +367,47 @@ public final class ServerTreePanel extends JScrollPane {
|
||||
}
|
||||
}
|
||||
|
||||
// ---- group icon strip ----
|
||||
|
||||
/** Gap kept between a row's label and the right-aligned icon strip. */
|
||||
private static final int BADGE_GAP = 8;
|
||||
/** Inset of the strip from the visible right edge. */
|
||||
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.
|
||||
*/
|
||||
private void paintBadges(Graphics g, JTree tree) {
|
||||
Rectangle visible = tree.getVisibleRect();
|
||||
int right = visible.x + visible.width - BADGE_MARGIN;
|
||||
for (int row = 0; row < tree.getRowCount(); row++) {
|
||||
Rectangle bounds = tree.getRowBounds(row);
|
||||
if (bounds == null || bounds.y + bounds.height < visible.y) continue;
|
||||
if (bounds.y > visible.y + visible.height) break;
|
||||
|
||||
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));
|
||||
if (icons.isEmpty()) continue;
|
||||
|
||||
GroupIcons.Row strip = new GroupIcons.Row(icons);
|
||||
int x = Math.max(bounds.x + bounds.width + BADGE_GAP, right - strip.getIconWidth());
|
||||
strip.paintIcon(tree, g, x, bounds.y + (bounds.height - strip.getIconHeight()) / 2);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Draws where the drop will land: an insertion line between rows, or an outline
|
||||
* around the row that will receive the dragged node.
|
||||
*/
|
||||
private static final class DropIndicatorTree extends JTree {
|
||||
private final class DropIndicatorTree extends JTree {
|
||||
/** Set while a drop would move a client into this channel row. */
|
||||
private TreePath highlight;
|
||||
|
||||
@@ -385,6 +424,7 @@ public final class ServerTreePanel extends JScrollPane {
|
||||
@Override
|
||||
protected void paintComponent(Graphics g) {
|
||||
super.paintComponent(g);
|
||||
paintBadges(g, this);
|
||||
JTree.DropLocation loc = getDropLocation();
|
||||
if (loc == null || loc.getPath() == null) return;
|
||||
|
||||
@@ -477,7 +517,26 @@ public final class ServerTreePanel extends JScrollPane {
|
||||
tree.repaint();
|
||||
}
|
||||
|
||||
/**
|
||||
* Re-measures every visible row, for changes that alter a row's width (a group
|
||||
* icon that finished downloading). A plain repaint would keep the cached widths
|
||||
* and clip the new icons.
|
||||
*/
|
||||
public void refreshRowSizes() {
|
||||
for (int i = tree.getRowCount() - 1; i >= 0; i--) {
|
||||
TreePath path = tree.getPathForRow(i);
|
||||
if (path != null) {
|
||||
treeModel.nodeChanged((DefaultMutableTreeNode) path.getLastPathComponent());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Draws a tree row: the status icon and the label. The group icon strip is painted
|
||||
* separately, right-aligned, by {@link #paintBadges}.
|
||||
*/
|
||||
private final class Renderer extends DefaultTreeCellRenderer {
|
||||
|
||||
@Override
|
||||
public Component getTreeCellRendererComponent(JTree tree, Object value, boolean sel,
|
||||
boolean expanded, boolean leaf, int row,
|
||||
@@ -504,9 +563,15 @@ public final class ServerTreePanel extends JScrollPane {
|
||||
}
|
||||
} else if (obj instanceof ClientEntry) {
|
||||
ClientEntry cl = (ClientEntry) obj;
|
||||
String primaryGroup = model.primaryServerGroupName(cl.serverGroupIds);
|
||||
String label = cl.nickname;
|
||||
if (primaryGroup != null) label += " [" + primaryGroup + "]";
|
||||
boolean hasIcons = !groupIcons.iconsOf(
|
||||
model.serverGroupsOf(cl.serverGroupIds),
|
||||
model.channelGroup(cl.channelGroupId)).isEmpty();
|
||||
if (!hasIcons) {
|
||||
// No icons (yet): fall back to naming the primary group inline.
|
||||
String primaryGroup = model.primaryServerGroupName(cl.serverGroupIds);
|
||||
if (primaryGroup != null) label += " [" + primaryGroup + "]";
|
||||
}
|
||||
setText(label);
|
||||
setIcon(iconFor(cl));
|
||||
setForeground(cl.talking ? Theme.TALKING : Theme.TREE_TEXT);
|
||||
|
||||
Reference in New Issue
Block a user