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

@@ -27,6 +27,12 @@ public final class Settings {
CONTINUOUS
}
/** Which look-and-feel variant the window is painted with. */
public enum Appearance {
LIGHT,
DARK
}
/** Voice-activation strategy, mirroring the TS3 client's VAD modes. */
public enum VadMode {
/** Speech-probability detector only. */
@@ -125,6 +131,9 @@ public final class Settings {
/** Extra folder to look for icon packs in, on top of the well-known locations. */
public String iconPackDir = "";
/** Look-and-feel variant; see {@link Appearance}. */
public Appearance appearance = Appearance.LIGHT;
// ---- window chrome ----
/** Whether the status bar at the bottom of the window is shown. */
public boolean showStatusBar = true;
@@ -218,6 +227,7 @@ public final class Settings {
soundPackDir = props.getProperty("soundPackDir", soundPackDir);
iconPack = props.getProperty("iconPack", iconPack);
iconPackDir = props.getProperty("iconPackDir", iconPackDir);
appearance = parseAppearance(props.getProperty("appearance"), appearance);
showStatusBar = parseB(props.getProperty("showStatusBar"), showStatusBar);
showMasterVolumeSlider = parseB(props.getProperty("showMasterVolumeSlider"), showMasterVolumeSlider);
notifications.load(props);
@@ -259,6 +269,7 @@ public final class Settings {
props.setProperty("soundPackDir", soundPackDir);
props.setProperty("iconPack", iconPack);
props.setProperty("iconPackDir", iconPackDir);
props.setProperty("appearance", appearance.name());
props.setProperty("showStatusBar", Boolean.toString(showStatusBar));
props.setProperty("showMasterVolumeSlider", Boolean.toString(showMasterVolumeSlider));
notifications.store(props);
@@ -273,6 +284,15 @@ public final class Settings {
}
}
private static Appearance parseAppearance(String v, Appearance def) {
if (v == null) return def;
try {
return Appearance.valueOf(v);
} catch (IllegalArgumentException e) {
return def;
}
}
private static VadMode parseVadMode(String v, VadMode def) {
if (v == null) return def;
try {