The toolbar gains an away toggle with a drop-down: it toggles away on the
current server with no message, while the menu carries the global actions
("Set Globally Away", "Set Globally Away Status"), the saved presets and
their editor. Presets live in ~/.ts3jclient/away.properties and are
managed in a list with add/remove and double-click renaming.
Clients in the channel tree now show their away message in brackets after
the nickname instead of their primary server group.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
289 lines
9.2 KiB
Java
289 lines
9.2 KiB
Java
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.
|
|
*
|
|
* <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 = 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 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();
|
|
g.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
|
|
g.setRenderingHint(RenderingHints.KEY_STROKE_CONTROL, RenderingHints.VALUE_STROKE_PURE);
|
|
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() {
|
|
return themed("CHANNEL_GREEN", g -> paintChannel(g, new Color(0x3E7CB1), false));
|
|
}
|
|
|
|
/** A channel nobody can join without its password. */
|
|
public static ImageIcon channelLocked() {
|
|
return themed("CHANNEL_PRIVATE", g -> paintChannel(g, new Color(0x8A6D3B), true));
|
|
}
|
|
|
|
/** 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 themed("PLAYER_OFF", g -> paintPerson(g, Theme.IDLE_CLIENT));
|
|
}
|
|
|
|
public static ImageIcon clientTalking() {
|
|
return themed("PLAYER_ON", g -> paintPerson(g, Theme.TALKING));
|
|
}
|
|
|
|
public static ImageIcon clientAway() {
|
|
return themed("AWAY", g -> paintPerson(g, Theme.AWAY));
|
|
}
|
|
|
|
/** 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 themed("INPUT_MUTED", Icons::paintMicMuted);
|
|
}
|
|
|
|
public static ImageIcon speakerMuted() {
|
|
return themed("OUTPUT_MUTED", Icons::paintSpeakerMuted);
|
|
}
|
|
|
|
// ---- 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);
|
|
}
|
|
|
|
/** 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 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.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);
|
|
}
|
|
}
|