diff --git a/ts3-client/core/src/main/java/com/ts3client/config/Settings.java b/ts3-client/core/src/main/java/com/ts3client/config/Settings.java
index b356ea6..c8ba187 100644
--- a/ts3-client/core/src/main/java/com/ts3client/config/Settings.java
+++ b/ts3-client/core/src/main/java/com/ts3client/config/Settings.java
@@ -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 {
diff --git a/ts3-client/pom.xml b/ts3-client/pom.xml
index db3079e..6cdfc81 100644
--- a/ts3-client/pom.xml
+++ b/ts3-client/pom.xml
@@ -23,6 +23,7 @@
UTF-81.0.32.1.0
+ 3.7.23.5.6
@@ -50,6 +51,11 @@
jsvg${jsvg.version}
+
+ com.formdev
+ flatlaf
+ ${flatlaf.version}
+ com.ts3clientts3-client-core
diff --git a/ts3-client/swing/pom.xml b/ts3-client/swing/pom.xml
index ad09dbd..815c722 100644
--- a/ts3-client/swing/pom.xml
+++ b/ts3-client/swing/pom.xml
@@ -35,6 +35,10 @@
com.github.weisjjsvg
+
+ com.formdev
+ flatlaf
+ org.junit.jupiterjunit-jupiter
@@ -84,6 +88,15 @@
**
+
+
+ com.formdev:flatlaf
+
+ **
+
+ *:*
diff --git a/ts3-client/swing/src/main/java/com/ts3client/Main.java b/ts3-client/swing/src/main/java/com/ts3client/Main.java
index 0be765e..6aa28a9 100644
--- a/ts3-client/swing/src/main/java/com/ts3client/Main.java
+++ b/ts3-client/swing/src/main/java/com/ts3client/Main.java
@@ -2,14 +2,14 @@ package com.ts3client;
import com.ts3client.config.Settings;
import com.ts3client.ui.IconTheme;
+import com.ts3client.ui.LookAndFeelManager;
import com.ts3client.ui.MainFrame;
import javax.swing.SwingUtilities;
-import javax.swing.UIManager;
/**
- * Application entry point. Loads settings, applies the system look-and-feel and
- * shows the main window on the Swing event dispatch thread.
+ * Application entry point. Loads settings, installs the look-and-feel and shows the
+ * main window on the Swing event dispatch thread.
*/
public final class Main {
@@ -23,11 +23,7 @@ public final class Main {
final Settings settings = Settings.load();
SwingUtilities.invokeLater(() -> {
- try {
- UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName());
- } catch (Exception ignored) {
- // fall back to cross-platform L&F
- }
+ LookAndFeelManager.install(settings.appearance);
IconTheme.get().reload(settings);
new MainFrame(settings).setVisible(true);
});
diff --git a/ts3-client/swing/src/main/java/com/ts3client/ui/ChannelPermissionsPanel.java b/ts3-client/swing/src/main/java/com/ts3client/ui/ChannelPermissionsPanel.java
index 3a88662..46b8ab1 100644
--- a/ts3-client/swing/src/main/java/com/ts3client/ui/ChannelPermissionsPanel.java
+++ b/ts3-client/swing/src/main/java/com/ts3client/ui/ChannelPermissionsPanel.java
@@ -108,7 +108,7 @@ final class ChannelPermissionsPanel extends JPanel {
void setStatus(String message, boolean error) {
status.setText(message == null || message.isEmpty() ? " " : message);
- status.setForeground(error ? Color.RED.darker() : Theme.CHAT_SYSTEM);
+ status.setForeground(error ? Color.RED.darker() : Theme.chatSystem());
}
private void setEditable(boolean editable) {
diff --git a/ts3-client/swing/src/main/java/com/ts3client/ui/ChatPanel.java b/ts3-client/swing/src/main/java/com/ts3client/ui/ChatPanel.java
index b266390..5aa4a77 100644
--- a/ts3-client/swing/src/main/java/com/ts3client/ui/ChatPanel.java
+++ b/ts3-client/swing/src/main/java/com/ts3client/ui/ChatPanel.java
@@ -16,8 +16,6 @@ import javax.swing.SwingUtilities;
import javax.swing.event.HyperlinkEvent;
import javax.swing.text.BadLocationException;
import javax.swing.text.html.HTMLDocument;
-import javax.swing.text.html.HTMLEditorKit;
-import javax.swing.text.html.StyleSheet;
import java.awt.BorderLayout;
import java.awt.Component;
import java.awt.Cursor;
@@ -75,7 +73,7 @@ public final class ChatPanel extends JPanel {
public ChatPanel() {
super(new BorderLayout());
- tabs.setFont(Theme.UI_FONT);
+ tabs.setFont(Theme.uiFont());
tabs.setTabLayoutPolicy(JTabbedPane.SCROLL_TAB_LAYOUT);
addTab(serverTab, false);
addTab(channelTab, false);
@@ -186,7 +184,7 @@ public final class ChatPanel extends JPanel {
/** System notices always land in the server tab. */
public void appendSystem(String text) {
- edt(() -> serverTab.appendLine("" + stamp()
+ edt(() -> serverTab.appendLine("" + stamp()
+ BBCode.escape(text) + ""));
}
@@ -196,7 +194,7 @@ public final class ChatPanel extends JPanel {
* clickable here — everything else in them is literal text.
*/
public void appendServerLog(String text) {
- edt(() -> serverTab.appendLine("" + stamp()
+ edt(() -> serverTab.appendLine("" + stamp()
+ BBCode.linksToHtml(text) + ""));
}
@@ -288,13 +286,12 @@ public final class ChatPanel extends JPanel {
final Target target;
final int clientId;
final String title;
- final JEditorPane log = new JEditorPane();
+ final JEditorPane log = HtmlStyles.pane("font-family:sans-serif; font-size:11px; margin:4px 6px;");
final JScrollPane scroll;
/** Set for a static-content tab (e.g. a moved-out description); null for a conversation. */
final String noteKey;
final Runnable onClose;
private final Icon icon;
- private final HTMLDocument doc;
private JLabel titleLabel;
Tab(Target target, int clientId, String title) {
@@ -314,28 +311,7 @@ public final class ChatPanel extends JPanel {
this.noteKey = noteKey;
this.onClose = onClose;
- HTMLEditorKit kit = new HTMLEditorKit();
- StyleSheet css = new StyleSheet();
- css.addStyleSheet(kit.getStyleSheet());
- css.addRule("body { font-family:sans-serif; font-size:11px; color:#202020; margin:4px 6px; }");
- css.addRule("a { color:" + hex(Theme.CHAT_NAME) + "; text-decoration:none; }");
- // Client references look exactly like a message author's name.
- css.addRule("a." + BBCode.IDENTITY_LINK_CLASS
- + " { color:" + hex(Theme.CHAT_NAME) + "; font-weight:bold; text-decoration:none; }");
- // Channel (and other TeamSpeak protocol) references stand out of a line the
- // same way a client's name does.
- css.addRule("a." + BBCode.TS_LINK_CLASS
- + " { color:" + hex(Theme.CHAT_NAME) + "; font-weight:bold; text-decoration:none; }");
- // External links are underlined so they can't be mistaken for an identity.
- css.addRule("a." + BBCode.EXTERNAL_LINK_CLASS
- + " { color:" + hex(Theme.LINK) + "; text-decoration:underline; }");
- kit.setStyleSheet(css);
-
- log.setEditorKit(kit);
- log.setEditable(false);
- log.setBackground(Theme.CHAT_BG);
log.setText("");
- doc = (HTMLDocument) log.getDocument();
log.addHyperlinkListener(e -> {
if (e.getEventType() != HyperlinkEvent.EventType.ACTIVATED) return;
onLink(e.getDescription());
@@ -363,14 +339,14 @@ public final class ChatPanel extends JPanel {
JPanel p = new JPanel(new FlowLayout(FlowLayout.LEFT, 4, 0));
p.setOpaque(false);
titleLabel = new JLabel(title, icon, JLabel.LEADING);
- titleLabel.setFont(Theme.UI_FONT);
+ titleLabel.setFont(Theme.uiFont());
p.add(titleLabel);
dragReorder.attach(p);
dragReorder.attach(titleLabel);
if (closable) {
JLabel close = new JLabel("×");
- close.setFont(Theme.UI_BOLD);
- close.setForeground(Theme.CHAT_SYSTEM);
+ close.setFont(Theme.uiBold());
+ close.setForeground(Theme.chatSystem());
close.setCursor(Cursor.getPredefinedCursor(Cursor.HAND_CURSOR));
close.setToolTipText("Close chat");
close.addMouseListener(new MouseAdapter() {
@@ -394,8 +370,8 @@ public final class ChatPanel extends JPanel {
void setUnread(boolean unread) {
if (titleLabel == null) return;
- titleLabel.setFont(unread ? Theme.UI_BOLD : Theme.UI_FONT);
- titleLabel.setForeground(unread ? Theme.ACCENT : null);
+ titleLabel.setFont(unread ? Theme.uiBold() : Theme.uiFont());
+ titleLabel.setForeground(unread ? Theme.accent() : null);
}
void appendMessage(int fromId, String from, String text) {
@@ -403,8 +379,8 @@ public final class ChatPanel extends JPanel {
String sender = fromId > 0
? "" + name + ""
- : "" + name + "";
- appendLine("" + stamp() + ""
+ : "" + name + "";
+ appendLine("" + stamp() + ""
+ sender + ": " + BBCode.toHtml(text));
}
@@ -416,6 +392,7 @@ public final class ChatPanel extends JPanel {
void appendLine(String html) {
try {
+ HTMLDocument doc = (HTMLDocument) log.getDocument();
doc.insertBeforeEnd(doc.getElement("chatlog"), "
" + html + "
");
log.setCaretPosition(doc.getLength());
} catch (BadLocationException | IOException ignored) {
@@ -423,8 +400,4 @@ public final class ChatPanel extends JPanel {
if (tabs.getSelectedComponent() != scroll) setUnread(true);
}
}
-
- private static String hex(java.awt.Color c) {
- return String.format("#%06X", c.getRGB() & 0xFFFFFF);
- }
}
diff --git a/ts3-client/swing/src/main/java/com/ts3client/ui/ConnectionInfoDialog.java b/ts3-client/swing/src/main/java/com/ts3client/ui/ConnectionInfoDialog.java
index 82442f2..f709051 100644
--- a/ts3-client/swing/src/main/java/com/ts3client/ui/ConnectionInfoDialog.java
+++ b/ts3-client/swing/src/main/java/com/ts3client/ui/ConnectionInfoDialog.java
@@ -63,7 +63,7 @@ public final class ConnectionInfoDialog extends JDialog {
this.clientId = clientId;
JPanel content = new JPanel(new BorderLayout(0, 10));
- content.setBackground(Theme.WINDOW_BG);
+ content.setBackground(Theme.windowBg());
content.setBorder(BorderFactory.createEmptyBorder(12, 14, 12, 14));
content.add(buildSummary(), BorderLayout.NORTH);
content.add(buildTabs(), BorderLayout.CENTER);
@@ -92,7 +92,7 @@ public final class ConnectionInfoDialog extends JDialog {
private JPanel buildSummary() {
JPanel grid = new JPanel(new GridBagLayout());
- grid.setBackground(Theme.WINDOW_BG);
+ grid.setBackground(Theme.windowBg());
int row = 0;
addRow(grid, row++, "Address", addressValue);
addRow(grid, row++, "Client version", versionValue);
@@ -106,8 +106,8 @@ public final class ConnectionInfoDialog extends JDialog {
private JTabbedPane buildTabs() {
JTabbedPane tabs = new JTabbedPane();
- tabs.setFont(Theme.UI_FONT);
- tabs.setBackground(Theme.WINDOW_BG);
+ tabs.setFont(Theme.uiFont());
+ tabs.setBackground(Theme.windowBg());
tabs.addTab("Total", totalTab);
tabs.addTab("Speech", speechTab);
tabs.addTab("Keep Alive", keepAliveTab);
@@ -118,7 +118,7 @@ public final class ConnectionInfoDialog extends JDialog {
private JPanel buildButtons() {
JPanel bar = new JPanel();
bar.setLayout(new BoxLayout(bar, BoxLayout.X_AXIS));
- bar.setBackground(Theme.WINDOW_BG);
+ bar.setBackground(Theme.windowBg());
bar.add(Box.createHorizontalGlue());
JButton close = new JButton("Close");
close.addActionListener(e -> dispose());
@@ -128,8 +128,8 @@ public final class ConnectionInfoDialog extends JDialog {
private static JLabel value() {
JLabel l = new JLabel("—");
- l.setFont(Theme.UI_FONT);
- l.setForeground(Theme.TREE_TEXT);
+ l.setFont(Theme.uiFont());
+ l.setForeground(Theme.treeText());
return l;
}
@@ -140,7 +140,7 @@ public final class ConnectionInfoDialog extends JDialog {
lc.anchor = GridBagConstraints.WEST;
lc.insets = new Insets(2, 0, 2, 16);
JLabel key = new JLabel(label);
- key.setFont(Theme.UI_FONT);
+ key.setFont(Theme.uiFont());
key.setForeground(new Color(0x5A6B7B));
grid.add(key, lc);
@@ -255,7 +255,7 @@ public final class ConnectionInfoDialog extends JDialog {
KindTab() {
super(new GridBagLayout());
- setBackground(Theme.WINDOW_BG);
+ setBackground(Theme.windowBg());
setBorder(BorderFactory.createEmptyBorder(10, 12, 10, 12));
int row = 0;
addRow(this, row++, "Packet loss", packetLoss);
diff --git a/ts3-client/swing/src/main/java/com/ts3client/ui/DescriptionEditorDialog.java b/ts3-client/swing/src/main/java/com/ts3client/ui/DescriptionEditorDialog.java
index 203f676..455aaf2 100644
--- a/ts3-client/swing/src/main/java/com/ts3client/ui/DescriptionEditorDialog.java
+++ b/ts3-client/swing/src/main/java/com/ts3client/ui/DescriptionEditorDialog.java
@@ -51,14 +51,14 @@ final class DescriptionEditorDialog extends JDialog {
area.setCaretPosition(0);
preview.setEditable(false);
- preview.setBackground(Theme.CHAT_BG);
+ preview.setBackground(Theme.chatBg());
preview.setBorder(BorderFactory.createEmptyBorder(6, 8, 6, 8));
body.add(new JScrollPane(area), EDIT_CARD);
body.add(new JScrollPane(preview), PREVIEW_CARD);
JLabel hint = new JLabel("Press Button to \"Preview\" the changes in channel info.");
- hint.setForeground(Theme.CHAT_SYSTEM);
+ hint.setForeground(Theme.chatSystem());
hint.setBorder(BorderFactory.createEmptyBorder(4, 6, 0, 6));
getContentPane().setLayout(new BorderLayout());
diff --git a/ts3-client/swing/src/main/java/com/ts3client/ui/IconPackPanel.java b/ts3-client/swing/src/main/java/com/ts3client/ui/DesignPanel.java
similarity index 75%
rename from ts3-client/swing/src/main/java/com/ts3client/ui/IconPackPanel.java
rename to ts3-client/swing/src/main/java/com/ts3client/ui/DesignPanel.java
index 214dcc6..84ea932 100644
--- a/ts3-client/swing/src/main/java/com/ts3client/ui/IconPackPanel.java
+++ b/ts3-client/swing/src/main/java/com/ts3client/ui/DesignPanel.java
@@ -26,13 +26,14 @@ 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.
+ * Options page for how the client looks: the light or dark look-and-feel, and the icon
+ * pack the user interface is drawn with — where to look for more of them, and a viewer
+ * showing everything the active pack contains.
*
- *
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.
+ *
Both apply right away so the change can be seen in the window behind the dialog;
+ * cancelling puts the previous ones back.
*/
-final class IconPackPanel extends JPanel {
+final class DesignPanel extends JPanel {
/** Size of the tiles in the icon viewer. */
private static final int PREVIEW_SIZE = 32;
@@ -44,18 +45,22 @@ final class IconPackPanel extends JPanel {
private final Settings settings;
private final String originalPackId;
private final String originalPackDir;
+ private final Settings.Appearance originalAppearance;
+ private final JComboBox appearanceCombo =
+ new JComboBox<>(Settings.Appearance.values());
private final JComboBox