Draw the interface with TeamSpeak icon packs
Icon packs are read in TeamSpeak's own format — a zip (or unpacked folder) of SVG/PNG art plus a settings.ini mapping icon keys to files — so the packs of an installed client and the ones from its add-on site work unchanged. Legacy packs without that mapping (default.zip) are resolved by their file naming convention instead, picking the resolution closest to the drawn size. The pack draws the tree, toolbar, menus, context menus, file browser and the default group icons; a pack's FALLBACK option decides whether what it lacks comes from default.zip, and anything still missing falls back to the icons the client draws itself. Options → Design picks the pack and shows a viewer of everything in it. Vector art is rasterised with JSVG, and the pack search roots are shared with the sound packs. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -8,13 +8,17 @@ import java.awt.RenderingHints;
|
||||
import java.awt.image.BufferedImage;
|
||||
|
||||
/**
|
||||
* Programmatically drawn vector icons (no external image assets), so the client
|
||||
* is fully self-contained. All icons are rendered at 16×16 with a small
|
||||
* cache.
|
||||
* The user interface's icons, taken from the active {@link IconTheme} pack and
|
||||
* named by TeamSpeak's icon keys.
|
||||
*
|
||||
* <p>The icons the client cannot do without are also drawn programmatically here,
|
||||
* so a machine with no icon pack installed still gets a complete (if plain) set.
|
||||
* Everything else — menu and dialog decoration — simply stays blank without a pack,
|
||||
* which is why {@link #of(String)} may return {@code null}.
|
||||
*/
|
||||
public final class Icons {
|
||||
|
||||
private static final int SZ = 16;
|
||||
private static final int SZ = IconTheme.SIZE;
|
||||
|
||||
private Icons() {
|
||||
}
|
||||
@@ -23,6 +27,25 @@ public final class Icons {
|
||||
void paint(Graphics2D g);
|
||||
}
|
||||
|
||||
/** @return the pack's icon for a TeamSpeak icon key, or {@code null} when it has none */
|
||||
public static ImageIcon of(String key) {
|
||||
return IconTheme.icon(key);
|
||||
}
|
||||
|
||||
/** @return the pack's icon at a given size, or {@code null} when it has none */
|
||||
public static ImageIcon of(String key, int size) {
|
||||
return IconTheme.icon(key, size);
|
||||
}
|
||||
|
||||
private static ImageIcon themed(String key, Painter fallback) {
|
||||
return themed(key, SZ, fallback);
|
||||
}
|
||||
|
||||
private static ImageIcon themed(String key, int size, Painter fallback) {
|
||||
ImageIcon icon = IconTheme.icon(key, size);
|
||||
return icon != null ? icon : make(fallback);
|
||||
}
|
||||
|
||||
private static ImageIcon make(Painter p) {
|
||||
BufferedImage img = new BufferedImage(SZ, SZ, BufferedImage.TYPE_INT_ARGB);
|
||||
Graphics2D g = img.createGraphics();
|
||||
@@ -36,174 +59,225 @@ public final class Icons {
|
||||
// ---- tree icons ----
|
||||
|
||||
public static ImageIcon server() {
|
||||
return make(g -> {
|
||||
g.setColor(new Color(0x2C6EA5));
|
||||
g.fillRoundRect(2, 3, 12, 4, 2, 2);
|
||||
g.fillRoundRect(2, 9, 12, 4, 2, 2);
|
||||
g.setColor(new Color(0x9FD0F0));
|
||||
g.fillOval(4, 4, 2, 2);
|
||||
g.fillOval(4, 10, 2, 2);
|
||||
});
|
||||
return themed("SERVER_GREEN", Icons::paintServer);
|
||||
}
|
||||
|
||||
public static ImageIcon channel() {
|
||||
return channelPainted(new Color(0x3E7CB1), false);
|
||||
return themed("CHANNEL_GREEN", g -> paintChannel(g, new Color(0x3E7CB1), false));
|
||||
}
|
||||
|
||||
/** A channel nobody can join without its password. */
|
||||
public static ImageIcon channelLocked() {
|
||||
return channelPainted(new Color(0x8A6D3B), true);
|
||||
return themed("CHANNEL_PRIVATE", g -> paintChannel(g, new Color(0x8A6D3B), true));
|
||||
}
|
||||
|
||||
private static ImageIcon channelPainted(Color c, boolean lock) {
|
||||
return make(g -> {
|
||||
g.setColor(c);
|
||||
g.setStroke(new BasicStroke(1.6f));
|
||||
// simple speaker-cone glyph
|
||||
g.fillRect(2, 6, 3, 4);
|
||||
int[] xs = {5, 9, 9, 5};
|
||||
int[] ys = {6, 3, 13, 10};
|
||||
g.fillPolygon(xs, ys, 4);
|
||||
g.setStroke(new BasicStroke(1.4f));
|
||||
g.drawArc(9, 5, 4, 6, -60, 120);
|
||||
if (lock) {
|
||||
g.setColor(new Color(0xB8860B));
|
||||
g.fillRoundRect(10, 9, 5, 5, 1, 1);
|
||||
g.setColor(Color.WHITE);
|
||||
g.fillRect(12, 10, 1, 2);
|
||||
}
|
||||
});
|
||||
/** A channel that has reached its client limit. */
|
||||
public static ImageIcon channelFull() {
|
||||
return themed("CHANNEL_RED", g -> paintChannel(g, new Color(0xA53F3F), false));
|
||||
}
|
||||
|
||||
// ---- client status icons ----
|
||||
|
||||
public static ImageIcon clientIdle() {
|
||||
return person(Theme.IDLE_CLIENT);
|
||||
return themed("PLAYER_OFF", g -> paintPerson(g, Theme.IDLE_CLIENT));
|
||||
}
|
||||
|
||||
public static ImageIcon clientTalking() {
|
||||
return person(Theme.TALKING);
|
||||
return themed("PLAYER_ON", g -> paintPerson(g, Theme.TALKING));
|
||||
}
|
||||
|
||||
public static ImageIcon clientAway() {
|
||||
return person(Theme.AWAY);
|
||||
return themed("AWAY", g -> paintPerson(g, Theme.AWAY));
|
||||
}
|
||||
|
||||
private static ImageIcon person(Color c) {
|
||||
return make(g -> {
|
||||
g.setColor(c);
|
||||
g.fillOval(5, 2, 6, 6); // head
|
||||
g.fillRoundRect(3, 9, 10, 6, 4, 4); // shoulders
|
||||
});
|
||||
/** A channel commander that is not talking. */
|
||||
public static ImageIcon clientCommander() {
|
||||
return themed("PLAYER_COMMANDER_OFF", g -> paintPerson(g, Theme.IDLE_CLIENT));
|
||||
}
|
||||
|
||||
/** A channel commander that is talking. */
|
||||
public static ImageIcon clientCommanderTalking() {
|
||||
return themed("PLAYER_COMMANDER_ON", g -> paintPerson(g, Theme.TALKING));
|
||||
}
|
||||
|
||||
public static ImageIcon clientQuery() {
|
||||
return themed("SERVER_QUERY", g -> paintPerson(g, Theme.IDLE_CLIENT));
|
||||
}
|
||||
|
||||
public static ImageIcon micMuted() {
|
||||
return make(g -> {
|
||||
g.setColor(Theme.MUTED);
|
||||
g.fillRoundRect(6, 2, 4, 7, 2, 2);
|
||||
g.setStroke(new BasicStroke(1.4f));
|
||||
g.drawArc(4, 6, 8, 6, 200, 140);
|
||||
g.drawLine(8, 12, 8, 14);
|
||||
g.setStroke(new BasicStroke(2f));
|
||||
g.drawLine(2, 2, 14, 14); // slash
|
||||
});
|
||||
return themed("INPUT_MUTED", Icons::paintMicMuted);
|
||||
}
|
||||
|
||||
public static ImageIcon speakerMuted() {
|
||||
return make(g -> {
|
||||
g.setColor(Theme.MUTED);
|
||||
g.fillRect(2, 6, 3, 4);
|
||||
int[] xs = {5, 9, 9, 5};
|
||||
int[] ys = {6, 3, 13, 10};
|
||||
g.fillPolygon(xs, ys, 4);
|
||||
g.setStroke(new BasicStroke(2f));
|
||||
g.drawLine(2, 2, 14, 14);
|
||||
});
|
||||
return themed("OUTPUT_MUTED", Icons::paintSpeakerMuted);
|
||||
}
|
||||
|
||||
// ---- toolbar / action icons ----
|
||||
|
||||
public static ImageIcon connect() {
|
||||
return make(g -> {
|
||||
g.setColor(new Color(0x2E8B57));
|
||||
g.setStroke(new BasicStroke(2f));
|
||||
g.drawLine(3, 8, 8, 8);
|
||||
g.fillRoundRect(8, 5, 5, 6, 2, 2);
|
||||
g.drawLine(10, 3, 10, 5);
|
||||
g.drawLine(12, 3, 12, 5);
|
||||
});
|
||||
return themed("CONNECT", IconTheme.TOOLBAR_SIZE, Icons::paintConnect);
|
||||
}
|
||||
|
||||
public static ImageIcon disconnect() {
|
||||
return make(g -> {
|
||||
g.setColor(Theme.MUTED);
|
||||
g.setStroke(new BasicStroke(2f));
|
||||
g.drawLine(3, 8, 8, 8);
|
||||
g.fillRoundRect(8, 5, 5, 6, 2, 2);
|
||||
g.drawLine(2, 3, 6, 13);
|
||||
});
|
||||
return themed("DISCONNECT", IconTheme.TOOLBAR_SIZE, Icons::paintDisconnect);
|
||||
}
|
||||
|
||||
public static ImageIcon mic() {
|
||||
return make(g -> {
|
||||
g.setColor(new Color(0x37474F));
|
||||
g.fillRoundRect(6, 2, 4, 7, 2, 2);
|
||||
g.setStroke(new BasicStroke(1.4f));
|
||||
g.drawArc(4, 6, 8, 6, 200, 140);
|
||||
g.drawLine(8, 12, 8, 14);
|
||||
g.drawLine(6, 14, 10, 14);
|
||||
});
|
||||
return themed("CAPTURE", IconTheme.TOOLBAR_SIZE, Icons::paintMic);
|
||||
}
|
||||
|
||||
public static ImageIcon speaker() {
|
||||
return make(g -> {
|
||||
g.setColor(new Color(0x37474F));
|
||||
g.fillRect(2, 6, 3, 4);
|
||||
int[] xs = {5, 9, 9, 5};
|
||||
int[] ys = {6, 3, 13, 10};
|
||||
g.fillPolygon(xs, ys, 4);
|
||||
g.setStroke(new BasicStroke(1.4f));
|
||||
g.drawArc(9, 5, 4, 6, -60, 120);
|
||||
});
|
||||
return themed("PLAYBACK", IconTheme.TOOLBAR_SIZE, Icons::paintSpeaker);
|
||||
}
|
||||
|
||||
public static ImageIcon micMutedLarge() {
|
||||
return themed("INPUT_MUTED", IconTheme.TOOLBAR_SIZE, Icons::paintMicMuted);
|
||||
}
|
||||
|
||||
public static ImageIcon speakerMutedLarge() {
|
||||
return themed("OUTPUT_MUTED", IconTheme.TOOLBAR_SIZE, Icons::paintSpeakerMuted);
|
||||
}
|
||||
|
||||
/** Marks the server that currently owns the microphone. */
|
||||
public static ImageIcon micActive() {
|
||||
return make(g -> {
|
||||
g.setColor(Theme.TALKING);
|
||||
g.fillOval(1, 1, 14, 14);
|
||||
g.setColor(Color.WHITE);
|
||||
g.fillRoundRect(6, 3, 4, 6, 2, 2);
|
||||
g.setStroke(new BasicStroke(1.4f));
|
||||
g.drawArc(4, 6, 8, 6, 200, 140);
|
||||
g.drawLine(8, 11, 8, 13);
|
||||
});
|
||||
return themed("ACTIVATE_MICROPHONE", IconTheme.TOOLBAR_SIZE, Icons::paintMicActive);
|
||||
}
|
||||
|
||||
/** The same marker at tab-label size. */
|
||||
public static ImageIcon micActiveSmall() {
|
||||
return themed("ACTIVATE_MICROPHONE", Icons::paintMicActive);
|
||||
}
|
||||
|
||||
public static ImageIcon settings() {
|
||||
return make(g -> {
|
||||
g.setColor(new Color(0x37474F));
|
||||
g.setStroke(new BasicStroke(2f));
|
||||
g.drawOval(5, 5, 6, 6);
|
||||
for (int a = 0; a < 360; a += 45) {
|
||||
double r = Math.toRadians(a);
|
||||
int x1 = (int) (8 + Math.cos(r) * 5);
|
||||
int y1 = (int) (8 + Math.sin(r) * 5);
|
||||
int x2 = (int) (8 + Math.cos(r) * 7);
|
||||
int y2 = (int) (8 + Math.sin(r) * 7);
|
||||
g.drawLine(x1, y1, x2, y2);
|
||||
}
|
||||
});
|
||||
return themed("SETTINGS", IconTheme.TOOLBAR_SIZE, Icons::paintSettings);
|
||||
}
|
||||
|
||||
public static ImageIcon app() {
|
||||
return make(g -> {
|
||||
g.setColor(Theme.ACCENT);
|
||||
g.fillRoundRect(1, 1, 14, 14, 4, 4);
|
||||
return make(Icons::paintApp);
|
||||
}
|
||||
|
||||
// ---- built-in painters ----
|
||||
|
||||
private static void paintServer(Graphics2D g) {
|
||||
g.setColor(new Color(0x2C6EA5));
|
||||
g.fillRoundRect(2, 3, 12, 4, 2, 2);
|
||||
g.fillRoundRect(2, 9, 12, 4, 2, 2);
|
||||
g.setColor(new Color(0x9FD0F0));
|
||||
g.fillOval(4, 4, 2, 2);
|
||||
g.fillOval(4, 10, 2, 2);
|
||||
}
|
||||
|
||||
private static void paintChannel(Graphics2D g, Color c, boolean lock) {
|
||||
g.setColor(c);
|
||||
g.setStroke(new BasicStroke(1.6f));
|
||||
// simple speaker-cone glyph
|
||||
g.fillRect(2, 6, 3, 4);
|
||||
int[] xs = {5, 9, 9, 5};
|
||||
int[] ys = {6, 3, 13, 10};
|
||||
g.fillPolygon(xs, ys, 4);
|
||||
g.setStroke(new BasicStroke(1.4f));
|
||||
g.drawArc(9, 5, 4, 6, -60, 120);
|
||||
if (lock) {
|
||||
g.setColor(new Color(0xB8860B));
|
||||
g.fillRoundRect(10, 9, 5, 5, 1, 1);
|
||||
g.setColor(Color.WHITE);
|
||||
g.setStroke(new BasicStroke(1.6f));
|
||||
g.drawArc(4, 5, 8, 8, 30, 120);
|
||||
g.drawArc(2, 3, 12, 12, 30, 120);
|
||||
g.fillOval(7, 9, 2, 2);
|
||||
});
|
||||
g.fillRect(12, 10, 1, 2);
|
||||
}
|
||||
}
|
||||
|
||||
private static void paintPerson(Graphics2D g, Color c) {
|
||||
g.setColor(c);
|
||||
g.fillOval(5, 2, 6, 6); // head
|
||||
g.fillRoundRect(3, 9, 10, 6, 4, 4); // shoulders
|
||||
}
|
||||
|
||||
private static void paintMicMuted(Graphics2D g) {
|
||||
g.setColor(Theme.MUTED);
|
||||
g.fillRoundRect(6, 2, 4, 7, 2, 2);
|
||||
g.setStroke(new BasicStroke(1.4f));
|
||||
g.drawArc(4, 6, 8, 6, 200, 140);
|
||||
g.drawLine(8, 12, 8, 14);
|
||||
g.setStroke(new BasicStroke(2f));
|
||||
g.drawLine(2, 2, 14, 14); // slash
|
||||
}
|
||||
|
||||
private static void paintSpeakerMuted(Graphics2D g) {
|
||||
g.setColor(Theme.MUTED);
|
||||
g.fillRect(2, 6, 3, 4);
|
||||
int[] xs = {5, 9, 9, 5};
|
||||
int[] ys = {6, 3, 13, 10};
|
||||
g.fillPolygon(xs, ys, 4);
|
||||
g.setStroke(new BasicStroke(2f));
|
||||
g.drawLine(2, 2, 14, 14);
|
||||
}
|
||||
|
||||
private static void paintConnect(Graphics2D g) {
|
||||
g.setColor(new Color(0x2E8B57));
|
||||
g.setStroke(new BasicStroke(2f));
|
||||
g.drawLine(3, 8, 8, 8);
|
||||
g.fillRoundRect(8, 5, 5, 6, 2, 2);
|
||||
g.drawLine(10, 3, 10, 5);
|
||||
g.drawLine(12, 3, 12, 5);
|
||||
}
|
||||
|
||||
private static void paintDisconnect(Graphics2D g) {
|
||||
g.setColor(Theme.MUTED);
|
||||
g.setStroke(new BasicStroke(2f));
|
||||
g.drawLine(3, 8, 8, 8);
|
||||
g.fillRoundRect(8, 5, 5, 6, 2, 2);
|
||||
g.drawLine(2, 3, 6, 13);
|
||||
}
|
||||
|
||||
private static void paintMic(Graphics2D g) {
|
||||
g.setColor(new Color(0x37474F));
|
||||
g.fillRoundRect(6, 2, 4, 7, 2, 2);
|
||||
g.setStroke(new BasicStroke(1.4f));
|
||||
g.drawArc(4, 6, 8, 6, 200, 140);
|
||||
g.drawLine(8, 12, 8, 14);
|
||||
g.drawLine(6, 14, 10, 14);
|
||||
}
|
||||
|
||||
private static void paintSpeaker(Graphics2D g) {
|
||||
g.setColor(new Color(0x37474F));
|
||||
g.fillRect(2, 6, 3, 4);
|
||||
int[] xs = {5, 9, 9, 5};
|
||||
int[] ys = {6, 3, 13, 10};
|
||||
g.fillPolygon(xs, ys, 4);
|
||||
g.setStroke(new BasicStroke(1.4f));
|
||||
g.drawArc(9, 5, 4, 6, -60, 120);
|
||||
}
|
||||
|
||||
private static void paintMicActive(Graphics2D g) {
|
||||
g.setColor(Theme.TALKING);
|
||||
g.fillOval(1, 1, 14, 14);
|
||||
g.setColor(Color.WHITE);
|
||||
g.fillRoundRect(6, 3, 4, 6, 2, 2);
|
||||
g.setStroke(new BasicStroke(1.4f));
|
||||
g.drawArc(4, 6, 8, 6, 200, 140);
|
||||
g.drawLine(8, 11, 8, 13);
|
||||
}
|
||||
|
||||
private static void paintSettings(Graphics2D g) {
|
||||
g.setColor(new Color(0x37474F));
|
||||
g.setStroke(new BasicStroke(2f));
|
||||
g.drawOval(5, 5, 6, 6);
|
||||
for (int a = 0; a < 360; a += 45) {
|
||||
double r = Math.toRadians(a);
|
||||
int x1 = (int) (8 + Math.cos(r) * 5);
|
||||
int y1 = (int) (8 + Math.sin(r) * 5);
|
||||
int x2 = (int) (8 + Math.cos(r) * 7);
|
||||
int y2 = (int) (8 + Math.sin(r) * 7);
|
||||
g.drawLine(x1, y1, x2, y2);
|
||||
}
|
||||
}
|
||||
|
||||
private static void paintApp(Graphics2D g) {
|
||||
g.setColor(Theme.ACCENT);
|
||||
g.fillRoundRect(1, 1, 14, 14, 4, 4);
|
||||
g.setColor(Color.WHITE);
|
||||
g.setStroke(new BasicStroke(1.6f));
|
||||
g.drawArc(4, 5, 8, 8, 30, 120);
|
||||
g.drawArc(2, 3, 12, 12, 30, 120);
|
||||
g.fillOval(7, 9, 2, 2);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user