Darken the dark theme, and hover a tree row like a menu
The client's own look-and-feel defaults now live beside LookAndFeelManager as FlatLaf properties files, which FlatLaf loads on top of each theme's own, instead of a handful of UIManager.put calls. The dark theme is built on three surfaces keyed to the toolbar's #282828: the window chrome, the tab strips and menu bar one step below it at #232323, and the content the client shows — channel tree, chat log, text fields, lists — at #1e1e1e. Selections are #364b6f, and channel names in the tree are white; the chat lines about server events keep the blue they had, which channel names no longer carry. Everything FlatLaf derives (borders, scroll bars, hover states) follows from those. A read-only editor pane is a chat log or an information view here, not a disabled input, so it keeps the content background rather than FlatLaf's inactive one; and since neither a text view nor a tree fills its viewport, the space below the last line takes their background instead of the window's. The row under the pointer is now washed in the same colour a hovered menu is, rather than outlined. FlatLaf states that colour as a shift of the surface beneath it, so it is resolved against the window background exactly as the menu bar resolves it. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -318,6 +318,7 @@ public final class ChatPanel extends JPanel {
|
||||
});
|
||||
|
||||
scroll = new JScrollPane(log);
|
||||
HtmlStyles.fillViewport(log);
|
||||
scroll.setBorder(BorderFactory.createEmptyBorder());
|
||||
}
|
||||
|
||||
|
||||
@@ -148,9 +148,9 @@ final class DropIndicatorTree extends JTree {
|
||||
g.setColor(getBackground());
|
||||
g.fillRect(clip.x, clip.y, clip.width, clip.height);
|
||||
paintSelection(g);
|
||||
paintHover(g);
|
||||
super.paintComponent(g);
|
||||
paintBadges(g);
|
||||
paintHover(g);
|
||||
JTree.DropLocation loc = getDropLocation();
|
||||
if (loc == null || loc.getPath() == null) return;
|
||||
|
||||
@@ -190,22 +190,19 @@ final class DropIndicatorTree extends JTree {
|
||||
}
|
||||
|
||||
/**
|
||||
* Outlines the row under the pointer, across the full width the row occupies —
|
||||
* the same area a click on it acts on. Spacers, which nothing can be done with,
|
||||
* are left alone.
|
||||
* Washes the row under the pointer across the full width it occupies — the same
|
||||
* area a click on it acts on. A selected row keeps its selection colour, and
|
||||
* spacers, which nothing can be done with, are left alone.
|
||||
*/
|
||||
private void paintHover(Graphics g) {
|
||||
if (hoverRow < 0 || getDropLocation() != null) return;
|
||||
if (hoverRow < 0 || getDropLocation() != null || isRowSelected(hoverRow)) return;
|
||||
Rectangle bounds = getRowBounds(hoverRow);
|
||||
TreePath path = getPathForRow(hoverRow);
|
||||
if (bounds == null || path == null || isSpacer(path)) return;
|
||||
|
||||
Rectangle visible = getVisibleRect();
|
||||
Graphics2D g2 = (Graphics2D) g.create();
|
||||
g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
|
||||
g2.setColor(Theme.treeHover());
|
||||
g2.drawRoundRect(visible.x, bounds.y, visible.width - 1, bounds.height - 1, 4, 4);
|
||||
g2.dispose();
|
||||
g.setColor(Theme.hover());
|
||||
g.fillRect(visible.x, bounds.y, visible.width, bounds.height);
|
||||
}
|
||||
|
||||
private boolean isSpacer(TreePath path) {
|
||||
|
||||
@@ -3,6 +3,7 @@ package com.ts3client.ui;
|
||||
import com.ts3client.text.BBCode;
|
||||
|
||||
import javax.swing.JEditorPane;
|
||||
import javax.swing.JViewport;
|
||||
import javax.swing.SwingUtilities;
|
||||
import javax.swing.text.html.HTMLEditorKit;
|
||||
import javax.swing.text.html.StyleSheet;
|
||||
@@ -42,6 +43,17 @@ final class HtmlStyles {
|
||||
return pane;
|
||||
}
|
||||
|
||||
/**
|
||||
* Paints the scroll pane {@code pane} sits in with the pane's own background: text
|
||||
* only reaches as far as it goes, and the empty space below it would otherwise show
|
||||
* the window's colour instead of the log's.
|
||||
*/
|
||||
static void fillViewport(JEditorPane pane) {
|
||||
if (pane.getParent() instanceof JViewport viewport) {
|
||||
viewport.setBackground(pane.getBackground());
|
||||
}
|
||||
}
|
||||
|
||||
/** Re-themes {@code pane}, keeping the text it shows. */
|
||||
private static void restyle(JEditorPane pane) {
|
||||
// Also runs from JEditorPane's constructor, before the pane is one of ours.
|
||||
@@ -68,7 +80,7 @@ final class HtmlStyles {
|
||||
// about what happened on the server.
|
||||
css.addRule(".muted { color:" + hex(Theme.chatSystem()) + "; }");
|
||||
css.addRule(".name { color:" + hex(Theme.chatName()) + "; font-weight:bold; }");
|
||||
css.addRule(".event { color:" + hex(Theme.channelText()) + "; }");
|
||||
css.addRule(".event { color:" + hex(Theme.chatEvent()) + "; }");
|
||||
css.addRule("a { color:" + hex(Theme.chatName()) + "; text-decoration:none; }");
|
||||
// Client references look exactly like a message author's name.
|
||||
css.addRule("a." + BBCode.IDENTITY_LINK_CLASS
|
||||
@@ -83,6 +95,7 @@ final class HtmlStyles {
|
||||
kit.setStyleSheet(css);
|
||||
|
||||
pane.setEditorKit(kit);
|
||||
fillViewport(pane);
|
||||
if (html != null) {
|
||||
pane.setText(html);
|
||||
pane.setCaretPosition(atEnd ? pane.getDocument().getLength() : 0);
|
||||
|
||||
@@ -55,6 +55,7 @@ public final class InfoPanel extends JScrollPane {
|
||||
}
|
||||
});
|
||||
setViewportView(pane);
|
||||
HtmlStyles.fillViewport(pane);
|
||||
clear();
|
||||
}
|
||||
|
||||
|
||||
@@ -5,29 +5,34 @@ import com.formdev.flatlaf.FlatLaf;
|
||||
import com.formdev.flatlaf.FlatLightLaf;
|
||||
import com.ts3client.config.Settings;
|
||||
|
||||
import javax.swing.UIManager;
|
||||
import java.awt.Window;
|
||||
|
||||
/**
|
||||
* Installs FlatLaf in the requested variant and keeps {@link Theme} in step with it.
|
||||
*
|
||||
* <p>Also carries the few look-and-feel defaults the client wants everywhere: rounded
|
||||
* controls, a slimmer tree row and scroll bars that only show their thumb.
|
||||
* <p>The client's own look-and-feel defaults — rounded controls, and the darker palette
|
||||
* the dark theme is built on — live beside this class as FlatLaf properties files, which
|
||||
* FlatLaf loads on top of the theme's own.
|
||||
*/
|
||||
public final class LookAndFeelManager {
|
||||
|
||||
/** Where our own FlatLaf properties files sit. */
|
||||
private static final String DEFAULTS_PACKAGE = "com.ts3client.ui.laf";
|
||||
|
||||
static {
|
||||
// Registered once: FlatLaf keeps every registration, and install() runs again on
|
||||
// each theme switch.
|
||||
FlatLaf.registerCustomDefaultsSource(DEFAULTS_PACKAGE);
|
||||
}
|
||||
|
||||
private LookAndFeelManager() {
|
||||
}
|
||||
|
||||
/** Installs the look-and-feel for {@code appearance}. Call before building any window. */
|
||||
public static void install(Settings.Appearance appearance) {
|
||||
FlatLaf laf = appearance == Settings.Appearance.DARK ? new FlatDarkLaf() : new FlatLightLaf();
|
||||
if (!FlatLaf.setup(laf)) {
|
||||
// Leave whatever look-and-feel is installed; Theme falls back to its own palette.
|
||||
Theme.refresh();
|
||||
return;
|
||||
}
|
||||
applyDefaults();
|
||||
// Whatever look-and-feel stays installed on failure, Theme falls back to its own palette.
|
||||
FlatLaf.setup(laf);
|
||||
Theme.refresh();
|
||||
}
|
||||
|
||||
@@ -43,17 +48,4 @@ public final class LookAndFeelManager {
|
||||
w.repaint();
|
||||
}
|
||||
}
|
||||
|
||||
private static void applyDefaults() {
|
||||
UIManager.put("Component.focusWidth", 1);
|
||||
UIManager.put("Component.arc", 6);
|
||||
UIManager.put("Button.arc", 6);
|
||||
UIManager.put("ScrollBar.showButtons", false);
|
||||
UIManager.put("ScrollBar.thumbArc", 8);
|
||||
UIManager.put("ScrollBar.trackArc", 8);
|
||||
UIManager.put("TabbedPane.tabHeight", 26);
|
||||
UIManager.put("Tree.rowHeight", 0);
|
||||
UIManager.put("Tree.paintLines", false);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -161,7 +161,6 @@ public final class ServerTreePanel extends JScrollPane {
|
||||
tree.setPathEditable(nicknameEditor::editsPath);
|
||||
tree.setInvokesStopCellEditing(true);
|
||||
setViewportView(tree);
|
||||
getViewport().setBackground(Theme.treeBg());
|
||||
// The icon strip is drawn against the viewport's right edge, so the blitted
|
||||
// pixels a scroll would reuse are stale; repaint the whole viewport instead.
|
||||
getViewport().setScrollMode(JViewport.SIMPLE_SCROLL_MODE);
|
||||
@@ -279,6 +278,14 @@ public final class ServerTreePanel extends JScrollPane {
|
||||
ChannelMenu.build(channel, actions).show(tree, e.getX(), e.getY());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void updateUI() {
|
||||
super.updateUI();
|
||||
// The rows stop at the last channel, so the space below them belongs to the tree
|
||||
// rather than to the window behind it.
|
||||
if (getViewport() != null) getViewport().setBackground(Theme.treeBg());
|
||||
}
|
||||
|
||||
/** The path of the tree node showing {@code target}, or {@code null}. */
|
||||
private TreePath pathOf(Object target) {
|
||||
java.util.Enumeration<?> nodes = root.breadthFirstEnumeration();
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
package com.ts3client.ui;
|
||||
|
||||
import com.formdev.flatlaf.ui.FlatUIUtils;
|
||||
|
||||
import javax.swing.UIManager;
|
||||
import java.awt.Color;
|
||||
import java.awt.Font;
|
||||
@@ -44,11 +46,11 @@ public final class Theme {
|
||||
// ---- surfaces, from the look-and-feel ----
|
||||
|
||||
public static Color windowBg() {
|
||||
return ui("Panel.background", 0xF0F0F0, 0x3C3F41);
|
||||
return ui("Panel.background", 0xF0F0F0, 0x282828);
|
||||
}
|
||||
|
||||
public static Color toolbarBg() {
|
||||
return ui("ToolBar.background", 0xE6E9ED, 0x3C3F41);
|
||||
return ui("ToolBar.background", 0xE6E9ED, 0x282828);
|
||||
}
|
||||
|
||||
public static Color statusBg() {
|
||||
@@ -56,42 +58,52 @@ public final class Theme {
|
||||
}
|
||||
|
||||
public static Color treeBg() {
|
||||
return ui("Tree.background", 0xFFFFFF, 0x2B2B2B);
|
||||
return ui("Tree.background", 0xFFFFFF, 0x1E1E1E);
|
||||
}
|
||||
|
||||
public static Color treeText() {
|
||||
return ui("Tree.foreground", 0x1E1E1E, 0xDFE1E5);
|
||||
return ui("Tree.foreground", 0x1E1E1E, 0xE0E0E0);
|
||||
}
|
||||
|
||||
public static Color treeSelection() {
|
||||
return ui("Tree.selectionBackground", 0xCFE3FB, 0x2F5075);
|
||||
return ui("Tree.selectionBackground", 0xCFE3FB, 0x364B6F);
|
||||
}
|
||||
|
||||
/** Outline drawn around the row the pointer is over. */
|
||||
public static Color treeHover() {
|
||||
return pick(0x9CC4EE, 0x4A6E96);
|
||||
/**
|
||||
* Behind the row the pointer is over: the same wash the menu bar puts behind a hovered
|
||||
* menu. The look-and-feel states it as a shift of whatever surface it sits on, and the
|
||||
* menu bar resolves it against the window, so this does too — a row hover and a menu
|
||||
* hover then carry the very same colour.
|
||||
*/
|
||||
public static Color hover() {
|
||||
return FlatUIUtils.deriveColor(ui("MenuBar.hoverBackground", 0xE6E6E6, 0x3D3D3D), windowBg());
|
||||
}
|
||||
|
||||
public static Color chatBg() {
|
||||
return ui("TextPane.background", 0xFAFAFA, 0x2B2B2B);
|
||||
return ui("TextPane.background", 0xFAFAFA, 0x1E1E1E);
|
||||
}
|
||||
|
||||
public static Color border() {
|
||||
return ui("Component.borderColor", 0xD0D0D0, 0x4B4B4B);
|
||||
return ui("Component.borderColor", 0xD0D0D0, 0x3E3E3E);
|
||||
}
|
||||
|
||||
public static Color chatText() {
|
||||
return ui("TextPane.foreground", 0x202020, 0xDFE1E5);
|
||||
return ui("TextPane.foreground", 0x202020, 0xE0E0E0);
|
||||
}
|
||||
|
||||
// ---- status colours ----
|
||||
|
||||
public static Color channelText() {
|
||||
return pick(0x21486B, 0xFFFFFF);
|
||||
}
|
||||
|
||||
/** The colour of a chat line about what happened on the server. */
|
||||
public static Color chatEvent() {
|
||||
return pick(0x21486B, 0x8FBCE6);
|
||||
}
|
||||
|
||||
public static Color serverText() {
|
||||
return pick(0x123456, 0xB9D4F0);
|
||||
return pick(0x123456, 0xDCE8F5);
|
||||
}
|
||||
|
||||
public static Color talking() {
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
#
|
||||
# A darker dark theme than FlatLaf's own, keyed to the toolbar's #282828.
|
||||
#
|
||||
# Three levels of surface: the window chrome (toolbar, panels, status bar) at
|
||||
# @background, the tab strips one step below it, and the content the client shows
|
||||
# (channel tree, chat log, text fields, lists) one step below those.
|
||||
#
|
||||
|
||||
@background = #282828
|
||||
@foreground = #e0e0e0
|
||||
@componentBackground = #1e1e1e
|
||||
@menuBackground = #232323
|
||||
|
||||
# Selected channel tree row, and every other selection with it.
|
||||
@selectionBackground = #364b6f
|
||||
|
||||
TabbedPane.background = #232323
|
||||
@@ -0,0 +1,14 @@
|
||||
#
|
||||
# Look-and-feel defaults for both themes. Loaded by FlatLaf itself, on top of the
|
||||
# theme's own properties; see LookAndFeelManager.
|
||||
#
|
||||
|
||||
# Softer corners than FlatLaf's default, and a tab strip with room to breathe.
|
||||
Component.arc = 6
|
||||
Button.arc = 6
|
||||
Component.focusWidth = 1
|
||||
TabbedPane.tabHeight = 26
|
||||
|
||||
# A read-only editor pane is a chat log or an information view here, not a disabled
|
||||
# input, so it keeps the content background instead of the window's.
|
||||
EditorPane.inactiveBackground = @componentBackground
|
||||
Reference in New Issue
Block a user