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 5aa4a77..b9f96f1 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 @@ -318,6 +318,7 @@ public final class ChatPanel extends JPanel { }); scroll = new JScrollPane(log); + HtmlStyles.fillViewport(log); scroll.setBorder(BorderFactory.createEmptyBorder()); } diff --git a/ts3-client/swing/src/main/java/com/ts3client/ui/DropIndicatorTree.java b/ts3-client/swing/src/main/java/com/ts3client/ui/DropIndicatorTree.java index b5f4846..5295f80 100644 --- a/ts3-client/swing/src/main/java/com/ts3client/ui/DropIndicatorTree.java +++ b/ts3-client/swing/src/main/java/com/ts3client/ui/DropIndicatorTree.java @@ -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) { diff --git a/ts3-client/swing/src/main/java/com/ts3client/ui/HtmlStyles.java b/ts3-client/swing/src/main/java/com/ts3client/ui/HtmlStyles.java index 02cf7c5..83644fd 100644 --- a/ts3-client/swing/src/main/java/com/ts3client/ui/HtmlStyles.java +++ b/ts3-client/swing/src/main/java/com/ts3client/ui/HtmlStyles.java @@ -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); diff --git a/ts3-client/swing/src/main/java/com/ts3client/ui/InfoPanel.java b/ts3-client/swing/src/main/java/com/ts3client/ui/InfoPanel.java index 3db2a84..ff08b2b 100644 --- a/ts3-client/swing/src/main/java/com/ts3client/ui/InfoPanel.java +++ b/ts3-client/swing/src/main/java/com/ts3client/ui/InfoPanel.java @@ -55,6 +55,7 @@ public final class InfoPanel extends JScrollPane { } }); setViewportView(pane); + HtmlStyles.fillViewport(pane); clear(); } diff --git a/ts3-client/swing/src/main/java/com/ts3client/ui/LookAndFeelManager.java b/ts3-client/swing/src/main/java/com/ts3client/ui/LookAndFeelManager.java index da3b83c..68b8a96 100644 --- a/ts3-client/swing/src/main/java/com/ts3client/ui/LookAndFeelManager.java +++ b/ts3-client/swing/src/main/java/com/ts3client/ui/LookAndFeelManager.java @@ -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. * - *
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. + *
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); - } - } diff --git a/ts3-client/swing/src/main/java/com/ts3client/ui/ServerTreePanel.java b/ts3-client/swing/src/main/java/com/ts3client/ui/ServerTreePanel.java index 631961c..3237942 100644 --- a/ts3-client/swing/src/main/java/com/ts3client/ui/ServerTreePanel.java +++ b/ts3-client/swing/src/main/java/com/ts3client/ui/ServerTreePanel.java @@ -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(); diff --git a/ts3-client/swing/src/main/java/com/ts3client/ui/Theme.java b/ts3-client/swing/src/main/java/com/ts3client/ui/Theme.java index d9dbdb6..f7a84b1 100644 --- a/ts3-client/swing/src/main/java/com/ts3client/ui/Theme.java +++ b/ts3-client/swing/src/main/java/com/ts3client/ui/Theme.java @@ -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() { diff --git a/ts3-client/swing/src/main/resources/com/ts3client/ui/laf/FlatDarkLaf.properties b/ts3-client/swing/src/main/resources/com/ts3client/ui/laf/FlatDarkLaf.properties new file mode 100644 index 0000000..1e88f3e --- /dev/null +++ b/ts3-client/swing/src/main/resources/com/ts3client/ui/laf/FlatDarkLaf.properties @@ -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 diff --git a/ts3-client/swing/src/main/resources/com/ts3client/ui/laf/FlatLaf.properties b/ts3-client/swing/src/main/resources/com/ts3client/ui/laf/FlatLaf.properties new file mode 100644 index 0000000..caa854a --- /dev/null +++ b/ts3-client/swing/src/main/resources/com/ts3client/ui/laf/FlatLaf.properties @@ -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