package com.ts3client.ui;
import com.github.weisj.jsvg.SVGDocument;
import com.github.weisj.jsvg.parser.LoaderContext;
import com.github.weisj.jsvg.parser.SVGLoader;
import com.github.weisj.jsvg.view.ViewBox;
import com.ts3client.config.Settings;
import com.ts3client.gfx.IconPack;
import com.ts3client.gfx.IconPacks;
import javax.imageio.ImageIO;
import javax.swing.ImageIcon;
import java.awt.Graphics2D;
import java.awt.Image;
import java.awt.RenderingHints;
import java.awt.image.BufferedImage;
import java.io.ByteArrayInputStream;
import java.io.File;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.CopyOnWriteArrayList;
/**
* The icons the user interface draws itself with, taken from the active
* {@link IconPack}.
*
*
Packs are TeamSpeak's, so an installed client's {@code gfx/} folder (and the
* packs from its add-on site) can be used as they are. A pack's own
* {@code FALLBACK} option decides whether icons it does not define are looked up
* in {@code default.zip}; whatever is still missing is drawn by {@link Icons}
* instead, so the client always has a complete set.
*
*
Rendering is cached per key and size: vector art is rasterised at the size it
* is asked for, bitmaps are scaled from the closest resolution the pack ships.
*/
public final class IconTheme {
/** The size of a tree row / menu item icon, as in the official client. */
public static final int SIZE = 16;
/** The size of the toolbar's buttons. */
public static final int TOOLBAR_SIZE = 24;
private static final IconTheme INSTANCE = new IconTheme();
private final Map cache = new ConcurrentHashMap<>();
private final List listeners = new CopyOnWriteArrayList<>();
private volatile List packs = List.of();
private volatile IconPack active;
private volatile IconPack fallback;
private IconTheme() {
}
public static IconTheme get() {
return INSTANCE;
}
/** The packs found on this machine, in display order. */
public List packs() {
return packs;
}
public IconPack activePack() {
return active;
}
/** Re-scans the pack folders and activates the one the settings name. */
public void reload(Settings settings) {
packs = IconPacks.findAll(new File(Settings.configDir(), "gfx"), settings.iconPackDir);
fallback = IconPacks.fallback(packs);
setPack(IconPacks.select(packs, settings.iconPack));
}
/** Switches to a pack (which may be {@code null} for the built-in icons). */
public void setPack(IconPack pack) {
if (active == pack) return;
active = pack;
cache.clear();
for (Runnable listener : listeners) listener.run();
}
/** Registers a repaint to run whenever the icons change. */
public void addListener(Runnable listener) {
listeners.add(listener);
}
/** @return the icon for a TeamSpeak icon key at 16×16, or {@code null} if no pack has it */
public static ImageIcon icon(String key) {
return icon(key, SIZE);
}
/** @return the icon for a TeamSpeak icon key, or {@code null} if no pack has it */
public static ImageIcon icon(String key, int size) {
return INSTANCE.lookup(key, size);
}
private ImageIcon lookup(String key, int size) {
if (key == null) return null;
String cacheKey = key + '@' + size;
ImageIcon cached = cache.get(cacheKey);
if (cached != null) return cached;
ImageIcon icon = render(active, key, size);
if (icon == null && active != null && active.fallsBack() && fallback != active) {
icon = render(fallback, key, size);
}
if (icon != null) cache.put(cacheKey, icon);
return icon;
}
private static ImageIcon render(IconPack pack, String key, int size) {
if (pack == null) return null;
byte[] data = pack.icon(key, size);
if (data == null || data.length == 0) return null;
BufferedImage image = pack.isVector(key, size) ? rasterize(data, size) : decode(data, size);
return image == null ? null : new ImageIcon(image);
}
private static BufferedImage rasterize(byte[] data, int size) {
try {
SVGDocument document = new SVGLoader().load(
new ByteArrayInputStream(data), null, LoaderContext.createDefault());
if (document == null) return null;
BufferedImage image = new BufferedImage(size, size, BufferedImage.TYPE_INT_ARGB);
Graphics2D g = image.createGraphics();
try {
g.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
g.setRenderingHint(RenderingHints.KEY_STROKE_CONTROL, RenderingHints.VALUE_STROKE_PURE);
document.render(null, g, new ViewBox(size, size));
} finally {
g.dispose();
}
return image;
} catch (Exception e) {
return null;
}
}
private static BufferedImage decode(byte[] data, int size) {
try {
BufferedImage source = ImageIO.read(new ByteArrayInputStream(data));
if (source == null) return null;
if (source.getWidth() == size && source.getHeight() == size) return source;
BufferedImage scaled = new BufferedImage(size, size, BufferedImage.TYPE_INT_ARGB);
Graphics2D g = scaled.createGraphics();
try {
g.setRenderingHint(RenderingHints.KEY_INTERPOLATION,
RenderingHints.VALUE_INTERPOLATION_BILINEAR);
g.setRenderingHint(RenderingHints.KEY_RENDERING, RenderingHints.VALUE_RENDER_QUALITY);
// Keep the aspect ratio of packs whose art is not square.
double factor = Math.min((double) size / source.getWidth(), (double) size / source.getHeight());
int w = Math.max(1, (int) Math.round(source.getWidth() * factor));
int h = Math.max(1, (int) Math.round(source.getHeight() * factor));
g.drawImage(source.getScaledInstance(w, h, Image.SCALE_SMOOTH),
(size - w) / 2, (size - h) / 2, null);
} finally {
g.dispose();
}
return scaled;
} catch (Exception e) {
return null;
}
}
}