package com.ts3client.ui; import javax.swing.ImageIcon; import java.awt.BasicStroke; import java.awt.Color; import java.awt.Graphics2D; import java.awt.RenderingHints; import java.awt.image.BufferedImage; /** * The user interface's icons, taken from the active {@link IconTheme} pack and * named by TeamSpeak's icon keys. * *

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 = IconTheme.SIZE; private Icons() { } private interface Painter { 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 for the first key it has, or {@code null} when it has none * of them — packs differ in which variants they ship */ public static ImageIcon ofAny(String... keys) { for (String key : keys) { ImageIcon icon = IconTheme.icon(key); if (icon != null) return icon; } return null; } /** @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(size, fallback); } /** * Like {@link #themed(String, Painter)}, but tries the keys in turn — packs need not * ship every variant (none of TeamSpeak's own has a subscribed password channel). */ private static ImageIcon themed(String[] keys, Painter fallback) { for (String key : keys) { ImageIcon icon = IconTheme.icon(key, SZ); if (icon != null) return icon; } return make(fallback); } private static ImageIcon make(Painter p) { return make(SZ, p); } /** Runs a painter (which draws in a 16×16 box) scaled to {@code size}. */ private static ImageIcon make(int size, Painter p) { BufferedImage img = new BufferedImage(size, size, BufferedImage.TYPE_INT_ARGB); Graphics2D g = img.createGraphics(); g.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON); g.setRenderingHint(RenderingHints.KEY_STROKE_CONTROL, RenderingHints.VALUE_STROKE_PURE); g.scale(size / (double) SZ, size / (double) SZ); p.paint(g); g.dispose(); return new ImageIcon(img); } // ---- tree icons ---- public static ImageIcon server() { return themed("SERVER_GREEN", Icons::paintServer); } public static ImageIcon channel(boolean subscribed) { return themed(keys("CHANNEL_GREEN", subscribed), g -> paintChannel(g, new Color(0x3E7CB1), false, subscribed)); } /** A channel nobody can join without its password. */ public static ImageIcon channelLocked(boolean subscribed) { return themed(keys("CHANNEL_PRIVATE", subscribed), g -> paintChannel(g, new Color(0x8A6D3B), true, subscribed)); } /** A channel that has reached its client limit. */ public static ImageIcon channelFull(boolean subscribed) { return themed(keys("CHANNEL_RED", subscribed), g -> paintChannel(g, new Color(0xA53F3F), false, subscribed)); } /** The pack keys to try for a channel icon, most specific first. */ private static String[] keys(String base, boolean subscribed) { return subscribed ? new String[]{base + "_SUBSCRIBED", base} : new String[]{base}; } // ---- client status icons ---- public static ImageIcon clientIdle() { return clientIdle(SZ); } public static ImageIcon clientIdle(int size) { return themed("PLAYER_OFF", size, g -> paintPerson(g, Theme.idleClient())); } public static ImageIcon clientTalking() { return clientTalking(SZ); } public static ImageIcon clientTalking(int size) { return themed("PLAYER_ON", size, g -> paintPerson(g, Theme.talking())); } public static ImageIcon clientAway() { return clientAway(SZ); } public static ImageIcon clientAway(int size) { return themed("AWAY", size, g -> paintPerson(g, Theme.away())); } /** A channel commander that is not talking. */ public static ImageIcon clientCommander() { return clientCommander(SZ); } /** A channel commander that is talking. */ public static ImageIcon clientCommanderTalking() { return clientCommanderTalking(SZ); } public static ImageIcon clientQuery() { return themed("SERVER_QUERY", g -> paintPerson(g, Theme.idleClient())); } public static ImageIcon micMuted() { return micMuted(SZ); } public static ImageIcon micMuted(int size) { return themed("INPUT_MUTED", size, Icons::paintMicMuted); } public static ImageIcon speakerMuted() { return speakerMuted(SZ); } public static ImageIcon speakerMuted(int size) { return themed("OUTPUT_MUTED", size, Icons::paintSpeakerMuted); } /** The capture device is unavailable (another tab holds it), as opposed to muted. */ public static ImageIcon micDisabled() { return micDisabled(SZ); } public static ImageIcon micDisabled(int size) { return themed("HARDWARE_INPUT_MUTED", size, Icons::paintMicDisabled); } /** The playback device is unavailable, as opposed to muted/deafened. */ public static ImageIcon speakerDisabled() { return speakerDisabled(SZ); } public static ImageIcon speakerDisabled(int size) { return themed("HARDWARE_OUTPUT_MUTED", size, Icons::paintSpeakerDisabled); } /** TS3's "Local Mic Mute": silenced, but not reported as muted. */ public static ImageIcon micLocalMuted() { return micLocalMuted(SZ); } public static ImageIcon micLocalMuted(int size) { return themed("INPUT_MUTED_LOCAL", size, Icons::paintMicLocalMuted); } /** A channel commander that is not talking. */ public static ImageIcon clientCommander(int size) { return themed("PLAYER_COMMANDER_OFF", size, g -> paintPerson(g, Theme.idleClient())); } /** A channel commander that is talking. */ public static ImageIcon clientCommanderTalking(int size) { return themed("PLAYER_COMMANDER_ON", size, g -> paintPerson(g, Theme.talking())); } // ---- toolbar / action icons ---- public static ImageIcon connect() { return themed("CONNECT", IconTheme.TOOLBAR_SIZE, Icons::paintConnect); } public static ImageIcon disconnect() { return themed("DISCONNECT", IconTheme.TOOLBAR_SIZE, Icons::paintDisconnect); } public static ImageIcon mic() { return themed("CAPTURE", IconTheme.TOOLBAR_SIZE, Icons::paintMic); } public static ImageIcon speaker() { 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 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); } /** * A fixed-width "channel icon / edit icon" label icon, used where a tab must not * change width as the object it shows changes (e.g. the info-panel-in-chat tab). */ public static ImageIcon channelClientPair() { ImageIcon channel = channel(true); ImageIcon edit = themed("EDIT", Icons::paintEdit); int gap = 8; int width = channel.getIconWidth() + gap + edit.getIconWidth(); int height = Math.max(channel.getIconHeight(), edit.getIconHeight()); BufferedImage img = new BufferedImage(width, height, BufferedImage.TYPE_INT_ARGB); Graphics2D g = img.createGraphics(); g.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON); g.drawImage(channel.getImage(), 0, (height - channel.getIconHeight()) / 2, null); g.setColor(Theme.idleClient()); g.setFont(Theme.uiFont()); java.awt.FontMetrics fm = g.getFontMetrics(); int slashX = channel.getIconWidth() + (gap - fm.stringWidth("/")) / 2; g.drawString("/", slashX, (height + fm.getAscent()) / 2 - 1); g.drawImage(edit.getImage(), channel.getIconWidth() + gap, (height - edit.getIconHeight()) / 2, null); g.dispose(); return new ImageIcon(img); } /** The toolbar's away marker. */ public static ImageIcon away() { return themed("AWAY", IconTheme.TOOLBAR_SIZE, g -> paintPerson(g, Theme.away())); } public static ImageIcon settings() { return themed("SETTINGS", IconTheme.TOOLBAR_SIZE, Icons::paintSettings); } public static ImageIcon app() { return app(SZ); } public static ImageIcon app(int size) { return make(size, 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); } /** * The speaker-cone glyph. An unsubscribed channel is drawn hollow and without the * sound waves, so the two states stay apart even without an icon pack. */ private static void paintChannel(Graphics2D g, Color c, boolean lock, boolean subscribed) { int[] xs = {5, 9, 9, 5}; int[] ys = {6, 3, 13, 10}; g.setColor(c); g.setStroke(new BasicStroke(subscribed ? 1.6f : 1.2f)); if (subscribed) { g.fillRect(2, 6, 3, 4); g.fillPolygon(xs, ys, 4); g.setStroke(new BasicStroke(1.4f)); g.drawArc(9, 5, 4, 6, -60, 120); } else { g.drawRect(2, 6, 3, 4); g.drawPolygon(xs, ys, 4); } 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); } } 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); } /** Same shape as {@link #paintMicMuted}, but grey: unavailable, not muted by choice. */ private static void paintMicDisabled(Graphics2D g) { g.setColor(Theme.idleClient()); 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); } /** Same shape as {@link #paintSpeakerMuted}, but grey: unavailable, not muted by choice. */ private static void paintSpeakerDisabled(Graphics2D g) { g.setColor(Theme.idleClient()); 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); } /** {@link #paintMicMuted}, marked with a small dot: silenced, but not reported as muted. */ private static void paintMicLocalMuted(Graphics2D g) { paintMicMuted(g); g.setColor(Theme.accent()); g.fillOval(11, 10, 4, 4); } 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); } /** A pencil glyph, drawn when no icon pack ships an "EDIT" icon. */ private static void paintEdit(Graphics2D g) { g.setColor(new Color(0x37474F)); g.setStroke(new BasicStroke(1.4f)); g.drawLine(3, 13, 11, 5); g.drawLine(3, 13, 4, 10); g.drawLine(4, 10, 11, 3); g.drawLine(11, 3, 13, 5); g.drawLine(13, 5, 11, 7); g.drawLine(11, 5, 13, 7); } 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); } }