Each connection now lives in its own ServerTab with its own tree, info and chat views plus its own mic-mute, deafen, away and commander state. MainFrame keeps only the shell (menus, toolbar, status bar) and shows one tab at a time; the tab bar below the toolbar appears once a second server is open. Incoming voice from every connection mixes into the same output — each connection already renders one line per speaker. Capture is exclusive: a connection only opens the microphone while it holds it, and a toolbar button moves it to the visible tab. The mic follows a disconnect to another connected tab, and push-to-talk drives whoever holds it. Startup bookmarks are no longer mutually exclusive: every flagged bookmark opens its own tab. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
210 lines
6.2 KiB
Java
210 lines
6.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;
|
|
|
|
/**
|
|
* 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.
|
|
*/
|
|
public final class Icons {
|
|
|
|
private static final int SZ = 16;
|
|
|
|
private Icons() {
|
|
}
|
|
|
|
private interface Painter {
|
|
void paint(Graphics2D g);
|
|
}
|
|
|
|
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 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);
|
|
});
|
|
}
|
|
|
|
public static ImageIcon channel() {
|
|
return channelPainted(new Color(0x3E7CB1), false);
|
|
}
|
|
|
|
public static ImageIcon channelLocked() {
|
|
return channelPainted(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);
|
|
}
|
|
});
|
|
}
|
|
|
|
// ---- client status icons ----
|
|
|
|
public static ImageIcon clientIdle() {
|
|
return person(Theme.IDLE_CLIENT);
|
|
}
|
|
|
|
public static ImageIcon clientTalking() {
|
|
return person(Theme.TALKING);
|
|
}
|
|
|
|
public static ImageIcon clientAway() {
|
|
return person(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
|
|
});
|
|
}
|
|
|
|
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
|
|
});
|
|
}
|
|
|
|
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);
|
|
});
|
|
}
|
|
|
|
// ---- 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);
|
|
});
|
|
}
|
|
|
|
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);
|
|
});
|
|
}
|
|
|
|
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);
|
|
});
|
|
}
|
|
|
|
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);
|
|
});
|
|
}
|
|
|
|
/** 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);
|
|
});
|
|
}
|
|
|
|
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);
|
|
}
|
|
});
|
|
}
|
|
|
|
public static ImageIcon app() {
|
|
return make(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);
|
|
});
|
|
}
|
|
}
|