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);
|
scroll = new JScrollPane(log);
|
||||||
|
HtmlStyles.fillViewport(log);
|
||||||
scroll.setBorder(BorderFactory.createEmptyBorder());
|
scroll.setBorder(BorderFactory.createEmptyBorder());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -148,9 +148,9 @@ final class DropIndicatorTree extends JTree {
|
|||||||
g.setColor(getBackground());
|
g.setColor(getBackground());
|
||||||
g.fillRect(clip.x, clip.y, clip.width, clip.height);
|
g.fillRect(clip.x, clip.y, clip.width, clip.height);
|
||||||
paintSelection(g);
|
paintSelection(g);
|
||||||
|
paintHover(g);
|
||||||
super.paintComponent(g);
|
super.paintComponent(g);
|
||||||
paintBadges(g);
|
paintBadges(g);
|
||||||
paintHover(g);
|
|
||||||
JTree.DropLocation loc = getDropLocation();
|
JTree.DropLocation loc = getDropLocation();
|
||||||
if (loc == null || loc.getPath() == null) return;
|
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 —
|
* Washes the row under the pointer across the full width it occupies — the same
|
||||||
* the same area a click on it acts on. Spacers, which nothing can be done with,
|
* area a click on it acts on. A selected row keeps its selection colour, and
|
||||||
* are left alone.
|
* spacers, which nothing can be done with, are left alone.
|
||||||
*/
|
*/
|
||||||
private void paintHover(Graphics g) {
|
private void paintHover(Graphics g) {
|
||||||
if (hoverRow < 0 || getDropLocation() != null) return;
|
if (hoverRow < 0 || getDropLocation() != null || isRowSelected(hoverRow)) return;
|
||||||
Rectangle bounds = getRowBounds(hoverRow);
|
Rectangle bounds = getRowBounds(hoverRow);
|
||||||
TreePath path = getPathForRow(hoverRow);
|
TreePath path = getPathForRow(hoverRow);
|
||||||
if (bounds == null || path == null || isSpacer(path)) return;
|
if (bounds == null || path == null || isSpacer(path)) return;
|
||||||
|
|
||||||
Rectangle visible = getVisibleRect();
|
Rectangle visible = getVisibleRect();
|
||||||
Graphics2D g2 = (Graphics2D) g.create();
|
g.setColor(Theme.hover());
|
||||||
g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
|
g.fillRect(visible.x, bounds.y, visible.width, bounds.height);
|
||||||
g2.setColor(Theme.treeHover());
|
|
||||||
g2.drawRoundRect(visible.x, bounds.y, visible.width - 1, bounds.height - 1, 4, 4);
|
|
||||||
g2.dispose();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private boolean isSpacer(TreePath path) {
|
private boolean isSpacer(TreePath path) {
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ package com.ts3client.ui;
|
|||||||
import com.ts3client.text.BBCode;
|
import com.ts3client.text.BBCode;
|
||||||
|
|
||||||
import javax.swing.JEditorPane;
|
import javax.swing.JEditorPane;
|
||||||
|
import javax.swing.JViewport;
|
||||||
import javax.swing.SwingUtilities;
|
import javax.swing.SwingUtilities;
|
||||||
import javax.swing.text.html.HTMLEditorKit;
|
import javax.swing.text.html.HTMLEditorKit;
|
||||||
import javax.swing.text.html.StyleSheet;
|
import javax.swing.text.html.StyleSheet;
|
||||||
@@ -42,6 +43,17 @@ final class HtmlStyles {
|
|||||||
return pane;
|
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. */
|
/** Re-themes {@code pane}, keeping the text it shows. */
|
||||||
private static void restyle(JEditorPane pane) {
|
private static void restyle(JEditorPane pane) {
|
||||||
// Also runs from JEditorPane's constructor, before the pane is one of ours.
|
// 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.
|
// about what happened on the server.
|
||||||
css.addRule(".muted { color:" + hex(Theme.chatSystem()) + "; }");
|
css.addRule(".muted { color:" + hex(Theme.chatSystem()) + "; }");
|
||||||
css.addRule(".name { color:" + hex(Theme.chatName()) + "; font-weight:bold; }");
|
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; }");
|
css.addRule("a { color:" + hex(Theme.chatName()) + "; text-decoration:none; }");
|
||||||
// Client references look exactly like a message author's name.
|
// Client references look exactly like a message author's name.
|
||||||
css.addRule("a." + BBCode.IDENTITY_LINK_CLASS
|
css.addRule("a." + BBCode.IDENTITY_LINK_CLASS
|
||||||
@@ -83,6 +95,7 @@ final class HtmlStyles {
|
|||||||
kit.setStyleSheet(css);
|
kit.setStyleSheet(css);
|
||||||
|
|
||||||
pane.setEditorKit(kit);
|
pane.setEditorKit(kit);
|
||||||
|
fillViewport(pane);
|
||||||
if (html != null) {
|
if (html != null) {
|
||||||
pane.setText(html);
|
pane.setText(html);
|
||||||
pane.setCaretPosition(atEnd ? pane.getDocument().getLength() : 0);
|
pane.setCaretPosition(atEnd ? pane.getDocument().getLength() : 0);
|
||||||
|
|||||||
@@ -55,6 +55,7 @@ public final class InfoPanel extends JScrollPane {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
setViewportView(pane);
|
setViewportView(pane);
|
||||||
|
HtmlStyles.fillViewport(pane);
|
||||||
clear();
|
clear();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -5,29 +5,34 @@ import com.formdev.flatlaf.FlatLaf;
|
|||||||
import com.formdev.flatlaf.FlatLightLaf;
|
import com.formdev.flatlaf.FlatLightLaf;
|
||||||
import com.ts3client.config.Settings;
|
import com.ts3client.config.Settings;
|
||||||
|
|
||||||
import javax.swing.UIManager;
|
|
||||||
import java.awt.Window;
|
import java.awt.Window;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Installs FlatLaf in the requested variant and keeps {@link Theme} in step with it.
|
* 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
|
* <p>The client's own look-and-feel defaults — rounded controls, and the darker palette
|
||||||
* controls, a slimmer tree row and scroll bars that only show their thumb.
|
* 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 {
|
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() {
|
private LookAndFeelManager() {
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Installs the look-and-feel for {@code appearance}. Call before building any window. */
|
/** Installs the look-and-feel for {@code appearance}. Call before building any window. */
|
||||||
public static void install(Settings.Appearance appearance) {
|
public static void install(Settings.Appearance appearance) {
|
||||||
FlatLaf laf = appearance == Settings.Appearance.DARK ? new FlatDarkLaf() : new FlatLightLaf();
|
FlatLaf laf = appearance == Settings.Appearance.DARK ? new FlatDarkLaf() : new FlatLightLaf();
|
||||||
if (!FlatLaf.setup(laf)) {
|
// Whatever look-and-feel stays installed on failure, Theme falls back to its own palette.
|
||||||
// Leave whatever look-and-feel is installed; Theme falls back to its own palette.
|
FlatLaf.setup(laf);
|
||||||
Theme.refresh();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
applyDefaults();
|
|
||||||
Theme.refresh();
|
Theme.refresh();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -43,17 +48,4 @@ public final class LookAndFeelManager {
|
|||||||
w.repaint();
|
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.setPathEditable(nicknameEditor::editsPath);
|
||||||
tree.setInvokesStopCellEditing(true);
|
tree.setInvokesStopCellEditing(true);
|
||||||
setViewportView(tree);
|
setViewportView(tree);
|
||||||
getViewport().setBackground(Theme.treeBg());
|
|
||||||
// The icon strip is drawn against the viewport's right edge, so the blitted
|
// 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.
|
// pixels a scroll would reuse are stale; repaint the whole viewport instead.
|
||||||
getViewport().setScrollMode(JViewport.SIMPLE_SCROLL_MODE);
|
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());
|
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}. */
|
/** The path of the tree node showing {@code target}, or {@code null}. */
|
||||||
private TreePath pathOf(Object target) {
|
private TreePath pathOf(Object target) {
|
||||||
java.util.Enumeration<?> nodes = root.breadthFirstEnumeration();
|
java.util.Enumeration<?> nodes = root.breadthFirstEnumeration();
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
package com.ts3client.ui;
|
package com.ts3client.ui;
|
||||||
|
|
||||||
|
import com.formdev.flatlaf.ui.FlatUIUtils;
|
||||||
|
|
||||||
import javax.swing.UIManager;
|
import javax.swing.UIManager;
|
||||||
import java.awt.Color;
|
import java.awt.Color;
|
||||||
import java.awt.Font;
|
import java.awt.Font;
|
||||||
@@ -44,11 +46,11 @@ public final class Theme {
|
|||||||
// ---- surfaces, from the look-and-feel ----
|
// ---- surfaces, from the look-and-feel ----
|
||||||
|
|
||||||
public static Color windowBg() {
|
public static Color windowBg() {
|
||||||
return ui("Panel.background", 0xF0F0F0, 0x3C3F41);
|
return ui("Panel.background", 0xF0F0F0, 0x282828);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static Color toolbarBg() {
|
public static Color toolbarBg() {
|
||||||
return ui("ToolBar.background", 0xE6E9ED, 0x3C3F41);
|
return ui("ToolBar.background", 0xE6E9ED, 0x282828);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static Color statusBg() {
|
public static Color statusBg() {
|
||||||
@@ -56,42 +58,52 @@ public final class Theme {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public static Color treeBg() {
|
public static Color treeBg() {
|
||||||
return ui("Tree.background", 0xFFFFFF, 0x2B2B2B);
|
return ui("Tree.background", 0xFFFFFF, 0x1E1E1E);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static Color treeText() {
|
public static Color treeText() {
|
||||||
return ui("Tree.foreground", 0x1E1E1E, 0xDFE1E5);
|
return ui("Tree.foreground", 0x1E1E1E, 0xE0E0E0);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static Color treeSelection() {
|
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() {
|
* Behind the row the pointer is over: the same wash the menu bar puts behind a hovered
|
||||||
return pick(0x9CC4EE, 0x4A6E96);
|
* 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() {
|
public static Color chatBg() {
|
||||||
return ui("TextPane.background", 0xFAFAFA, 0x2B2B2B);
|
return ui("TextPane.background", 0xFAFAFA, 0x1E1E1E);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static Color border() {
|
public static Color border() {
|
||||||
return ui("Component.borderColor", 0xD0D0D0, 0x4B4B4B);
|
return ui("Component.borderColor", 0xD0D0D0, 0x3E3E3E);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static Color chatText() {
|
public static Color chatText() {
|
||||||
return ui("TextPane.foreground", 0x202020, 0xDFE1E5);
|
return ui("TextPane.foreground", 0x202020, 0xE0E0E0);
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---- status colours ----
|
// ---- status colours ----
|
||||||
|
|
||||||
public static Color channelText() {
|
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);
|
return pick(0x21486B, 0x8FBCE6);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static Color serverText() {
|
public static Color serverText() {
|
||||||
return pick(0x123456, 0xB9D4F0);
|
return pick(0x123456, 0xDCE8F5);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static Color talking() {
|
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