mirror of
https://github.com/DSheirer/sdrtrunk.git
synced 2026-11-09 15:17:24 -05:00
Add preset themes selector under User Preferences > Application
Replace the single Dark Mode toggle with a curated selection of 9 preset themes, exposed as a vertical radio-button group: Light IntelliJ Nord Dark Darcula Dracula Solarized Light Solarized Dark Monokai Pro Each Theme enum value pairs a human-readable display name with a FlatLaf class name and a dark flag. The first four come from the core flatlaf dependency; the others come from flatlaf-intellij-themes which is added as a new implementation dependency (~1MB). ApplicationPreference now stores the selected theme by enum name under the new ui.theme key. isDarkMode() is retained as a compatibility shim that delegates to the current theme. Existing users with the legacy dark.mode boolean are auto-migrated on first read. ThemeManager is refactored to drive the LAF from the Theme: - Reflectively instantiates the FlatLaf class. - Reads Panel.background, Button.background, TextField.background, List.selectionBackground/Foreground, Label.foreground/disabledForeground, Component.borderColor and Table.alternateRowColor from UIManager *after* FlatLaf installs, then seeds those values into the JIDE-specific keys. Each theme automatically contributes its own palette to JIDE widgets without per-theme hardcoded colours. ApplicationPreferenceEditor swaps the ToggleSwitch for a ToggleGroup of RadioButtons that persist the selection through ApplicationPreference.setTheme.
This commit is contained in:
@@ -94,6 +94,7 @@ dependencies {
|
||||
implementation 'com.fasterxml.jackson.dataformat:jackson-dataformat-xml:2.20.1'
|
||||
implementation 'com.fazecast:jSerialComm:2.11.4'
|
||||
implementation 'com.formdev:flatlaf:3.5.4'
|
||||
implementation 'com.formdev:flatlaf-intellij-themes:3.5.4'
|
||||
implementation 'com.github.jiconfont:jiconfont-font_awesome:4.7.0.1'
|
||||
implementation 'com.github.jiconfont:jiconfont-javafx:1.0.0'
|
||||
implementation 'com.github.jiconfont:jiconfont-swing:1.0.1'
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/*
|
||||
* *****************************************************************************
|
||||
* Copyright (C) 2014-2024 Dennis Sheirer
|
||||
* Copyright (C) 2014-2026 Dennis Sheirer
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
@@ -19,14 +19,17 @@
|
||||
|
||||
package io.github.dsheirer.gui.preference.application;
|
||||
|
||||
import io.github.dsheirer.gui.theme.Theme;
|
||||
import io.github.dsheirer.preference.UserPreferences;
|
||||
import io.github.dsheirer.preference.application.ApplicationPreference;
|
||||
import javafx.geometry.HPos;
|
||||
import javafx.geometry.Insets;
|
||||
import javafx.geometry.Orientation;
|
||||
import javafx.scene.control.Label;
|
||||
import javafx.scene.control.RadioButton;
|
||||
import javafx.scene.control.Separator;
|
||||
import javafx.scene.control.Spinner;
|
||||
import javafx.scene.control.ToggleGroup;
|
||||
import javafx.scene.layout.ColumnConstraints;
|
||||
import javafx.scene.layout.GridPane;
|
||||
import javafx.scene.layout.HBox;
|
||||
@@ -45,7 +48,7 @@ public class ApplicationPreferenceEditor extends HBox
|
||||
private Label mAutoStartTimeoutLabel;
|
||||
private Spinner<Integer> mTimeoutSpinner;
|
||||
private ToggleSwitch mAutomaticDiagnosticMonitoringToggle;
|
||||
private ToggleSwitch mDarkModeToggle;
|
||||
private VBox mThemeRadioGroup;
|
||||
|
||||
/**
|
||||
* Constructs an instance
|
||||
@@ -75,11 +78,9 @@ public class ApplicationPreferenceEditor extends HBox
|
||||
mEditorPane.setHgap(3);
|
||||
mEditorPane.setPadding(new Insets(10, 10, 10, 10));
|
||||
|
||||
Label appearanceLabel = new Label("Appearance");
|
||||
mEditorPane.add(appearanceLabel, 0, row, 2, 1);
|
||||
GridPane.setHalignment(getDarkModeToggle(), HPos.RIGHT);
|
||||
mEditorPane.add(getDarkModeToggle(), 0, ++row);
|
||||
mEditorPane.add(new Label("Dark Mode"), 1, row, 2, 1);
|
||||
Label appearanceLabel = new Label("Theme");
|
||||
mEditorPane.add(appearanceLabel, 0, row, 3, 1);
|
||||
mEditorPane.add(getThemeRadioGroup(), 0, ++row, 3, 1);
|
||||
|
||||
Separator appearanceSeparator = new Separator(Orientation.HORIZONTAL);
|
||||
GridPane.setHgrow(appearanceSeparator, Priority.ALWAYS);
|
||||
@@ -152,18 +153,35 @@ public class ApplicationPreferenceEditor extends HBox
|
||||
}
|
||||
|
||||
/**
|
||||
* Toggle switch to enable/disable dark mode.
|
||||
* Vertical group of radio buttons - one per {@link Theme}. Selecting one persists the theme
|
||||
* via {@link ApplicationPreference#setTheme(Theme)} which triggers a global preference event
|
||||
* and a re-theme through the ThemeManager.
|
||||
*/
|
||||
private ToggleSwitch getDarkModeToggle()
|
||||
private VBox getThemeRadioGroup()
|
||||
{
|
||||
if(mDarkModeToggle == null)
|
||||
if(mThemeRadioGroup == null)
|
||||
{
|
||||
mDarkModeToggle = new ToggleSwitch();
|
||||
mDarkModeToggle.setSelected(mApplicationPreference.isDarkMode());
|
||||
mDarkModeToggle.selectedProperty().addListener((observable, oldValue, enabled) ->
|
||||
mApplicationPreference.setDarkMode(enabled));
|
||||
mThemeRadioGroup = new VBox(4);
|
||||
mThemeRadioGroup.setPadding(new Insets(2, 0, 2, 4));
|
||||
ToggleGroup group = new ToggleGroup();
|
||||
Theme current = mApplicationPreference.getTheme();
|
||||
|
||||
for(Theme theme: Theme.values())
|
||||
{
|
||||
RadioButton rb = new RadioButton(theme.getDisplayName());
|
||||
rb.setToggleGroup(group);
|
||||
rb.setUserData(theme);
|
||||
rb.setSelected(theme == current);
|
||||
rb.setOnAction(e -> {
|
||||
if(rb.isSelected())
|
||||
{
|
||||
mApplicationPreference.setTheme((Theme) rb.getUserData());
|
||||
}
|
||||
});
|
||||
mThemeRadioGroup.getChildren().add(rb);
|
||||
}
|
||||
}
|
||||
|
||||
return mDarkModeToggle;
|
||||
return mThemeRadioGroup;
|
||||
}
|
||||
}
|
||||
|
||||
97
src/main/java/io/github/dsheirer/gui/theme/Theme.java
Normal file
97
src/main/java/io/github/dsheirer/gui/theme/Theme.java
Normal file
@@ -0,0 +1,97 @@
|
||||
/*
|
||||
* *****************************************************************************
|
||||
* Copyright (C) 2014-2026 Dennis Sheirer
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>
|
||||
* ****************************************************************************
|
||||
*/
|
||||
|
||||
package io.github.dsheirer.gui.theme;
|
||||
|
||||
/**
|
||||
* Curated preset themes for the application.
|
||||
*
|
||||
* <p>Each theme pairs a FlatLaf Swing look-and-feel class with a flag indicating whether the
|
||||
* palette is dark. The dark flag drives whether the dark JavaFX stylesheet is applied to
|
||||
* registered Scenes. Other per-theme values (colours used to seed JIDE-specific UIManager
|
||||
* keys) are read at runtime from {@link javax.swing.UIManager} after the LAF installs, so each
|
||||
* theme automatically contributes its own palette to JIDE components.
|
||||
*/
|
||||
public enum Theme
|
||||
{
|
||||
LIGHT("Light", "com.formdev.flatlaf.FlatLightLaf", false),
|
||||
DARK("Dark", "com.formdev.flatlaf.FlatDarkLaf", true),
|
||||
INTELLIJ("IntelliJ", "com.formdev.flatlaf.FlatIntelliJLaf", false),
|
||||
DARCULA("Darcula", "com.formdev.flatlaf.FlatDarculaLaf", true),
|
||||
NORD("Nord", "com.formdev.flatlaf.intellijthemes.FlatNordIJTheme", true),
|
||||
DRACULA("Dracula", "com.formdev.flatlaf.intellijthemes.FlatDraculaIJTheme", true),
|
||||
SOLARIZED_LIGHT("Solarized Light", "com.formdev.flatlaf.intellijthemes.FlatSolarizedLightIJTheme", false),
|
||||
SOLARIZED_DARK("Solarized Dark", "com.formdev.flatlaf.intellijthemes.FlatSolarizedDarkIJTheme", true),
|
||||
MONOKAI_PRO("Monokai Pro", "com.formdev.flatlaf.intellijthemes.FlatMonokaiProIJTheme", true);
|
||||
|
||||
private final String mDisplayName;
|
||||
private final String mLafClassName;
|
||||
private final boolean mDark;
|
||||
|
||||
Theme(String displayName, String lafClassName, boolean dark)
|
||||
{
|
||||
mDisplayName = displayName;
|
||||
mLafClassName = lafClassName;
|
||||
mDark = dark;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return human-readable name shown in the preferences UI.
|
||||
*/
|
||||
public String getDisplayName()
|
||||
{
|
||||
return mDisplayName;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return fully qualified FlatLaf class name to instantiate via reflection.
|
||||
*/
|
||||
public String getLafClassName()
|
||||
{
|
||||
return mLafClassName;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return true if this theme is a dark palette - the JavaFX dark stylesheet is applied for
|
||||
* dark themes and removed for light themes.
|
||||
*/
|
||||
public boolean isDark()
|
||||
{
|
||||
return mDark;
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve a theme by its enum name, returning {@link #LIGHT} as a safe default if {@code name}
|
||||
* is null or unrecognised.
|
||||
*/
|
||||
public static Theme fromName(String name)
|
||||
{
|
||||
if(name != null)
|
||||
{
|
||||
for(Theme t: values())
|
||||
{
|
||||
if(t.name().equals(name))
|
||||
{
|
||||
return t;
|
||||
}
|
||||
}
|
||||
}
|
||||
return LIGHT;
|
||||
}
|
||||
}
|
||||
@@ -19,7 +19,6 @@
|
||||
|
||||
package io.github.dsheirer.gui.theme;
|
||||
|
||||
import com.formdev.flatlaf.FlatDarkLaf;
|
||||
import com.formdev.flatlaf.FlatLightLaf;
|
||||
import com.google.common.eventbus.Subscribe;
|
||||
import com.jidesoft.plaf.LookAndFeelFactory;
|
||||
@@ -45,14 +44,23 @@ import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
/**
|
||||
* Centralized light/dark theme manager.
|
||||
* Centralized theme manager that drives both Swing and JavaFX appearance from the user
|
||||
* preference selection.
|
||||
*
|
||||
* <p>Tracks every JavaFX {@link Scene} that opts in and applies (or removes) the dark stylesheet
|
||||
* when the user toggles the {@code Dark Mode} preference. Also drives Swing look-and-feel
|
||||
* via {@link FlatDarkLaf} / {@link FlatLightLaf} so the main Swing window and all child windows
|
||||
* follow the same setting.
|
||||
*
|
||||
* <p>Subscribes to the global event bus for {@link PreferenceType#APPLICATION} updates.
|
||||
* <p>Each {@link Theme} pairs a FlatLaf look-and-feel class with a dark/light flag. When a
|
||||
* theme is applied:
|
||||
* <ul>
|
||||
* <li>The Swing LAF is installed (reflected from {@link Theme#getLafClassName()}).</li>
|
||||
* <li>JIDE's LAF extension is reinstalled with an explicit Metal LAF for type detection so
|
||||
* JIDE's compile-time {@code WindowsLookAndFeel} {@code instanceof} checks do not hit
|
||||
* the missing-class branch on Linux/macOS JDKs.</li>
|
||||
* <li>FlatLaf's standard defaults are re-applied on top of JIDE's clobber, and a set of
|
||||
* JIDE-specific colour keys (JideTabbedPane.*, JideSplitPane.*, JideButton.*) are seeded
|
||||
* from FlatLaf's just-installed values so each theme's palette propagates to JIDE widgets
|
||||
* automatically.</li>
|
||||
* <li>The dark JavaFX stylesheet is applied to (or removed from) every registered Scene
|
||||
* depending on the theme's dark flag.</li>
|
||||
* </ul>
|
||||
*/
|
||||
public class ThemeManager
|
||||
{
|
||||
@@ -64,7 +72,7 @@ public class ThemeManager
|
||||
private final Set<WeakReference<java.awt.Component>> mSwingRoots = new LinkedHashSet<>();
|
||||
private final String mDarkStylesheetUrl;
|
||||
private volatile UserPreferences mUserPreferences;
|
||||
private volatile boolean mDarkMode;
|
||||
private volatile Theme mCurrentTheme = Theme.LIGHT;
|
||||
|
||||
private ThemeManager()
|
||||
{
|
||||
@@ -73,14 +81,11 @@ public class ThemeManager
|
||||
|
||||
if(mDarkStylesheetUrl == null)
|
||||
{
|
||||
mLog.warn("Dark stylesheet [{}] not found on classpath - dark mode will only affect Swing.",
|
||||
mLog.warn("Dark stylesheet [{}] not found on classpath - dark themes will only affect Swing.",
|
||||
DARK_STYLESHEET);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @return the singleton instance.
|
||||
*/
|
||||
public static ThemeManager getInstance()
|
||||
{
|
||||
return INSTANCE;
|
||||
@@ -89,8 +94,6 @@ public class ThemeManager
|
||||
/**
|
||||
* Bind the theme manager to the user preferences and apply the current preference immediately.
|
||||
* Safe to call multiple times - subsequent calls update the reference and re-apply.
|
||||
*
|
||||
* @param userPreferences source of the dark-mode preference.
|
||||
*/
|
||||
public void initialize(UserPreferences userPreferences)
|
||||
{
|
||||
@@ -101,30 +104,32 @@ public class ThemeManager
|
||||
|
||||
boolean firstBind = (mUserPreferences == null);
|
||||
mUserPreferences = userPreferences;
|
||||
mDarkMode = userPreferences.getApplicationPreference().isDarkMode();
|
||||
mCurrentTheme = userPreferences.getApplicationPreference().getTheme();
|
||||
|
||||
if(firstBind)
|
||||
{
|
||||
MyEventBus.getGlobalEventBus().register(this);
|
||||
}
|
||||
|
||||
applySwingLookAndFeel(mDarkMode);
|
||||
applySwingLookAndFeel(mCurrentTheme);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return current dark-mode state.
|
||||
* @return the currently active theme.
|
||||
*/
|
||||
public Theme getCurrentTheme()
|
||||
{
|
||||
return mCurrentTheme;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return true if the currently active theme is dark.
|
||||
*/
|
||||
public boolean isDarkMode()
|
||||
{
|
||||
return mDarkMode;
|
||||
return mCurrentTheme != null && mCurrentTheme.isDark();
|
||||
}
|
||||
|
||||
/**
|
||||
* Register a Scene to receive theme updates. The Scene is held via {@link WeakReference}
|
||||
* so callers do not need to unregister - garbage-collected Scenes are pruned on the next pass.
|
||||
*
|
||||
* @param scene to track.
|
||||
*/
|
||||
public void register(Scene scene)
|
||||
{
|
||||
if(scene == null)
|
||||
@@ -138,16 +143,15 @@ public class ThemeManager
|
||||
mScenes.add(new WeakReference<>(scene));
|
||||
}
|
||||
|
||||
applyToScene(scene, mDarkMode);
|
||||
applyToScene(scene, isDarkMode());
|
||||
}
|
||||
|
||||
/**
|
||||
* Register a Swing component that may be temporarily detached from any Window's component tree
|
||||
* (e.g. swappable right-pane components in the Channel tab). Plain
|
||||
* {@link SwingUtilities#updateComponentTreeUI(java.awt.Component)} walks of
|
||||
* {@link Window#getWindows()} miss such components and they end up carrying stale colours from
|
||||
* the LAF that was active when they were last attached. Registered components get
|
||||
* {@code updateComponentTreeUI} called on them whenever the theme is applied.
|
||||
* (e.g. swappable right-pane components in the Channel tab). Registered components get
|
||||
* {@link SwingUtilities#updateComponentTreeUI(java.awt.Component)} called on them whenever the
|
||||
* theme is applied, so they pick up the current palette even if they weren't attached when the
|
||||
* user toggled.
|
||||
*/
|
||||
public void registerSwing(java.awt.Component component)
|
||||
{
|
||||
@@ -171,10 +175,6 @@ public class ThemeManager
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Receives notification when any preference changes. Re-reads the dark-mode setting and
|
||||
* propagates if it differs from the last known value.
|
||||
*/
|
||||
@Subscribe
|
||||
public void onPreferenceUpdated(PreferenceType type)
|
||||
{
|
||||
@@ -183,20 +183,20 @@ public class ThemeManager
|
||||
return;
|
||||
}
|
||||
|
||||
boolean updated = mUserPreferences.getApplicationPreference().isDarkMode();
|
||||
Theme updated = mUserPreferences.getApplicationPreference().getTheme();
|
||||
|
||||
if(updated == mDarkMode)
|
||||
if(updated == mCurrentTheme)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
mDarkMode = updated;
|
||||
mCurrentTheme = updated;
|
||||
applyAll(updated);
|
||||
}
|
||||
|
||||
private void applyAll(boolean darkMode)
|
||||
private void applyAll(Theme theme)
|
||||
{
|
||||
applySwingLookAndFeel(darkMode);
|
||||
applySwingLookAndFeel(theme);
|
||||
|
||||
Runnable fxApply = () -> {
|
||||
synchronized(mScenes)
|
||||
@@ -211,19 +211,17 @@ public class ThemeManager
|
||||
}
|
||||
else
|
||||
{
|
||||
applyToScene(scene, darkMode);
|
||||
applyToScene(scene, theme.isDark());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//Also walk every currently-shown JavaFX window so that dialogs, alerts and
|
||||
//popups that were not explicitly registered still pick up the theme.
|
||||
for(javafx.stage.Window window: javafx.stage.Window.getWindows())
|
||||
{
|
||||
Scene scene = window.getScene();
|
||||
if(scene != null)
|
||||
{
|
||||
applyToScene(scene, darkMode);
|
||||
applyToScene(scene, theme.isDark());
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -270,21 +268,19 @@ public class ThemeManager
|
||||
}
|
||||
}
|
||||
|
||||
private void applySwingLookAndFeel(boolean darkMode)
|
||||
private void applySwingLookAndFeel(Theme theme)
|
||||
{
|
||||
//Install the LAF synchronously on the calling thread so that any Swing components
|
||||
//constructed immediately afterwards (in particular the main JFrame) find a fully
|
||||
//populated UIDefaults set. UIManager.setLookAndFeel is safe to call before the EDT
|
||||
//is running.
|
||||
LookAndFeel newLaf;
|
||||
//Install the LAF synchronously on the calling thread so any Swing components constructed
|
||||
//immediately afterwards (in particular the main JFrame) find a fully populated UIDefaults
|
||||
//set. UIManager.setLookAndFeel is safe to call before the EDT is running.
|
||||
LookAndFeel newLaf = instantiate(theme);
|
||||
try
|
||||
{
|
||||
newLaf = darkMode ? new FlatDarkLaf() : new FlatLightLaf();
|
||||
UIManager.setLookAndFeel(newLaf);
|
||||
}
|
||||
catch(Exception e)
|
||||
{
|
||||
mLog.error("Unable to apply Swing look-and-feel for dark mode = " + darkMode, e);
|
||||
mLog.error("Unable to apply Swing look-and-feel for theme = " + theme, e);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -292,8 +288,8 @@ public class ThemeManager
|
||||
{
|
||||
//JIDE 3.6.18 has compile-time `instanceof WindowsLookAndFeel` checks throughout
|
||||
//LookAndFeelFactory. The 3-arg overload lets us hand JIDE an explicit Metal LAF
|
||||
//instance for type detection while leaving the real (FlatLaf) defaults table in
|
||||
//place; JIDE's Metal branch matches first.
|
||||
//instance for type detection while leaving the real (FlatLaf) defaults table in place;
|
||||
//JIDE's Metal branch matches first.
|
||||
LookAndFeelFactory.installJideExtension(UIManager.getLookAndFeelDefaults(),
|
||||
new MetalLookAndFeel(), LookAndFeelFactory.VSNET_STYLE);
|
||||
}
|
||||
@@ -303,12 +299,12 @@ public class ThemeManager
|
||||
}
|
||||
|
||||
//JIDE's Metal-flavored extension overwrites FlatLaf's color/font/border defaults for
|
||||
//standard Swing component keys. Re-apply FlatLaf's defaults on top of JIDE's install
|
||||
//so the inner panels pick up dark backgrounds. JIDE's UI delegate registrations live
|
||||
//under Jide-prefixed keys that FlatLaf does not define, so they survive untouched.
|
||||
//standard Swing component keys. Re-apply FlatLaf's defaults on top of JIDE's install so
|
||||
//the inner panels pick up the right colours. JIDE's UI delegate registrations live under
|
||||
//Jide-prefixed keys that FlatLaf does not define, so they survive untouched.
|
||||
try
|
||||
{
|
||||
UIDefaults flatFresh = (darkMode ? new FlatDarkLaf() : new FlatLightLaf()).getDefaults();
|
||||
UIDefaults flatFresh = instantiate(theme).getDefaults();
|
||||
UIDefaults active = UIManager.getLookAndFeelDefaults();
|
||||
for(java.util.Map.Entry<Object, Object> entry: flatFresh.entrySet())
|
||||
{
|
||||
@@ -332,17 +328,14 @@ public class ThemeManager
|
||||
mLog.warn("Unable to re-apply FlatLaf defaults after JIDE install", t);
|
||||
}
|
||||
|
||||
//FlatLaf's per-component foreground/background lookups go through derived/lazy values
|
||||
//that JIDE's Metal initializer was also feeding from, so even after the defaults copy
|
||||
//above some labels and buttons end up with Metal-era dark foregrounds on a dark
|
||||
//background. Brute-force the standard component colour keys via UIManager.put which
|
||||
//writes to the user-defaults layer that takes precedence over the LAF defaults. Use
|
||||
//near-white rather than FlatLaf's #bbbbbb so text reads sharply on the dark panels.
|
||||
applyExplicitOverrides(darkMode);
|
||||
//Brute-force the standard component colour keys via UIManager.put which writes to the
|
||||
//user-defaults layer that takes precedence over the LAF defaults. Colours are read from
|
||||
//the FlatLaf-installed defaults so each theme contributes its own palette.
|
||||
applyExplicitOverrides(theme);
|
||||
|
||||
//Re-rendering any already-realized Swing windows must happen on the EDT. Also walk any
|
||||
//Swing components explicitly registered via registerSwing(...) - they may be detached
|
||||
//from any Window's tree at toggle time (e.g. swappable channel-tab right components).
|
||||
//Re-rendering already-realized Swing windows must happen on the EDT. Also walk any Swing
|
||||
//components explicitly registered via registerSwing(...) - they may be detached from any
|
||||
//Window's tree at toggle time (e.g. swappable channel-tab right components).
|
||||
Runnable updateRealized = () -> {
|
||||
for(Window window: Window.getWindows())
|
||||
{
|
||||
@@ -377,44 +370,57 @@ public class ThemeManager
|
||||
}
|
||||
}
|
||||
|
||||
private void applyExplicitOverrides(boolean darkMode)
|
||||
/**
|
||||
* Reflectively instantiate the FlatLaf class for the given theme. Falls back to
|
||||
* {@link FlatLightLaf} if the class is missing - that should not happen in a normal build but
|
||||
* keeps the app usable if a theme jar is removed.
|
||||
*/
|
||||
private LookAndFeel instantiate(Theme theme)
|
||||
{
|
||||
//Colors stored under UIManager keys must be ColorUIResource (or null) for
|
||||
//LookAndFeel.installColorsAndFont() to overwrite a component's existing colour
|
||||
//on subsequent updateComponentTreeUI calls. Using a plain java.awt.Color pins
|
||||
//the component foreground/background permanently and prevents the theme from
|
||||
//ever toggling back.
|
||||
ColorUIResource bgPanel;
|
||||
ColorUIResource bgRaised;
|
||||
ColorUIResource bgInput;
|
||||
ColorUIResource bgSelection;
|
||||
ColorUIResource fgPrimary;
|
||||
ColorUIResource fgSelection;
|
||||
ColorUIResource fgDisabled;
|
||||
ColorUIResource border;
|
||||
|
||||
if(darkMode)
|
||||
try
|
||||
{
|
||||
bgPanel = new ColorUIResource(0x2b2b2b);
|
||||
bgRaised = new ColorUIResource(0x3c3f41);
|
||||
bgInput = new ColorUIResource(0x313335);
|
||||
bgSelection = new ColorUIResource(0x214283);
|
||||
fgPrimary = new ColorUIResource(0xe6e6e6);
|
||||
fgSelection = new ColorUIResource(Color.WHITE);
|
||||
fgDisabled = new ColorUIResource(0x6a6a6a);
|
||||
border = new ColorUIResource(0x4f5356);
|
||||
Class<?> cls = Class.forName(theme.getLafClassName());
|
||||
return (LookAndFeel) cls.getDeclaredConstructor().newInstance();
|
||||
}
|
||||
else
|
||||
catch(Throwable t)
|
||||
{
|
||||
//Light mode mirrors FlatLightLaf's palette.
|
||||
bgPanel = new ColorUIResource(0xf2f2f2);
|
||||
bgRaised = new ColorUIResource(0xffffff);
|
||||
bgInput = new ColorUIResource(0xffffff);
|
||||
bgSelection = new ColorUIResource(0x2675bf);
|
||||
fgPrimary = new ColorUIResource(0x1e1e1e);
|
||||
fgSelection = new ColorUIResource(Color.WHITE);
|
||||
fgDisabled = new ColorUIResource(0x8c8c8c);
|
||||
border = new ColorUIResource(0xc4c4c4);
|
||||
mLog.error("Unable to instantiate LAF [{}] for theme {} - falling back to FlatLightLaf",
|
||||
theme.getLafClassName(), theme, t);
|
||||
return new FlatLightLaf();
|
||||
}
|
||||
}
|
||||
|
||||
private void applyExplicitOverrides(Theme theme)
|
||||
{
|
||||
boolean darkMode = theme.isDark();
|
||||
|
||||
//Read the FlatLaf-installed colours so each theme contributes its own palette to the
|
||||
//JIDE-specific keys and the user-defaults overrides. Fall back to reasonable defaults if
|
||||
//a key is missing.
|
||||
ColorUIResource bgPanel = uir(UIManager.getColor("Panel.background"),
|
||||
darkMode ? 0x2b2b2b : 0xf2f2f2);
|
||||
ColorUIResource bgRaised = uir(UIManager.getColor("Button.background"),
|
||||
darkMode ? 0x3c3f41 : 0xffffff);
|
||||
ColorUIResource bgInput = uir(UIManager.getColor("TextField.background"),
|
||||
darkMode ? 0x313335 : 0xffffff);
|
||||
ColorUIResource bgSelection = uir(UIManager.getColor("List.selectionBackground"),
|
||||
darkMode ? 0x214283 : 0x2675bf);
|
||||
ColorUIResource fgPrimary = uir(UIManager.getColor("Label.foreground"),
|
||||
darkMode ? 0xe6e6e6 : 0x1e1e1e);
|
||||
ColorUIResource fgSelection = uir(UIManager.getColor("List.selectionForeground"),
|
||||
0xffffff);
|
||||
ColorUIResource fgDisabled = uir(UIManager.getColor("Label.disabledForeground"),
|
||||
darkMode ? 0x6a6a6a : 0x8c8c8c);
|
||||
ColorUIResource border = uir(UIManager.getColor("Component.borderColor"),
|
||||
darkMode ? 0x4f5356 : 0xc4c4c4);
|
||||
ColorUIResource altRow = uir(UIManager.getColor("Table.alternateRowColor"),
|
||||
darkMode ? 0x353739 : 0xfafafa);
|
||||
|
||||
//Use a slightly brighter foreground than FlatLaf's default in dark mode so text reads
|
||||
//sharper on the JIDE-flavoured surfaces. Light themes keep their own contrast.
|
||||
if(darkMode && fgPrimary.getRed() < 0xd0)
|
||||
{
|
||||
fgPrimary = new ColorUIResource(0xe6e6e6);
|
||||
}
|
||||
|
||||
String[] simpleComponents = {
|
||||
@@ -434,13 +440,11 @@ public class ThemeManager
|
||||
UIManager.put(c + ".disabledText", fgDisabled);
|
||||
}
|
||||
|
||||
//Buttons sit on a slightly raised tone than the panel they live on.
|
||||
for(String c: new String[]{"Button", "ToggleButton", "ComboBox", "Spinner"})
|
||||
{
|
||||
UIManager.put(c + ".background", bgRaised);
|
||||
}
|
||||
|
||||
//Text inputs use their own background tone for affordance.
|
||||
for(String c: new String[]{"TextField", "TextArea", "TextPane", "EditorPane",
|
||||
"FormattedTextField", "PasswordField"})
|
||||
{
|
||||
@@ -452,7 +456,6 @@ public class ThemeManager
|
||||
UIManager.put(c + ".selectionForeground", fgSelection);
|
||||
}
|
||||
|
||||
//Tables, trees and lists.
|
||||
for(String c: new String[]{"Table", "Tree", "List"})
|
||||
{
|
||||
UIManager.put(c + ".background", bgInput);
|
||||
@@ -462,15 +465,15 @@ public class ThemeManager
|
||||
}
|
||||
|
||||
UIManager.put("Table.gridColor", border);
|
||||
UIManager.put("Table.alternateRowColor", new ColorUIResource(darkMode ? 0x353739 : 0xfafafa));
|
||||
UIManager.put("Table.alternateRowColor", altRow);
|
||||
UIManager.put("TableHeader.background", bgRaised);
|
||||
UIManager.put("TableHeader.foreground", fgPrimary);
|
||||
UIManager.put("TableHeader.cellBorder", javax.swing.BorderFactory.createMatteBorder(0, 0, 1, 1, border));
|
||||
UIManager.put("TableHeader.cellBorder",
|
||||
javax.swing.BorderFactory.createMatteBorder(0, 0, 1, 1, border));
|
||||
|
||||
//Tab labels read black-on-dark with the JIDE-flavoured TabbedPane otherwise.
|
||||
//Selected tabs sit on the raised surface (white in light mode, dark gray in dark mode),
|
||||
//not on the blue selection background, so the selected-tab text is primary foreground -
|
||||
//not the white selection foreground that is right for list/table row selections.
|
||||
//Tab labels: selected tabs sit on the raised surface (which is white in light themes),
|
||||
//not on the blue selection background, so the selected-tab text is the primary
|
||||
//foreground - not the white selection foreground that is right for row selections.
|
||||
UIManager.put("TabbedPane.foreground", fgPrimary);
|
||||
UIManager.put("TabbedPane.background", bgPanel);
|
||||
UIManager.put("TabbedPane.selectedForeground", fgPrimary);
|
||||
@@ -479,13 +482,11 @@ public class ThemeManager
|
||||
UIManager.put("TabbedPane.darkShadow", border);
|
||||
UIManager.put("TabbedPane.shadow", border);
|
||||
|
||||
//Menu hover and selected states.
|
||||
UIManager.put("MenuItem.selectionBackground", bgSelection);
|
||||
UIManager.put("MenuItem.selectionForeground", fgSelection);
|
||||
UIManager.put("Menu.selectionBackground", bgSelection);
|
||||
UIManager.put("Menu.selectionForeground", fgSelection);
|
||||
|
||||
//Borders and split-pane divider colour.
|
||||
UIManager.put("SplitPane.background", bgPanel);
|
||||
UIManager.put("SplitPane.dividerFocusColor", bgSelection);
|
||||
UIManager.put("SplitPaneDivider.draggingColor", bgSelection);
|
||||
@@ -494,16 +495,11 @@ public class ThemeManager
|
||||
UIManager.put("Separator.foreground", border);
|
||||
UIManager.put("Separator.background", bgPanel);
|
||||
|
||||
//ToolTip needs to stand out from the panel.
|
||||
UIManager.put("ToolTip.background", bgRaised);
|
||||
UIManager.put("ToolTip.foreground", fgPrimary);
|
||||
|
||||
//JIDE components (JideTabbedPane, JideSplitPane, JideButton, etc.) read their colors
|
||||
//from JIDE-specific UIManager keys, not the standard Swing keys. Set those too so
|
||||
//tabs and other JIDE widgets render with the same palette.
|
||||
//Selected/active tab text uses fgPrimary because the JIDE tabs sit on the raised
|
||||
//surface, not the blue selection background. In light mode the raised tone is white,
|
||||
//so fgSelection (also white) would render invisible-on-white.
|
||||
//JIDE components read their colours from JIDE-specific UIManager keys, not the standard
|
||||
//Swing keys. Seed those from the same palette so JIDE widgets follow the theme.
|
||||
UIManager.put("JideTabbedPane.background", bgPanel);
|
||||
UIManager.put("JideTabbedPane.foreground", fgPrimary);
|
||||
UIManager.put("JideTabbedPane.tabAreaBackground", bgPanel);
|
||||
@@ -535,6 +531,23 @@ public class ThemeManager
|
||||
UIManager.put("JideLabel.foreground", fgPrimary);
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert a {@link Color} to a {@link ColorUIResource}, falling back to {@code fallbackRgb} if
|
||||
* the input is null. Existing UIResource colours are returned as-is.
|
||||
*/
|
||||
private static ColorUIResource uir(Color c, int fallbackRgb)
|
||||
{
|
||||
if(c == null)
|
||||
{
|
||||
return new ColorUIResource(fallbackRgb);
|
||||
}
|
||||
if(c instanceof ColorUIResource cuir)
|
||||
{
|
||||
return cuir;
|
||||
}
|
||||
return new ColorUIResource(c);
|
||||
}
|
||||
|
||||
private void pruneDeadReferencesAndCheckPresent(Scene scene)
|
||||
{
|
||||
Iterator<WeakReference<Scene>> it = mScenes.iterator();
|
||||
|
||||
@@ -19,6 +19,7 @@
|
||||
|
||||
package io.github.dsheirer.preference.application;
|
||||
|
||||
import io.github.dsheirer.gui.theme.Theme;
|
||||
import io.github.dsheirer.preference.Preference;
|
||||
import io.github.dsheirer.preference.PreferenceType;
|
||||
import io.github.dsheirer.sample.Listener;
|
||||
@@ -34,12 +35,13 @@ public class ApplicationPreference extends Preference
|
||||
private static final String PREFERENCE_KEY_CHANNEL_AUTO_DIAGNOSTIC_MONITORING = "automatic.diagnostic.monitoring";
|
||||
private static final String PREFERENCE_KEY_CHANNEL_AUTO_START_TIMEOUT = "channel.auto.start.timeout";
|
||||
private static final String PREFERENCE_KEY_DARK_MODE = "dark.mode";
|
||||
private static final String PREFERENCE_KEY_THEME = "ui.theme";
|
||||
|
||||
private final static Logger mLog = LoggerFactory.getLogger(ApplicationPreference.class);
|
||||
private Preferences mPreferences = Preferences.userNodeForPackage(ApplicationPreference.class);
|
||||
private Integer mChannelAutoStartTimeout;
|
||||
private Boolean mAutomaticDiagnosticMonitoring;
|
||||
private Boolean mDarkMode;
|
||||
private Theme mTheme;
|
||||
|
||||
/**
|
||||
* Constructs an instance
|
||||
@@ -108,27 +110,53 @@ public class ApplicationPreference extends Preference
|
||||
}
|
||||
|
||||
/**
|
||||
* Indicates whether dark mode is enabled for the application UI.
|
||||
* @return enabled.
|
||||
* Returns the selected UI theme. Falls back to a sensible default if no preference has been
|
||||
* stored - if the legacy {@code dark.mode} boolean is true the default is {@link Theme#DARK},
|
||||
* otherwise {@link Theme#LIGHT}.
|
||||
*/
|
||||
public boolean isDarkMode()
|
||||
public Theme getTheme()
|
||||
{
|
||||
if(mDarkMode == null)
|
||||
if(mTheme == null)
|
||||
{
|
||||
mDarkMode = mPreferences.getBoolean(PREFERENCE_KEY_DARK_MODE, false);
|
||||
String stored = mPreferences.get(PREFERENCE_KEY_THEME, null);
|
||||
if(stored != null)
|
||||
{
|
||||
mTheme = Theme.fromName(stored);
|
||||
}
|
||||
else
|
||||
{
|
||||
//Migrate from the older boolean preference so existing users keep their setting.
|
||||
mTheme = mPreferences.getBoolean(PREFERENCE_KEY_DARK_MODE, false) ? Theme.DARK : Theme.LIGHT;
|
||||
}
|
||||
}
|
||||
|
||||
return mDarkMode;
|
||||
return mTheme;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the dark mode preference.
|
||||
* @param enabled true to enable dark mode.
|
||||
* Sets the selected UI theme.
|
||||
*/
|
||||
public void setDarkMode(boolean enabled)
|
||||
public void setTheme(Theme theme)
|
||||
{
|
||||
mDarkMode = enabled;
|
||||
mPreferences.putBoolean(PREFERENCE_KEY_DARK_MODE, enabled);
|
||||
if(theme == null)
|
||||
{
|
||||
theme = Theme.LIGHT;
|
||||
}
|
||||
|
||||
mTheme = theme;
|
||||
mPreferences.put(PREFERENCE_KEY_THEME, theme.name());
|
||||
//Keep the legacy boolean in sync so any older code path that reads it still behaves
|
||||
//correctly.
|
||||
mPreferences.putBoolean(PREFERENCE_KEY_DARK_MODE, theme.isDark());
|
||||
notifyPreferenceUpdated();
|
||||
}
|
||||
|
||||
/**
|
||||
* @return true if the currently selected theme is a dark palette. Retained for callers that
|
||||
* only care about the dark/light distinction.
|
||||
*/
|
||||
public boolean isDarkMode()
|
||||
{
|
||||
return getTheme().isDark();
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user