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:
@@ -1,6 +1,7 @@
|
||||
package com.ts3client;
|
||||
|
||||
import com.ts3client.config.Settings;
|
||||
import com.ts3client.ui.IconTheme;
|
||||
import com.ts3client.ui.MainFrame;
|
||||
|
||||
import javax.swing.SwingUtilities;
|
||||
@@ -27,6 +28,7 @@ public final class Main {
|
||||
} catch (Exception ignored) {
|
||||
// fall back to cross-platform L&F
|
||||
}
|
||||
IconTheme.get().reload(settings);
|
||||
new MainFrame(settings).setVisible(true);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -16,11 +16,11 @@ final class ChannelMenu {
|
||||
|
||||
static JPopupMenu build(ChannelNode channel, ServerTreePanel.Actions actions) {
|
||||
JPopupMenu menu = new JPopupMenu();
|
||||
JMenuItem join = new JMenuItem("Join channel");
|
||||
JMenuItem join = new JMenuItem("Join channel", Icons.of("CHANNEL_SWITCH"));
|
||||
join.addActionListener(a -> actions.joinChannel(channel.id));
|
||||
menu.add(join);
|
||||
menu.addSeparator();
|
||||
JMenuItem files = new JMenuItem("Browse files");
|
||||
JMenuItem files = new JMenuItem("Browse files", Icons.of("FILETRANSFER"));
|
||||
files.addActionListener(a -> actions.browseFiles(channel));
|
||||
menu.add(files);
|
||||
return menu;
|
||||
|
||||
@@ -17,24 +17,25 @@ final class ClientMenu {
|
||||
static JPopupMenu build(ClientEntry client, boolean self, ServerTreePanel.Actions actions) {
|
||||
JPopupMenu menu = new JPopupMenu();
|
||||
if (!self) {
|
||||
JMenuItem pm = new JMenuItem("Open text chat");
|
||||
JMenuItem pm = new JMenuItem("Open text chat", Icons.of("PLAYER_CHAT"));
|
||||
pm.addActionListener(a -> actions.openPrivateChat(client));
|
||||
menu.add(pm);
|
||||
JMenuItem poke = new JMenuItem("Poke");
|
||||
JMenuItem poke = new JMenuItem("Poke", Icons.of("POKE"));
|
||||
poke.addActionListener(a -> actions.pokeClient(client));
|
||||
menu.add(poke);
|
||||
menu.addSeparator();
|
||||
boolean muted = actions.isClientLocallyMuted(client.id);
|
||||
JMenuItem mute = new JMenuItem(muted ? "Unmute client" : "Mute client");
|
||||
JMenuItem mute = new JMenuItem(muted ? "Unmute client" : "Mute client",
|
||||
Icons.of(muted ? "PLAYER_ON" : "INPUT_MUTED"));
|
||||
mute.addActionListener(a -> actions.toggleClientMute(client));
|
||||
menu.add(mute);
|
||||
} else {
|
||||
JMenuItem me = new JMenuItem("This is you");
|
||||
JMenuItem me = new JMenuItem("This is you", Icons.of("PLAYER_OFF"));
|
||||
me.setEnabled(false);
|
||||
menu.add(me);
|
||||
}
|
||||
menu.addSeparator();
|
||||
JMenuItem info = new JMenuItem("Connection Info");
|
||||
JMenuItem info = new JMenuItem("Connection Info", Icons.of("INFO"));
|
||||
info.addActionListener(a -> actions.showConnectionInfo(client));
|
||||
menu.add(info);
|
||||
return menu;
|
||||
|
||||
@@ -52,9 +52,9 @@ public final class FileBrowserDialog extends JDialog {
|
||||
private final FileTableModel tableModel = new FileTableModel();
|
||||
private final JTable table = new JTable(tableModel);
|
||||
private final JLabel pathLabel = new JLabel("/");
|
||||
private final JButton upButton = new JButton("Up");
|
||||
private final JButton downloadButton = new JButton("Download");
|
||||
private final JButton deleteButton = new JButton("Delete");
|
||||
private final JButton upButton = new JButton("Up", Icons.of("FILE_UP"));
|
||||
private final JButton downloadButton = new JButton("Download", Icons.of("DOWNLOAD"));
|
||||
private final JButton deleteButton = new JButton("Delete", Icons.of("DELETE"));
|
||||
private final JPanel transfersPanel = new JPanel();
|
||||
|
||||
private volatile String currentPath = "/";
|
||||
@@ -88,11 +88,11 @@ public final class FileBrowserDialog extends JDialog {
|
||||
bar.setBackground(Theme.WINDOW_BG);
|
||||
|
||||
upButton.addActionListener(e -> navigateUp());
|
||||
JButton refresh = new JButton("Refresh");
|
||||
JButton refresh = new JButton("Refresh", Icons.of("FILE_REFRESH"));
|
||||
refresh.addActionListener(e -> refresh());
|
||||
JButton mkdir = new JButton("New folder");
|
||||
JButton mkdir = new JButton("New folder", Icons.of("ADD_FOLDER"));
|
||||
mkdir.addActionListener(e -> createDirectory());
|
||||
JButton upload = new JButton("Upload…");
|
||||
JButton upload = new JButton("Upload…", Icons.of("UPLOAD"));
|
||||
upload.addActionListener(e -> chooseUpload());
|
||||
downloadButton.addActionListener(e -> downloadSelected());
|
||||
deleteButton.addActionListener(e -> deleteSelected());
|
||||
@@ -122,6 +122,7 @@ public final class FileBrowserDialog extends JDialog {
|
||||
table.setSelectionMode(ListSelectionModel.MULTIPLE_INTERVAL_SELECTION);
|
||||
table.setFillsViewportHeight(true);
|
||||
table.getColumnModel().getColumn(0).setPreferredWidth(300);
|
||||
table.getColumnModel().getColumn(0).setCellRenderer(new NameRenderer());
|
||||
table.getColumnModel().getColumn(1).setPreferredWidth(90);
|
||||
table.getColumnModel().getColumn(2).setPreferredWidth(70);
|
||||
table.getColumnModel().getColumn(3).setPreferredWidth(150);
|
||||
@@ -437,6 +438,18 @@ public final class FileBrowserDialog extends JDialog {
|
||||
}
|
||||
}
|
||||
|
||||
/** Marks folders and files with the icon pack's own glyphs. */
|
||||
private final class NameRenderer extends javax.swing.table.DefaultTableCellRenderer {
|
||||
@Override
|
||||
public java.awt.Component getTableCellRendererComponent(JTable t, Object value, boolean selected,
|
||||
boolean focus, int row, int column) {
|
||||
super.getTableCellRendererComponent(t, value, selected, focus, row, column);
|
||||
boolean directory = row >= 0 && row < tableModel.getRowCount() && tableModel.get(row).isDirectory();
|
||||
setIcon(Icons.of(directory ? "FOLDER" : "DEFAULT"));
|
||||
return this;
|
||||
}
|
||||
}
|
||||
|
||||
// ---- formatting ----
|
||||
|
||||
private static String formatBytes(long bytes) {
|
||||
|
||||
@@ -37,6 +37,15 @@ public final class GroupIcons {
|
||||
}
|
||||
|
||||
public ImageIcon icon(long iconId) {
|
||||
if (iconId == 0) return null;
|
||||
// TeamSpeak's default group icons belong to the icon pack, which draws them in
|
||||
// its own style; only server-uploaded icons come off the wire. The theme keeps
|
||||
// its own cache, so those are not cached again here.
|
||||
if (iconId > 0 && iconId <= IconRepository.MAX_BUNDLED_ID) {
|
||||
ImageIcon themed = IconTheme.icon("GROUP_" + iconId, SIZE);
|
||||
if (themed != null) return themed;
|
||||
}
|
||||
|
||||
ImageIcon cached = decoded.get(iconId);
|
||||
if (cached != null) return cached;
|
||||
|
||||
|
||||
@@ -0,0 +1,198 @@
|
||||
package com.ts3client.ui;
|
||||
|
||||
import com.ts3client.config.Settings;
|
||||
import com.ts3client.gfx.IconPack;
|
||||
|
||||
import javax.swing.BorderFactory;
|
||||
import javax.swing.DefaultListCellRenderer;
|
||||
import javax.swing.DefaultListModel;
|
||||
import javax.swing.ImageIcon;
|
||||
import javax.swing.JButton;
|
||||
import javax.swing.JComboBox;
|
||||
import javax.swing.JComponent;
|
||||
import javax.swing.JFileChooser;
|
||||
import javax.swing.JLabel;
|
||||
import javax.swing.JList;
|
||||
import javax.swing.JPanel;
|
||||
import javax.swing.JScrollPane;
|
||||
import javax.swing.JTextField;
|
||||
import javax.swing.SwingConstants;
|
||||
import java.awt.BorderLayout;
|
||||
import java.awt.Component;
|
||||
import java.awt.Dimension;
|
||||
import java.awt.GridBagConstraints;
|
||||
import java.awt.GridBagLayout;
|
||||
import java.awt.Insets;
|
||||
import java.io.File;
|
||||
|
||||
/**
|
||||
* Options page for icon packs: which pack the user interface is drawn with, where
|
||||
* to look for more of them, and a viewer showing everything the pack contains.
|
||||
*
|
||||
* <p>Picking a pack applies it right away so the change can be seen in the window
|
||||
* behind the dialog; cancelling puts the previous one back.
|
||||
*/
|
||||
final class IconPackPanel extends JPanel {
|
||||
|
||||
/** Size of the tiles in the icon viewer. */
|
||||
private static final int PREVIEW_SIZE = 32;
|
||||
private static final int TILE = 44;
|
||||
|
||||
/** Stands for "no pack": the icons the client draws itself. */
|
||||
private static final String BUILT_IN = "Built-in icons";
|
||||
|
||||
private final Settings settings;
|
||||
private final String originalPackId;
|
||||
private final String originalPackDir;
|
||||
|
||||
private final JComboBox<Object> packCombo = new JComboBox<>();
|
||||
private final JLabel packInfo = new JLabel();
|
||||
private final JTextField packDirField;
|
||||
private final DefaultListModel<String> previewModel = new DefaultListModel<>();
|
||||
private final JList<String> preview = new JList<>(previewModel);
|
||||
|
||||
IconPackPanel(Settings settings) {
|
||||
super(new BorderLayout(0, 8));
|
||||
this.settings = settings;
|
||||
this.originalPackId = settings.iconPack;
|
||||
this.originalPackDir = settings.iconPackDir;
|
||||
this.packDirField = new JTextField(settings.iconPackDir, 18);
|
||||
|
||||
setBorder(BorderFactory.createEmptyBorder(12, 12, 12, 12));
|
||||
add(buildHeader(), BorderLayout.NORTH);
|
||||
add(buildPreview(), BorderLayout.CENTER);
|
||||
|
||||
reloadPacks();
|
||||
}
|
||||
|
||||
/** Copies the chosen pack into the settings; the caller saves them. */
|
||||
void apply() {
|
||||
settings.iconPackDir = packDirField.getText().trim();
|
||||
Object selected = packCombo.getSelectedItem();
|
||||
settings.iconPack = selected instanceof IconPack pack ? pack.id() : "";
|
||||
}
|
||||
|
||||
/** Puts back the pack the dialog started with, for a cancelled edit. */
|
||||
void revert() {
|
||||
settings.iconPack = originalPackId;
|
||||
settings.iconPackDir = originalPackDir;
|
||||
IconTheme.get().reload(settings);
|
||||
}
|
||||
|
||||
// ---- layout ----
|
||||
|
||||
private JComponent buildHeader() {
|
||||
JPanel p = new JPanel(new GridBagLayout());
|
||||
GridBagConstraints c = new GridBagConstraints();
|
||||
c.insets = new Insets(2, 2, 2, 2);
|
||||
c.anchor = GridBagConstraints.WEST;
|
||||
c.fill = GridBagConstraints.HORIZONTAL;
|
||||
|
||||
packCombo.setToolTipText("Icon packs installed here or in a TeamSpeak 3 client");
|
||||
packCombo.addActionListener(e -> onPackSelected());
|
||||
packCombo.setRenderer(new DefaultListCellRenderer() {
|
||||
@Override
|
||||
public Component getListCellRendererComponent(JList<?> list, Object value, int index,
|
||||
boolean selected, boolean focus) {
|
||||
super.getListCellRendererComponent(list, value, index, selected, focus);
|
||||
setText(value instanceof IconPack pack ? pack.name() : String.valueOf(value));
|
||||
return this;
|
||||
}
|
||||
});
|
||||
|
||||
int row = 0;
|
||||
addRow(p, c, row++, new JLabel("Icon pack:"), packCombo);
|
||||
c.gridx = 1;
|
||||
c.gridy = row++;
|
||||
packInfo.setEnabled(false);
|
||||
p.add(packInfo, c);
|
||||
|
||||
JPanel dirRow = new JPanel(new BorderLayout(6, 0));
|
||||
JButton browse = new JButton("Browse…");
|
||||
browse.addActionListener(e -> browseForPackFolder());
|
||||
dirRow.add(packDirField, BorderLayout.CENTER);
|
||||
dirRow.add(browse, BorderLayout.EAST);
|
||||
packDirField.setToolTipText("Extra folder to look for icon packs in");
|
||||
addRow(p, c, row, new JLabel("Extra pack folder:"), dirRow);
|
||||
return p;
|
||||
}
|
||||
|
||||
private JComponent buildPreview() {
|
||||
preview.setLayoutOrientation(JList.HORIZONTAL_WRAP);
|
||||
preview.setVisibleRowCount(-1);
|
||||
preview.setFixedCellWidth(TILE);
|
||||
preview.setFixedCellHeight(TILE);
|
||||
preview.setCellRenderer(new DefaultListCellRenderer() {
|
||||
@Override
|
||||
public Component getListCellRendererComponent(JList<?> list, Object value, int index,
|
||||
boolean selected, boolean focus) {
|
||||
super.getListCellRendererComponent(list, value, index, selected, focus);
|
||||
String key = String.valueOf(value);
|
||||
ImageIcon icon = Icons.of(key, PREVIEW_SIZE);
|
||||
setText("");
|
||||
setIcon(icon);
|
||||
setToolTipText(key);
|
||||
setHorizontalAlignment(SwingConstants.CENTER);
|
||||
return this;
|
||||
}
|
||||
});
|
||||
|
||||
JScrollPane scroll = new JScrollPane(preview);
|
||||
scroll.setPreferredSize(new Dimension(360, 200));
|
||||
scroll.setBorder(BorderFactory.createTitledBorder("Icons in this pack"));
|
||||
return scroll;
|
||||
}
|
||||
|
||||
// ---- behaviour ----
|
||||
|
||||
private void reloadPacks() {
|
||||
IconTheme theme = IconTheme.get();
|
||||
theme.reload(settings);
|
||||
// Filling the combo box changes its selection, and with it the active pack,
|
||||
// so remember the one the settings asked for before touching it.
|
||||
IconPack selected = theme.activePack();
|
||||
|
||||
packCombo.removeAllItems();
|
||||
packCombo.addItem(BUILT_IN);
|
||||
for (IconPack pack : theme.packs()) packCombo.addItem(pack);
|
||||
packCombo.setSelectedItem(selected == null ? BUILT_IN : selected);
|
||||
onPackSelected();
|
||||
}
|
||||
|
||||
private void onPackSelected() {
|
||||
Object selected = packCombo.getSelectedItem();
|
||||
IconPack pack = selected instanceof IconPack p ? p : null;
|
||||
IconTheme.get().setPack(pack);
|
||||
|
||||
previewModel.clear();
|
||||
if (pack == null) {
|
||||
packInfo.setText("Drawn by the client itself");
|
||||
} else {
|
||||
for (String key : pack.keys()) previewModel.addElement(key);
|
||||
packInfo.setText(pack.keys().size() + " icons"
|
||||
+ (pack.fallsBack() ? ", missing ones from the default pack" : ""));
|
||||
}
|
||||
}
|
||||
|
||||
private void browseForPackFolder() {
|
||||
JFileChooser chooser = new JFileChooser();
|
||||
chooser.setFileSelectionMode(JFileChooser.DIRECTORIES_ONLY);
|
||||
chooser.setDialogTitle("Folder containing icon packs");
|
||||
String current = packDirField.getText().trim();
|
||||
if (!current.isEmpty()) chooser.setCurrentDirectory(new File(current));
|
||||
if (chooser.showOpenDialog(this) != JFileChooser.APPROVE_OPTION) return;
|
||||
packDirField.setText(chooser.getSelectedFile().getAbsolutePath());
|
||||
settings.iconPackDir = packDirField.getText();
|
||||
reloadPacks();
|
||||
}
|
||||
|
||||
private static void addRow(JPanel p, GridBagConstraints c, int row, JLabel label, Component field) {
|
||||
c.gridx = 0;
|
||||
c.gridy = row;
|
||||
c.weightx = 0;
|
||||
p.add(label, c);
|
||||
c.gridx = 1;
|
||||
c.weightx = 1;
|
||||
p.add(field, c);
|
||||
}
|
||||
}
|
||||
167
ts3-client/swing/src/main/java/com/ts3client/ui/IconTheme.java
Normal file
167
ts3-client/swing/src/main/java/com/ts3client/ui/IconTheme.java
Normal file
@@ -0,0 +1,167 @@
|
||||
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}.
|
||||
*
|
||||
* <p>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.
|
||||
*
|
||||
* <p>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<String, ImageIcon> cache = new ConcurrentHashMap<>();
|
||||
private final List<Runnable> listeners = new CopyOnWriteArrayList<>();
|
||||
|
||||
private volatile List<IconPack> 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<IconPack> 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -72,6 +72,7 @@ public final class MainFrame extends JFrame implements ServerTabPane.Listener {
|
||||
private final JLabel statusLabel = new JLabel("Not connected");
|
||||
private final JLabel codecLabel = new JLabel();
|
||||
|
||||
private JToolBar toolbar;
|
||||
private JButton connectButton;
|
||||
private JButton disconnectButton;
|
||||
private JToggleButton activeButton;
|
||||
@@ -108,7 +109,10 @@ public final class MainFrame extends JFrame implements ServerTabPane.Listener {
|
||||
|
||||
setJMenuBar(buildMenuBar());
|
||||
|
||||
add(buildToolbar(), BorderLayout.NORTH);
|
||||
toolbar = buildToolbar();
|
||||
add(toolbar, BorderLayout.NORTH);
|
||||
// Switching the icon pack in the options dialog re-decorates the whole window.
|
||||
IconTheme.get().addListener(() -> SwingUtilities.invokeLater(this::rebuildIcons));
|
||||
add(tabPane, BorderLayout.CENTER);
|
||||
add(buildStatusBar(), BorderLayout.SOUTH);
|
||||
|
||||
@@ -140,15 +144,15 @@ public final class MainFrame extends JFrame implements ServerTabPane.Listener {
|
||||
JMenuBar bar = new JMenuBar();
|
||||
|
||||
JMenu connections = new JMenu("Connections");
|
||||
JMenuItem connect = new JMenuItem("Connect…");
|
||||
JMenuItem connect = new JMenuItem("Connect…", Icons.of("CONNECT"));
|
||||
connect.setAccelerator(KeyStroke.getKeyStroke("control S"));
|
||||
connect.addActionListener(e -> showConnectDialog());
|
||||
JMenuItem disconnect = new JMenuItem("Disconnect");
|
||||
JMenuItem disconnect = new JMenuItem("Disconnect", Icons.of("DISCONNECT"));
|
||||
disconnect.addActionListener(e -> doDisconnect());
|
||||
JMenuItem closeTab = new JMenuItem("Close tab");
|
||||
JMenuItem closeTab = new JMenuItem("Close tab", Icons.of("CLOSE_BUTTON"));
|
||||
closeTab.setAccelerator(KeyStroke.getKeyStroke("control W"));
|
||||
closeTab.addActionListener(e -> closeTab(selected));
|
||||
JMenuItem quit = new JMenuItem("Quit");
|
||||
JMenuItem quit = new JMenuItem("Quit", Icons.of("QUIT"));
|
||||
quit.addActionListener(e -> {
|
||||
shutdown();
|
||||
System.exit(0);
|
||||
@@ -163,17 +167,17 @@ public final class MainFrame extends JFrame implements ServerTabPane.Listener {
|
||||
rebuildBookmarksMenu();
|
||||
|
||||
JMenu self = new JMenu("Self");
|
||||
JMenuItem mute = new JMenuItem("Toggle microphone");
|
||||
JMenuItem mute = new JMenuItem("Toggle microphone", Icons.of("CAPTURE"));
|
||||
mute.addActionListener(e -> micButton.doClick());
|
||||
JMenuItem deaf = new JMenuItem("Toggle speakers");
|
||||
JMenuItem deaf = new JMenuItem("Toggle speakers", Icons.of("PLAYBACK"));
|
||||
deaf.addActionListener(e -> speakerButton.doClick());
|
||||
awayItem = new JCheckBoxMenuItem("Away");
|
||||
awayItem = new JCheckBoxMenuItem("Away", Icons.of("AWAY"), false);
|
||||
awayItem.addActionListener(e -> toggleAway());
|
||||
commanderItem = new JCheckBoxMenuItem("Channel commander");
|
||||
commanderItem = new JCheckBoxMenuItem("Channel commander", Icons.of("CHANNEL_COMMANDER"), false);
|
||||
commanderItem.addActionListener(e -> {
|
||||
if (selected != null) selected.setCommander(commanderItem.isSelected());
|
||||
});
|
||||
JMenuItem nick = new JMenuItem("Change nickname…");
|
||||
JMenuItem nick = new JMenuItem("Change nickname…", Icons.of("CHANGE_NICKNAME"));
|
||||
nick.addActionListener(e -> changeNickname());
|
||||
self.add(mute);
|
||||
self.add(deaf);
|
||||
@@ -184,16 +188,16 @@ public final class MainFrame extends JFrame implements ServerTabPane.Listener {
|
||||
self.add(nick);
|
||||
|
||||
JMenu tools = new JMenu("Tools");
|
||||
JMenuItem identitiesItem = new JMenuItem("Identities…");
|
||||
JMenuItem identitiesItem = new JMenuItem("Identities…", Icons.of("IDENTITY_MANAGER"));
|
||||
identitiesItem.addActionListener(e -> showIdentities());
|
||||
JMenuItem options = new JMenuItem("Options…");
|
||||
JMenuItem options = new JMenuItem("Options…", Icons.of("SETTINGS"));
|
||||
options.addActionListener(e -> showSettings());
|
||||
tools.add(identitiesItem);
|
||||
tools.addSeparator();
|
||||
tools.add(options);
|
||||
|
||||
JMenu help = new JMenu("Help");
|
||||
JMenuItem about = new JMenuItem("About");
|
||||
JMenuItem about = new JMenuItem("About", Icons.of("ABOUT"));
|
||||
about.addActionListener(e -> showAbout());
|
||||
help.add(about);
|
||||
|
||||
@@ -208,14 +212,14 @@ public final class MainFrame extends JFrame implements ServerTabPane.Listener {
|
||||
private void rebuildBookmarksMenu() {
|
||||
bookmarksMenu.removeAll();
|
||||
for (Bookmark b : bookmarks.all()) {
|
||||
JMenuItem item = new JMenuItem(b.displayName());
|
||||
JMenuItem item = new JMenuItem(b.displayName(), Icons.of("SERVER_GREEN"));
|
||||
item.addActionListener(e -> connectToBookmark(b));
|
||||
bookmarksMenu.add(item);
|
||||
}
|
||||
if (!bookmarks.all().isEmpty()) bookmarksMenu.addSeparator();
|
||||
JMenuItem addCurrent = new JMenuItem("Add current server…");
|
||||
JMenuItem addCurrent = new JMenuItem("Add current server…", Icons.of("BOOKMARK_ADD"));
|
||||
addCurrent.addActionListener(e -> addCurrentServerBookmark());
|
||||
JMenuItem manage = new JMenuItem("Manage bookmarks…");
|
||||
JMenuItem manage = new JMenuItem("Manage bookmarks…", Icons.of("BOOKMARK_MANAGER"));
|
||||
manage.addActionListener(e -> new BookmarksDialog(this, bookmarks, identities,
|
||||
this::connectToBookmark, this::rebuildBookmarksMenu).setVisible(true));
|
||||
bookmarksMenu.add(addCurrent);
|
||||
@@ -275,6 +279,19 @@ public final class MainFrame extends JFrame implements ServerTabPane.Listener {
|
||||
return tb;
|
||||
}
|
||||
|
||||
/** Rebuilds the icon-bearing chrome after the active icon pack changed. */
|
||||
private void rebuildIcons() {
|
||||
setIconImage(Icons.app().getImage());
|
||||
setJMenuBar(buildMenuBar());
|
||||
remove(toolbar);
|
||||
toolbar = buildToolbar();
|
||||
add(toolbar, BorderLayout.NORTH);
|
||||
updateToolbar();
|
||||
refreshTabs();
|
||||
revalidate();
|
||||
repaint();
|
||||
}
|
||||
|
||||
private JPanel buildStatusBar() {
|
||||
JPanel bar = new JPanel(new BorderLayout());
|
||||
bar.setBackground(Theme.STATUS_BG);
|
||||
@@ -574,9 +591,9 @@ public final class MainFrame extends JFrame implements ServerTabPane.Listener {
|
||||
boolean micMuted = connected && selected.isMicMuted();
|
||||
boolean deaf = connected && selected.isDeafened();
|
||||
micButton.setSelected(micMuted);
|
||||
micButton.setIcon(micMuted ? Icons.micMuted() : Icons.mic());
|
||||
micButton.setIcon(micMuted ? Icons.micMutedLarge() : Icons.mic());
|
||||
speakerButton.setSelected(deaf);
|
||||
speakerButton.setIcon(deaf ? Icons.speakerMuted() : Icons.speaker());
|
||||
speakerButton.setIcon(deaf ? Icons.speakerMutedLarge() : Icons.speaker());
|
||||
activeButton.setSelected(selected != null && selected == micTab);
|
||||
awayItem.setSelected(connected && selected.isAway());
|
||||
commanderItem.setSelected(connected && selected.isCommander());
|
||||
|
||||
@@ -132,7 +132,7 @@ final class ServerTabPane extends JPanel {
|
||||
cell.setOpaque(false);
|
||||
cell.setBorder(BorderFactory.createEmptyBorder(1, 0, 1, 0));
|
||||
|
||||
JLabel label = new JLabel(tab.title(), hasMic ? Icons.micActive() : null, JLabel.LEADING);
|
||||
JLabel label = new JLabel(tab.title(), hasMic ? Icons.micActiveSmall() : null, JLabel.LEADING);
|
||||
label.setFont(Theme.UI_FONT);
|
||||
label.setToolTipText(hasMic ? "Speaking on this server" : tab.status());
|
||||
// A label with a tooltip swallows mouse events, so select the tab explicitly.
|
||||
|
||||
@@ -583,7 +583,7 @@ public final class ServerTreePanel extends JScrollPane {
|
||||
setFont(Theme.UI_FONT);
|
||||
} else {
|
||||
setText(c.name);
|
||||
setIcon(c.hasPassword ? Icons.channelLocked() : Icons.channel());
|
||||
setIcon(iconFor(c));
|
||||
setForeground(Theme.CHANNEL_TEXT);
|
||||
setFont(Theme.UI_BOLD);
|
||||
}
|
||||
@@ -609,10 +609,21 @@ public final class ServerTreePanel extends JScrollPane {
|
||||
return this;
|
||||
}
|
||||
|
||||
private ImageIcon iconFor(ChannelNode c) {
|
||||
if (c.hasPassword) return Icons.channelLocked();
|
||||
if (c.maxClients >= 0 && c.clients.size() >= c.maxClients) return Icons.channelFull();
|
||||
return Icons.channel();
|
||||
}
|
||||
|
||||
/** The client's state, in the order the official client gives them priority. */
|
||||
private ImageIcon iconFor(ClientEntry cl) {
|
||||
if (cl.isQuery()) return Icons.clientQuery();
|
||||
if (cl.outputMuted) return Icons.speakerMuted();
|
||||
if (cl.inputMuted) return Icons.micMuted();
|
||||
if (cl.away) return Icons.clientAway();
|
||||
if (cl.channelCommander) {
|
||||
return cl.talking ? Icons.clientCommanderTalking() : Icons.clientCommander();
|
||||
}
|
||||
if (cl.talking) return Icons.clientTalking();
|
||||
return Icons.clientIdle();
|
||||
}
|
||||
|
||||
@@ -56,6 +56,7 @@ public final class SettingsDialog extends JDialog {
|
||||
private final Runnable onApply;
|
||||
|
||||
private NotificationsPanel notificationsPanel;
|
||||
private IconPackPanel iconPackPanel;
|
||||
|
||||
private JComboBox<AudioDevices.Device> inputCombo;
|
||||
private JComboBox<AudioDevices.Device> outputCombo;
|
||||
@@ -104,6 +105,8 @@ public final class SettingsDialog extends JDialog {
|
||||
tabs.addTab("Voice Activation", scrollable(buildVoiceTab()));
|
||||
notificationsPanel = new NotificationsPanel(settings, sounds);
|
||||
tabs.addTab("Notifications", notificationsPanel);
|
||||
iconPackPanel = new IconPackPanel(settings);
|
||||
tabs.addTab("Design", iconPackPanel);
|
||||
|
||||
JPanel buttons = new JPanel(new BorderLayout());
|
||||
JPanel right = new JPanel();
|
||||
@@ -115,6 +118,7 @@ public final class SettingsDialog extends JDialog {
|
||||
});
|
||||
cancel.addActionListener(e -> {
|
||||
notificationsPanel.revert();
|
||||
iconPackPanel.revert();
|
||||
close();
|
||||
});
|
||||
right.add(ok);
|
||||
@@ -549,6 +553,7 @@ public final class SettingsDialog extends JDialog {
|
||||
settings.fec = fecCheck.isSelected();
|
||||
settings.music = musicCheck.isSelected();
|
||||
notificationsPanel.apply();
|
||||
iconPackPanel.apply();
|
||||
settings.save();
|
||||
|
||||
if (liveMic != null) {
|
||||
|
||||
Reference in New Issue
Block a user