Files
ts3java/ts3-client/swing/src/main/java/com/ts3client/ui/GroupIcons.java
ericek111 9d996f48a2 Scale the interface fractionally on Linux
Java2D on X11 (and so under XWayland) truncates its scale to a whole
number, drawing 150 % at 100 %. On Linux, pin Java2D to 1 and hand the
full factor to FlatLaf, taken from XSETTINGS Xft/DPI, KWin's Xwayland
scale or GDK_SCALE, or chosen in Options → Design.

The client's own sizes now go through UIScale: borders, gaps, dialog and
row sizes, icons (rendered at the scaled size, so they stay crisp),
custom-painted components and font sizes in the HTML views.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-23 22:33:27 +00:00

114 lines
3.9 KiB
Java

package com.ts3client.ui;
import com.formdev.flatlaf.util.UIScale;
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) {
if (iconId == 0) return null;
// TeamSpeak's default group icons belong to the icon pack, which draws them in
// its own style; only server-uploaded icons come off the wire. The theme keeps
// its own cache, so those are not cached again here.
if (iconId > 0 && iconId <= IconRepository.MAX_BUNDLED_ID) {
ImageIcon themed = IconTheme.icon("GROUP_" + iconId, UIScale.scale(SIZE));
if (themed != null) return themed;
}
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;
int size = UIScale.scale(SIZE);
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() + UIScale.scale(GAP);
}
}
@Override
public int getIconWidth() {
int width = 0;
int gap = UIScale.scale(GAP);
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;
}
}
}