Paint the client with FlatLaf, in a light and a dark theme

The look-and-feel is FlatLaf now, chosen light or dark on the new Design
tab and remembered in the settings. Theme keeps only the colours TeamSpeak
gives meaning to (talking, away, muted, channel and server names) and takes
its surfaces and fonts from the look-and-feel, so a switch recolours the
whole window, chat logs included: the HTML panes share one themed stylesheet
and re-parse their content when the theme changes.

Also fixes the channel tree not scrolling: the tree listened for the mouse
wheel to re-check its hover row, and AWT only forwards a wheel event to the
enclosing scroll pane when the component under the pointer has no wheel
listener of its own. The hover row now follows the viewport instead, which
also covers scrollbar drags and keyboard scrolling.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-08-19 21:06:55 +00:00
parent d952ffd856
commit 35f10ed14c
25 changed files with 484 additions and 191 deletions

View File

@@ -1,39 +1,144 @@
package com.ts3client.ui;
import javax.swing.UIManager;
import java.awt.Color;
import java.awt.Font;
/** Central palette + fonts approximating the light TeamSpeak 3 look. */
/**
* The palette and fonts the client paints with, on top of the active look-and-feel.
*
* <p>Surfaces (window, tree, chat background) and fonts come straight from the
* look-and-feel, so the client follows whatever FlatLaf theme is installed. The
* status colours TeamSpeak gives meaning to — talking, away, muted, channel and
* server names — are ours, in a light and a dark variant; {@link #refresh()} picks
* the variant matching the installed theme.
*/
public final class Theme {
public static final Color WINDOW_BG = new Color(0xF0F0F0);
public static final Color TREE_BG = new Color(0xFFFFFF);
public static final Color TREE_SELECTION = new Color(0xCFE3FB);
/** Outline drawn around the row the pointer is over. */
public static final Color TREE_HOVER = new Color(0x9CC4EE);
public static final Color TREE_TEXT = new Color(0x1E1E1E);
public static final Color CHANNEL_TEXT = new Color(0x21486B);
public static final Color SERVER_TEXT = new Color(0x123456);
public static final Color TALKING = new Color(0x33B35A);
public static final Color IDLE_CLIENT = new Color(0x6E7B87);
public static final Color AWAY = new Color(0xC98A1B);
public static final Color MUTED = new Color(0xC0392B);
public static final Color TOOLBAR_BG = new Color(0xE6E9ED);
public static final Color STATUS_BG = new Color(0xE6E9ED);
public static final Color ACCENT = new Color(0x2C7BE5);
public static final Color CHAT_BG = new Color(0xFAFAFA);
public static final Color CHAT_SYSTEM = new Color(0x8A8A8A);
public static final Color CHAT_NAME = new Color(0x2C7BE5);
public static final Color CHAT_TEXT = new Color(0x202020);
/** Links that leave the client, distinct from the colour used for identities. */
public static final Color LINK = new Color(0x1A5FB4);
public static final Font UI_FONT = new Font("SansSerif", Font.PLAIN, 12);
public static final Font UI_BOLD = new Font("SansSerif", Font.BOLD, 12);
private static boolean dark;
private Theme() {
}
/** Re-reads the installed look-and-feel; call after changing it. */
static void refresh() {
Color bg = UIManager.getColor("Panel.background");
dark = bg != null && luminance(bg) < 0.5;
}
public static boolean isDark() {
return dark;
}
// ---- fonts ----
public static Font uiFont() {
Font f = UIManager.getFont("Label.font");
return f != null ? f : new Font("SansSerif", Font.PLAIN, 12);
}
public static Font uiBold() {
return uiFont().deriveFont(Font.BOLD);
}
// ---- surfaces, from the look-and-feel ----
public static Color windowBg() {
return ui("Panel.background", 0xF0F0F0, 0x3C3F41);
}
public static Color toolbarBg() {
return ui("ToolBar.background", 0xE6E9ED, 0x3C3F41);
}
public static Color statusBg() {
return windowBg();
}
public static Color treeBg() {
return ui("Tree.background", 0xFFFFFF, 0x2B2B2B);
}
public static Color treeText() {
return ui("Tree.foreground", 0x1E1E1E, 0xDFE1E5);
}
public static Color treeSelection() {
return ui("Tree.selectionBackground", 0xCFE3FB, 0x2F5075);
}
/** Outline drawn around the row the pointer is over. */
public static Color treeHover() {
return pick(0x9CC4EE, 0x4A6E96);
}
public static Color chatBg() {
return ui("TextPane.background", 0xFAFAFA, 0x2B2B2B);
}
public static Color border() {
return ui("Component.borderColor", 0xD0D0D0, 0x4B4B4B);
}
public static Color chatText() {
return ui("TextPane.foreground", 0x202020, 0xDFE1E5);
}
// ---- status colours ----
public static Color channelText() {
return pick(0x21486B, 0x8FBCE6);
}
public static Color serverText() {
return pick(0x123456, 0xB9D4F0);
}
public static Color talking() {
return pick(0x33B35A, 0x4CC479);
}
public static Color idleClient() {
return pick(0x6E7B87, 0x9AA6B2);
}
public static Color away() {
return pick(0xC98A1B, 0xE0A83C);
}
public static Color muted() {
return pick(0xC0392B, 0xE05C4C);
}
public static Color accent() {
return pick(0x2C7BE5, 0x5C9BEE);
}
public static Color chatSystem() {
return pick(0x8A8A8A, 0x9A9A9A);
}
public static Color chatName() {
return accent();
}
/** Links that leave the client, distinct from the colour used for identities. */
public static Color link() {
return pick(0x1A5FB4, 0x6EA8FF);
}
private static Color pick(int light, int darkRgb) {
return new Color(dark ? darkRgb : light);
}
private static Color ui(String key, int lightFallback, int darkFallback) {
// Returned as the look-and-feel's own UIResource colour, so components painted with
// it are recoloured automatically when the look-and-feel is switched.
Color c = UIManager.getColor(key);
return c != null ? c : pick(lightFallback, darkFallback);
}
private static double luminance(Color c) {
return (0.2126 * c.getRed() + 0.7152 * c.getGreen() + 0.0722 * c.getBlue()) / 255.0;
}
}