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>
114 lines
3.9 KiB
Java
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×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;
|
|
}
|
|
}
|
|
}
|