mirror of
https://github.com/DSheirer/sdrtrunk.git
synced 2026-11-09 15:17:24 -05:00
Dark mode (#2411)
* Add dark mode toggle under User Preferences > Application
- Add darkMode boolean preference to ApplicationPreference (persisted via java.util.prefs)
- Add ToggleSwitch labeled "Dark Mode" to ApplicationPreferenceEditor
- Add ThemeManager singleton that listens for preference updates and applies/removes
the dark stylesheet to all registered JavaFX Scenes and swaps the Swing LAF
- Add sdrtrunk_dark.css with dark styling for controls, tables, menus, dialogs, charts
- Add FlatLaf dependency for full Swing dark theming (replaces previous OS-specific
MetalLookAndFeel setup; FlatLaf works cross-platform)
- Register all Stages created in JavaFxWindowManager with the ThemeManager and walk
Window.getWindows() at apply-time so unregistered dialogs/alerts also pick up the theme
- Add Foojay toolchain resolver to settings.gradle so Gradle can auto-provision JDK 25
* Fix dark-mode startup failures on Linux
- Apply Swing LAF synchronously on the calling thread so the main JFrame's RootPane
has a populated UIDefaults set when constructed. Previously the LAF install was
scheduled via EventQueue.invokeLater, so JFrame construction raced ahead and threw
"no ComponentUI class for: javax.swing.JPanel".
- Call LookAndFeelFactory.installJideExtension(VSNET_STYLE) with an explicit style
instead of the no-arg auto-detection variant. The auto-detection path probes
com.sun.java.swing.plaf.windows.WindowsLookAndFeel via Class.forName, which is
absent on non-Windows JDKs and threw ClassNotFoundException at startup.
- Bounce only the SwingUtilities.updateComponentTreeUI walk through the EDT so it
redraws already-realized windows on theme toggle.
* Fix JIDE/Windows-LookAndFeel crash on non-Windows JDKs
JIDE 3.6.18's LookAndFeelFactory contains compile-time
\`instanceof com.sun.java.swing.plaf.windows.WindowsLookAndFeel\` checks in
multiple methods, including a JIDE PropertyChangeListener that fires on every
UIManager.setLookAndFeel call. On Linux and macOS JDKs the class is genuinely
absent from java.desktop, so the JVM throws NoClassDefFoundError the first time
JIDE's dispatch code is executed (during JFrame init and again from
JideSplitPane.updateUI on each LAF change).
Two complementary fixes:
1. Pass an explicit MetalLookAndFeel instance to the 3-arg
installJideExtension(UIDefaults, LookAndFeel, int) overload so JIDE's Metal
branch matches first and never falls through to the Windows branch in the
normal install path.
2. Add a no-op stub class at com.sun.java.swing.plaf.windows.WindowsLookAndFeel
so that the compile-time class reference resolves on non-Windows JDKs. The
stub reports isSupportedLookAndFeel = false and is never instantiated -
JIDE only uses it for instanceof comparisons against the active LAF, which
always return false in our setup. On Windows JDKs the real class in
java.desktop is loaded by the platform classloader and shadows this stub.
* Re-apply FlatLaf defaults after JIDE install + strengthen FX borders
Symptom: With dark mode enabled, the Swing main window's spectrum chrome and
menu bar were dark (FlatLaf) but tables, tabbed panes and the inner content
area were light/Metal coloured.
Root cause: LookAndFeelFactory.installJideExtension installs JIDE's
Metal-flavored UI defaults (VsnetMetalUtils etc.) into the live UIDefaults
table, overwriting FlatLaf's standard component colors and fonts for keys
like Table.background, TabbedPane.foreground, Panel.background.
Fix: After the JIDE install, walk a fresh FlatLaf UIDefaults table and put
every key back into the active defaults, skipping anything Jide-prefixed.
JIDE's UI delegate registrations live under Jide* keys that FlatLaf does
not define, so they survive untouched while the standard Swing color and
font keys are restored to their dark equivalents.
Also strengthen the JavaFX dark stylesheet: explicit borders on text fields,
tables, titled panes, split-pane dividers and separators so components have
more defining outlines in dark mode.
* Force-override Swing foreground colors via UIManager user-defaults layer
Symptom: With dark mode enabled the inner Swing content area now had dark
panels (good) but tab labels, table cells, status-bar labels and button
text were rendering nearly black-on-dark (bad).
Root cause: UIManager.getLookAndFeelDefaults() lookups for FlatLaf's
component foreground keys still resolve to Metal-era dark values after the
JIDE install + FlatLaf re-apply, because some FlatLaf keys are derived /
LazyValue and recompute against state that JIDE seeded.
Fix: After the bulk FlatLaf-defaults copy, explicitly UIManager.put the
foreground / background / selection / disabled / border colors for every
standard Swing component family. UIManager.put writes to the user-defaults
layer that takes precedence over the LAF defaults, so these overrides win
regardless of what JIDE or derived-value resolution produces.
Also bump the JavaFX dark stylesheet text colour from #d6d6d6 to #ececec so
labels read closer to white on the dark panels.
* Theme JIDE-specific UIManager keys so JideTabbedPane reads dark colors
The Now Playing / Map / Tuners / Playlist Editor / Details / Events /
Messages / Channel tabs are JIDE JideTabbedPane components and their UI
delegate reads JideTabbedPane.* UIManager keys, not the standard
TabbedPane.* keys. The previous overrides only set the standard keys so
the tabs were still rendering with JIDE's Metal-era dark foreground on
the now-dark background.
Override the JIDE-specific text foreground keys
(JideTabbedPane.foreground, .selectedTabTextForeground,
.unselectedTabTextForeground, .activeTabTextForeground) along with the
matching background and shadow keys. Also theme JideSplitPane,
JideButton and JideLabel keys for consistency.
* Remove hardcoded Color.BLACK foreground on JideTabbedPanes
ControllerPanel.getTabbedPane() and NowPlayingPanel.getTabbedPane() each
called setForeground(Color.BLACK) on their JideTabbedPane instances.
Color.BLACK is a plain java.awt.Color, not a ColorUIResource, so it is
never replaced by LookAndFeel.installColorsAndFont() on subsequent UI
installs - it permanently pins the tab labels to black regardless of any
JIDE or FlatLaf defaults the theme manager sets. In dark mode this leaves
the tab labels black-on-dark and unreadable.
Drop the explicit foreground calls so the LAF (JideTabbedPaneUI reading
JideTabbedPane.foreground / .unselectedTabTextForeground /
.selectedTabTextForeground / .activeTabTextForeground) drives the colour
in both light and dark modes.
* Register Channel tab's JFXPanel scenes with the ThemeManager
The Channel tab embeds two JavaFX scenes inside Swing via JFXPanel: the
noise squelch chart (NoiseSquelchView) and the symbol view. Neither was
registered with the ThemeManager, so the dark stylesheet was never applied
to them. In dark mode the chart legend, axis area and surrounding panel
rendered with JavaFX's default light Modena styling against the otherwise
dark UI.
JFXPanel-hosted scenes are invisible to javafx.stage.Window.getWindows(),
so the theme manager cannot find them automatically on toggle - they have
to be explicitly registered at creation time, the same as the JavaFX
Stages JavaFxWindowManager already registers.
* Use ColorUIResource for theme overrides + brighten chart series colors
Toggle-off problem: applyExplicitOverrides was storing plain java.awt.Color
values via UIManager.put. LookAndFeel.installColorsAndFont only replaces a
component's existing colour when it is null or an instance of UIResource -
plain Colors are not, so once a component picked up its dark colour it kept
that colour for the rest of the JVM lifetime even after toggling back to
light mode. Switching every override to ColorUIResource lets subsequent
updateComponentTreeUI calls replace the cached colour cleanly.
Chart visibility: the JavaFX charts on the Channel tab (NoiseSquelchView)
were inheriting Modena's default 8-colour series palette, which has several
hues that read poorly against a dark plot background. The existing
sdrtrunk_style.css also hardcodes .chart-symbol to solid black. Override
.default-color0..7.chart-series-line and matching .chart-line-symbol selectors
with a brighter high-contrast palette, darken the plot background to #1c1c1c
for stronger contrast against the grid lines, and add explicit colours for
axis tick marks, alternate row fills and legend border.
* Use primary foreground for selected tab text instead of selection foreground
The previous overrides set JideTabbedPane.selectedTabTextForeground and
TabbedPane.selectedForeground to fgSelection (always white). Selected tabs
render on the raised surface (bgRaised) which is white in light mode, so
white-on-white made selected tab labels invisible.
Selected tabs are not list/table row selections that use the blue
bgSelection background - they sit on the same raised tone as other surfaces.
Use fgPrimary so the text is dark on the light raised surface in light mode
and light on the dark raised surface in dark mode.
* Brighten chart data points to solid white in dark mode
* Restore default chart symbol size, only override colour
* Refresh detached Channel-tab swing components on theme toggle
The Channel tab swaps one of three right-pane components into its
JSplitPane depending on the active decoder type: SignalPowerView for AM,
the NoiseSquelchPanel JFXPanel for NBFM, the SymbolPanel JFXPanel for
feedback decoders. Only one is attached at any moment - the other two
are detached references on the ChannelSpectrumPanel.
SwingUtilities.updateComponentTreeUI(Window) during a theme toggle walks
only the currently-realized component trees, so the detached swappables
miss the refresh and carry stale colours from the LAF that was active
when they were last attached. Swapping back in then shows e.g. an AM
SignalPowerView in dark colours after a light-mode toggle.
Add ThemeManager.registerSwing(Component) tracking, walk those refs on
every toggle, and register the three swappables when constructed.
* 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.
* Fix theme switching past first toggle + move Themes to Appearance sub-page
Critical regression in 56b0f6c9: applyExplicitOverrides reads colours via
UIManager.getColor(...) which consults the merged defaults (user defaults
overlay LAF defaults). After the first apply our own UIManager.put calls
populate the user-defaults layer, so subsequent reads return our previous
theme's colours instead of the newly-installed LAF's colours. The result:
the first theme switch works, every switch after that silently re-applies
the previous palette and the UI appears frozen on the first picked theme.
Switch every palette read to UIManager.getLookAndFeelDefaults().getColor(...)
which is the LAF-only layer and ignores the user-defaults overlay we wrote.
Also:
- Move the Themes selector to its own sub-node:
User Preferences > Application > Appearance
PreferenceEditorType gets a new APPEARANCE entry, the existing APPLICATION
leaf is relabelled "General" so the tree no longer reads "Application >
Application > Application". New AppearancePreferenceEditor hosts the theme
radio group; ApplicationPreferenceEditor loses it.
- Replace a few hardcoded label foregrounds that read as
invisible-on-dark-bg with LAF-driven equivalents:
TunerViewPanel TunerStatus.DISABLED: DARK_GRAY -> Label.disabledForeground
BroadcastStatusPanel DISABLED state: LIGHT_GRAY -> Label.disabledForeground
JFrequencyControl Digit mouseExited: Color.WHITE -> TextField.background
Semantic colour usage (RED/YELLOW for error and warning states, the
audio-panel hardware-style black backgrounds) is intentionally kept.
* Auto-theme every JavaFX window so transient dialogs follow the theme
The Playlist Editor's main Scene is already registered with the ThemeManager,
but the many Alert / TextInputDialog / RadioReferenceUnavailableAlert popups
it spawns each create a fresh Stage with a fresh Scene that nothing registers.
Once one is shown after a theme switch it renders with default Modena styling
on top of an otherwise themed app.
Install a ListChangeListener on javafx.stage.Window.getWindows() at startup.
Every newly-added Window is themed immediately if its Scene is set, otherwise
a one-shot sceneProperty listener applies the theme the moment the Scene is
attached. Already-open windows at install time are also walked once.
This removes the need to manually register every transient dialog and covers
the playlist editor, alias editor confirmations, broadcastify error alerts,
channel-map editor warnings, and any future ad-hoc Alert / Dialog the
codebase grows.
* Curate themes to 8 modern variants + per-theme JavaFX accent colours
Theme set is trimmed and refocused:
Drop: IntelliJ (too similar to Light), FlatDarculaLaf (duplicates Dracula),
Monokai Pro (low identity in this app).
Add: Material Deep Ocean (vibrant teal dark), Arc Dark (flat modern dark).
Keep: Light, Dark, Nord, Dracula (FlatDraculaIJTheme - the one user liked),
Solarized Light, Solarized Dark.
Each theme now contributes its own accent palette to the JavaFX panels too,
not just the Swing chrome. ThemeManager builds a small per-theme stylesheet
at apply time by reading the FlatLaf-installed colours (Panel.background,
TextField.background, Component.accentColor, List.selectionBackground/Foreground,
Label.foreground, Component.borderColor, Table.alternateRowColor) and
substituting them into Modena's core variables (-fx-base, -fx-accent,
-fx-selection-bar, -fx-control-inner-background, etc.) plus a handful of
explicit rules for table selection rows, default button, scroll bar thumb.
The generated CSS is attached to every registered Scene as a data: URL, so
Nord reads blue-tinted, Dracula purple-tinted, Solarized cream/teal,
Material Deep Ocean dark teal, Arc Dark cool gray. The static
sdrtrunk_dark.css remains as the structural baseline for all dark themes.
* Add GUI scale slider under Appearance preferences
New slider in User Preferences > Application > Appearance lets users zoom
the whole UI between 50% and 200% in 10% increments. The value is
persisted as a new ApplicationPreference (ui.gui.scale, default 1.0,
bounded 0.5-2.0) and applied immediately on slider release - intermediate
drag positions do not trigger a re-apply so the LAF reinstall cost is
paid once at the end of the drag.
Swing side: ThemeManager sets the flatlaf.uiScale system property before
each setLookAndFeel call, so the new FlatLaf instance picks up the user's
zoom on initialise. All component sizes (fonts, padding, icons) scale
together because FlatLaf does the math internally.
JavaFX side: the per-theme accent stylesheet emits an
.root { -fx-font-size: <12 * scale>px; }
rule. Modena's component sizes are em-relative so cascading the font
size scales the JavaFX panels in sync with the Swing chrome.
The onPreferenceUpdated guard now compares both theme and scale, so
moving only the slider still triggers a re-apply. A Reset button next
to the slider returns to 100%.
This commit is contained in:
@@ -89,6 +89,8 @@ dependencies {
|
||||
implementation 'ch.qos.logback:logback-core:1.5.21'
|
||||
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,4 +1,4 @@
|
||||
plugins {
|
||||
id 'org.gradle.toolchains.foojay-resolver-convention' version '1.0.0'
|
||||
}
|
||||
rootProject.name = 'sdr-trunk'
|
||||
rootProject.name = 'sdr-trunk'
|
||||
|
||||
@@ -0,0 +1,81 @@
|
||||
/*
|
||||
* *****************************************************************************
|
||||
* 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 com.sun.java.swing.plaf.windows;
|
||||
|
||||
import javax.swing.LookAndFeel;
|
||||
import javax.swing.UIDefaults;
|
||||
|
||||
/**
|
||||
* No-op stub for the JDK's Windows look-and-feel.
|
||||
*
|
||||
* <p>JIDE 3.6.18's {@code LookAndFeelFactory} contains compile-time
|
||||
* {@code instanceof com.sun.java.swing.plaf.windows.WindowsLookAndFeel} checks throughout its
|
||||
* dispatch logic. On Linux and macOS JDK builds that class is genuinely absent from
|
||||
* {@code java.desktop}, so the JVM throws {@link NoClassDefFoundError} the first time JIDE's
|
||||
* dispatch code executes the {@code instanceof} bytecode. That happens during
|
||||
* {@link com.jidesoft.swing.JideSplitPane#updateUI()} on every LAF change.
|
||||
*
|
||||
* <p>This stub lives in the classpath so the class reference resolves on non-Windows JDKs.
|
||||
* On Windows JDK builds the real {@code WindowsLookAndFeel} is loaded from the
|
||||
* {@code java.desktop} module via the boot class loader and shadows this class.
|
||||
*
|
||||
* <p>The stub reports itself as not native and not supported - JIDE never instantiates this
|
||||
* class, it only uses it for {@code instanceof} comparisons against the active LAF. Since
|
||||
* sdrtrunk never installs this LAF, the {@code instanceof} check always returns false and
|
||||
* JIDE proceeds to its next branch.
|
||||
*/
|
||||
public class WindowsLookAndFeel extends LookAndFeel
|
||||
{
|
||||
@Override
|
||||
public String getName()
|
||||
{
|
||||
return "Windows (stub)";
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getID()
|
||||
{
|
||||
return "WindowsStub";
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getDescription()
|
||||
{
|
||||
return "Compatibility stub for JIDE's instanceof checks on non-Windows JDKs.";
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isNativeLookAndFeel()
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isSupportedLookAndFeel()
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public UIDefaults getDefaults()
|
||||
{
|
||||
return new UIDefaults();
|
||||
}
|
||||
}
|
||||
@@ -151,7 +151,10 @@ public class BroadcastStatusPanel extends JPanel
|
||||
else if(state == BroadcastState.DISABLED)
|
||||
{
|
||||
setBackground(table.getBackground());
|
||||
setForeground(Color.LIGHT_GRAY);
|
||||
//Use the LAF's disabled-foreground so the cell reads correctly on any
|
||||
//theme - LIGHT_GRAY is invisible on a light background.
|
||||
Color disabled = javax.swing.UIManager.getColor("Label.disabledForeground");
|
||||
setForeground(disabled != null ? disabled : table.getForeground());
|
||||
}
|
||||
else if(state == BroadcastState.INVALID_SETTINGS ||
|
||||
state == BroadcastState.NETWORK_UNAVAILABLE)
|
||||
|
||||
@@ -28,7 +28,6 @@ import io.github.dsheirer.module.decode.event.MessageActivityPanel;
|
||||
import io.github.dsheirer.playlist.PlaylistManager;
|
||||
import io.github.dsheirer.preference.UserPreferences;
|
||||
import io.github.dsheirer.settings.SettingsManager;
|
||||
import java.awt.Color;
|
||||
import net.miginfocom.swing.MigLayout;
|
||||
|
||||
import javax.swing.JPanel;
|
||||
@@ -98,7 +97,6 @@ public class NowPlayingPanel extends JPanel
|
||||
mTabbedPane.addTab("Messages", mMessageActivityPanel);
|
||||
mTabbedPane.addTab("Channel", mChannelSpectrumSquelchPanel);
|
||||
mTabbedPane.setFont(this.getFont());
|
||||
mTabbedPane.setForeground(Color.BLACK);
|
||||
//Register state change listener to toggle visibility state for channel tab to turn-on/off FFT processing
|
||||
mTabbedPane.addChangeListener(e -> mChannelSpectrumSquelchPanel.setPanelVisible(getTabbedPane().getSelectedIndex() == getTabbedPane()
|
||||
.indexOfComponent(mChannelSpectrumSquelchPanel)));
|
||||
|
||||
@@ -100,7 +100,6 @@ public class ControllerPanel extends JPanel
|
||||
}
|
||||
};
|
||||
mTabbedPane.setFont(this.getFont());
|
||||
mTabbedPane.setForeground(Color.BLACK);
|
||||
mTabbedPane.addTab("Now Playing", mNowPlayingPanel);
|
||||
mTabbedPane.addTab("Map", mMapPanel);
|
||||
mTabbedPane.addTab("Tuners", mTunerManagerPanel);
|
||||
|
||||
@@ -35,6 +35,7 @@ import io.github.dsheirer.gui.preference.PreferenceEditorType;
|
||||
import io.github.dsheirer.gui.preference.UserPreferencesEditor;
|
||||
import io.github.dsheirer.gui.preference.ViewUserPreferenceEditorRequest;
|
||||
import io.github.dsheirer.gui.preference.calibration.CalibrationDialog;
|
||||
import io.github.dsheirer.gui.theme.ThemeManager;
|
||||
import io.github.dsheirer.gui.viewer.MessageRecordingViewer;
|
||||
import io.github.dsheirer.gui.viewer.ViewRecordingViewerRequest;
|
||||
import io.github.dsheirer.icon.IconModel;
|
||||
@@ -135,6 +136,7 @@ public class JavaFxWindowManager extends Application
|
||||
//JFXPanel has to be populated on the FX event thread
|
||||
Platform.runLater(() -> {
|
||||
Scene scene = new Scene(new StatusBox(resourceMonitor));
|
||||
ThemeManager.getInstance().register(scene);
|
||||
mStatusPanel.setScene(scene);
|
||||
});
|
||||
}
|
||||
@@ -207,6 +209,7 @@ public class JavaFxWindowManager extends Application
|
||||
{
|
||||
createJFXPanel();
|
||||
Scene scene = new Scene(getRecordingViewer(), 1100, 800);
|
||||
ThemeManager.getInstance().register(scene);
|
||||
mRecordingViewerStage = new Stage();
|
||||
mRecordingViewerStage.setTitle("sdrtrunk - Message Recording Viewer (.bits)");
|
||||
mRecordingViewerStage.setScene(scene);
|
||||
@@ -232,6 +235,7 @@ public class JavaFxWindowManager extends Application
|
||||
{
|
||||
createJFXPanel();
|
||||
Scene scene = new Scene(getIconManager(), 500, 500);
|
||||
ThemeManager.getInstance().register(scene);
|
||||
mIconManagerStage = new Stage();
|
||||
mIconManagerStage.setTitle("sdrtrunk - Icon Manager");
|
||||
mIconManagerStage.setScene(scene);
|
||||
@@ -280,6 +284,7 @@ public class JavaFxWindowManager extends Application
|
||||
{
|
||||
createJFXPanel();
|
||||
Scene scene = new Scene(getJmbeEditor(), 650, 650);
|
||||
ThemeManager.getInstance().register(scene);
|
||||
mJmbeEditorStage = new Stage();
|
||||
mJmbeEditorStage.setTitle("sdrtrunk - JMBE Library Updater");
|
||||
mJmbeEditorStage.setScene(scene);
|
||||
@@ -321,6 +326,7 @@ public class JavaFxWindowManager extends Application
|
||||
{
|
||||
createJFXPanel();
|
||||
Scene scene = new Scene(getPlaylistEditor(), 1000, 750);
|
||||
ThemeManager.getInstance().register(scene);
|
||||
mPlaylistStage = new Stage();
|
||||
mPlaylistStage.setTitle("sdrtrunk - Playlist Editor");
|
||||
mPlaylistStage.setScene(scene);
|
||||
@@ -371,6 +377,7 @@ public class JavaFxWindowManager extends Application
|
||||
{
|
||||
createJFXPanel();
|
||||
Scene scene = new Scene(getUserPreferencesEditor(), 900, 500);
|
||||
ThemeManager.getInstance().register(scene);
|
||||
mUserPreferencesStage = new Stage();
|
||||
mUserPreferencesStage.setTitle("sdrtrunk - User Preferences");
|
||||
mUserPreferencesStage.setScene(scene);
|
||||
@@ -414,6 +421,7 @@ public class JavaFxWindowManager extends Application
|
||||
{
|
||||
createJFXPanel();
|
||||
Scene scene = new Scene(getChannelMapEditor(), 500, 500);
|
||||
ThemeManager.getInstance().register(scene);
|
||||
mChannelMapStage = new Stage();
|
||||
mChannelMapStage.setTitle("sdrtrunk - Channel Map Editor");
|
||||
mChannelMapStage.setScene(scene);
|
||||
|
||||
@@ -18,7 +18,6 @@
|
||||
*/
|
||||
package io.github.dsheirer.gui;
|
||||
|
||||
import com.jidesoft.plaf.LookAndFeelFactory;
|
||||
import com.jidesoft.swing.JideSplitPane;
|
||||
import io.github.dsheirer.alias.AliasModel;
|
||||
import io.github.dsheirer.audio.DuplicateCallDetector;
|
||||
@@ -38,6 +37,7 @@ import io.github.dsheirer.gui.preference.CalibrateRequest;
|
||||
import io.github.dsheirer.gui.preference.PreferenceEditorType;
|
||||
import io.github.dsheirer.gui.preference.ViewUserPreferenceEditorRequest;
|
||||
import io.github.dsheirer.gui.preference.calibration.CalibrationDialog;
|
||||
import io.github.dsheirer.gui.theme.ThemeManager;
|
||||
import io.github.dsheirer.gui.viewer.ViewRecordingViewerRequest;
|
||||
import io.github.dsheirer.icon.IconModel;
|
||||
import io.github.dsheirer.log.ApplicationLog;
|
||||
@@ -81,7 +81,6 @@ import java.nio.file.FileSystems;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.Optional;
|
||||
import java.util.prefs.Preferences;
|
||||
import javafx.application.Platform;
|
||||
@@ -102,10 +101,8 @@ import javax.swing.JMenuItem;
|
||||
import javax.swing.JOptionPane;
|
||||
import javax.swing.JSeparator;
|
||||
import javax.swing.KeyStroke;
|
||||
import javax.swing.UIManager;
|
||||
import javax.swing.event.MenuEvent;
|
||||
import javax.swing.event.MenuListener;
|
||||
import javax.swing.plaf.metal.MetalLookAndFeel;
|
||||
|
||||
public class SDRTrunk implements Listener<TunerEvent>
|
||||
{
|
||||
@@ -147,6 +144,9 @@ public class SDRTrunk implements Listener<TunerEvent>
|
||||
{
|
||||
if(!GraphicsEnvironment.isHeadless())
|
||||
{
|
||||
//Initialize the theme manager before any Swing components are realized so the chosen
|
||||
//look-and-feel applies to the very first window that is created.
|
||||
ThemeManager.getInstance().initialize(mUserPreferences);
|
||||
mMainGui = new JFrame();
|
||||
}
|
||||
|
||||
@@ -163,21 +163,6 @@ public class SDRTrunk implements Listener<TunerEvent>
|
||||
|
||||
mResourceMonitor = new ResourceMonitor(mUserPreferences);
|
||||
|
||||
String operatingSystem = System.getProperty("os.name", "generic").toLowerCase(Locale.ENGLISH);
|
||||
|
||||
if(operatingSystem.contains("mac") || operatingSystem.contains("nux"))
|
||||
{
|
||||
try
|
||||
{
|
||||
UIManager.setLookAndFeel(MetalLookAndFeel.class.getName());
|
||||
LookAndFeelFactory.installJideExtension();
|
||||
}
|
||||
catch(Exception e)
|
||||
{
|
||||
mLog.error("Error trying to set Metal look and feel for OS [" + operatingSystem + "]");
|
||||
}
|
||||
}
|
||||
|
||||
ThreadPool.logSettings();
|
||||
|
||||
//Load properties file
|
||||
|
||||
@@ -24,6 +24,7 @@ import io.github.dsheirer.dsp.filter.channelizer.PolyphaseChannelSource;
|
||||
import io.github.dsheirer.gui.power.SignalPowerView;
|
||||
import io.github.dsheirer.gui.squelch.NoiseSquelchView;
|
||||
import io.github.dsheirer.gui.symbol.SymbolView;
|
||||
import io.github.dsheirer.gui.theme.ThemeManager;
|
||||
import io.github.dsheirer.module.ProcessingChain;
|
||||
import io.github.dsheirer.module.decode.FeedbackDecoder;
|
||||
import io.github.dsheirer.module.decode.PrimaryDecoder;
|
||||
@@ -112,6 +113,11 @@ public class ChannelSpectrumPanel extends JPanel implements Listener<ProcessingC
|
||||
mPlaylistManager = playlistManager;
|
||||
mNoiseSquelchView = new NoiseSquelchView(mPlaylistManager);
|
||||
mSignalPowerView = new SignalPowerView(mPlaylistManager);
|
||||
|
||||
//Register the three swappable right-pane components with the theme manager. Only one is
|
||||
//attached to the split pane at a time, so the LAF-toggle walk of Window.getWindows() can
|
||||
//miss the detached ones - they get refreshed explicitly via registerSwing.
|
||||
ThemeManager.getInstance().registerSwing(mSignalPowerView);
|
||||
setLayout(new MigLayout("insets 0", "[grow,fill]", "[grow,fill]"));
|
||||
|
||||
JPanel fftPanel = new JPanel();
|
||||
@@ -222,10 +228,13 @@ public class ChannelSpectrumPanel extends JPanel implements Listener<ProcessingC
|
||||
|
||||
mNoiseSquelchPanel = new JFXPanel();
|
||||
mSymbolPanel = new JFXPanel();
|
||||
ThemeManager.getInstance().registerSwing(mNoiseSquelchPanel);
|
||||
ThemeManager.getInstance().registerSwing(mSymbolPanel);
|
||||
|
||||
//Spin noise squelch panel construction off onto the JavafX UI thread.
|
||||
Platform.runLater(() -> {
|
||||
Scene scene = new Scene(mNoiseSquelchView);
|
||||
ThemeManager.getInstance().register(scene);
|
||||
mNoiseSquelchPanel.setScene(scene);
|
||||
Scene scene2 = new Scene(mSymbolView);
|
||||
URL resource = getClass().getResource("/sdrtrunk_style.css");
|
||||
@@ -239,6 +248,7 @@ public class ChannelSpectrumPanel extends JPanel implements Listener<ProcessingC
|
||||
LOGGER.warn("Can't find stylesheet resource for sdrtrunk");
|
||||
}
|
||||
|
||||
ThemeManager.getInstance().register(scene2);
|
||||
mSymbolPanel.setScene(scene2);
|
||||
});
|
||||
|
||||
|
||||
@@ -562,7 +562,10 @@ public class JFrequencyControl extends JPanel implements ISourceEventProcessor
|
||||
@Override
|
||||
public void mouseExited(MouseEvent e)
|
||||
{
|
||||
Digit.this.setBackground(Color.WHITE);
|
||||
//Restore the LAF text-field background rather than hardcoding white so the digit
|
||||
//cells match the active theme when the mouse leaves.
|
||||
Color textBg = javax.swing.UIManager.getColor("TextField.background");
|
||||
Digit.this.setBackground(textBg != null ? textBg : Color.WHITE);
|
||||
repaint();
|
||||
}
|
||||
|
||||
|
||||
@@ -19,6 +19,7 @@
|
||||
|
||||
package io.github.dsheirer.gui.preference;
|
||||
|
||||
import io.github.dsheirer.gui.preference.application.AppearancePreferenceEditor;
|
||||
import io.github.dsheirer.gui.preference.application.ApplicationPreferenceEditor;
|
||||
import io.github.dsheirer.gui.preference.calibration.VectorCalibrationPreferenceEditor;
|
||||
import io.github.dsheirer.gui.preference.call.CallManagementPreferenceEditor;
|
||||
@@ -42,6 +43,8 @@ public class PreferenceEditorFactory
|
||||
{
|
||||
case APPLICATION:
|
||||
return new ApplicationPreferenceEditor(userPreferences);
|
||||
case APPEARANCE:
|
||||
return new AppearancePreferenceEditor(userPreferences);
|
||||
case AUDIO_CALL_MANAGEMENT:
|
||||
return new CallManagementPreferenceEditor(userPreferences);
|
||||
case AUDIO_MP3:
|
||||
|
||||
@@ -24,7 +24,8 @@ package io.github.dsheirer.gui.preference;
|
||||
*/
|
||||
public enum PreferenceEditorType
|
||||
{
|
||||
APPLICATION("Application"),
|
||||
APPLICATION("General"),
|
||||
APPEARANCE("Appearance"),
|
||||
CHANNEL_EVENT("Channel Events"),
|
||||
DIRECTORY("Directories"),
|
||||
JMBE_LIBRARY("JMBE Audio Library"),
|
||||
|
||||
@@ -167,6 +167,7 @@ public class UserPreferencesEditor extends BorderPane
|
||||
|
||||
TreeItem<String> applicationItem = new TreeItem<>("Application");
|
||||
applicationItem.getChildren().add(new TreeItem(PreferenceEditorType.APPLICATION));
|
||||
applicationItem.getChildren().add(new TreeItem(PreferenceEditorType.APPEARANCE));
|
||||
treeRoot.getChildren().add(applicationItem);
|
||||
applicationItem.setExpanded(true);
|
||||
|
||||
|
||||
@@ -0,0 +1,205 @@
|
||||
/*
|
||||
* *****************************************************************************
|
||||
* 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.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.Insets;
|
||||
import javafx.geometry.Orientation;
|
||||
import javafx.scene.control.Button;
|
||||
import javafx.scene.control.Label;
|
||||
import javafx.scene.control.RadioButton;
|
||||
import javafx.scene.control.Separator;
|
||||
import javafx.scene.control.Slider;
|
||||
import javafx.scene.control.ToggleGroup;
|
||||
import javafx.scene.layout.ColumnConstraints;
|
||||
import javafx.scene.layout.GridPane;
|
||||
import javafx.scene.layout.HBox;
|
||||
import javafx.scene.layout.Priority;
|
||||
import javafx.scene.layout.VBox;
|
||||
|
||||
/**
|
||||
* Appearance preferences - selects the active UI {@link Theme} and the global GUI zoom factor for
|
||||
* both the Swing main window and the JavaFX panels.
|
||||
*/
|
||||
public class AppearancePreferenceEditor extends HBox
|
||||
{
|
||||
private final ApplicationPreference mApplicationPreference;
|
||||
private GridPane mEditorPane;
|
||||
private VBox mThemeRadioGroup;
|
||||
private Slider mGuiScaleSlider;
|
||||
private Label mGuiScaleValueLabel;
|
||||
|
||||
public AppearancePreferenceEditor(UserPreferences userPreferences)
|
||||
{
|
||||
mApplicationPreference = userPreferences.getApplicationPreference();
|
||||
setMaxWidth(Double.MAX_VALUE);
|
||||
|
||||
VBox vbox = new VBox();
|
||||
vbox.setMaxHeight(Double.MAX_VALUE);
|
||||
vbox.setMaxWidth(Double.MAX_VALUE);
|
||||
vbox.getChildren().add(getEditorPane());
|
||||
HBox.setHgrow(vbox, Priority.ALWAYS);
|
||||
getChildren().add(vbox);
|
||||
}
|
||||
|
||||
private GridPane getEditorPane()
|
||||
{
|
||||
if(mEditorPane == null)
|
||||
{
|
||||
int row = 0;
|
||||
mEditorPane = new GridPane();
|
||||
mEditorPane.setMaxWidth(Double.MAX_VALUE);
|
||||
mEditorPane.setVgap(10);
|
||||
mEditorPane.setHgap(8);
|
||||
mEditorPane.setPadding(new Insets(10, 10, 10, 10));
|
||||
|
||||
mEditorPane.add(new Label("Theme"), 0, row, 3, 1);
|
||||
mEditorPane.add(getThemeRadioGroup(), 0, ++row, 3, 1);
|
||||
|
||||
Separator separator = new Separator(Orientation.HORIZONTAL);
|
||||
GridPane.setHgrow(separator, Priority.ALWAYS);
|
||||
mEditorPane.add(separator, 0, ++row, 3, 1);
|
||||
|
||||
mEditorPane.add(new Label("GUI Scale"), 0, ++row, 3, 1);
|
||||
|
||||
HBox sliderRow = new HBox(8);
|
||||
sliderRow.setPadding(new Insets(2, 0, 2, 4));
|
||||
HBox.setHgrow(getGuiScaleSlider(), Priority.ALWAYS);
|
||||
Button resetButton = new Button("Reset");
|
||||
resetButton.setOnAction(e -> {
|
||||
getGuiScaleSlider().setValue(ApplicationPreference.DEFAULT_GUI_SCALE);
|
||||
mApplicationPreference.setGuiScale(ApplicationPreference.DEFAULT_GUI_SCALE);
|
||||
});
|
||||
sliderRow.getChildren().addAll(getGuiScaleSlider(), getGuiScaleValueLabel(), resetButton);
|
||||
mEditorPane.add(sliderRow, 0, ++row, 3, 1);
|
||||
|
||||
Label hint = new Label("Adjusts the size of all UI components. Takes effect immediately.");
|
||||
hint.setWrapText(true);
|
||||
mEditorPane.add(hint, 0, ++row, 3, 1);
|
||||
|
||||
ColumnConstraints c1 = new ColumnConstraints();
|
||||
c1.setPercentWidth(30);
|
||||
ColumnConstraints c2 = new ColumnConstraints();
|
||||
c2.setHgrow(Priority.ALWAYS);
|
||||
mEditorPane.getColumnConstraints().addAll(c1, c2);
|
||||
}
|
||||
|
||||
return mEditorPane;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 VBox getThemeRadioGroup()
|
||||
{
|
||||
if(mThemeRadioGroup == null)
|
||||
{
|
||||
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 mThemeRadioGroup;
|
||||
}
|
||||
|
||||
/**
|
||||
* Slider to adjust the global GUI zoom factor. The slider snaps to 10% increments and only
|
||||
* persists the value when the user releases the slider so we do not thrash a LAF reinstall on
|
||||
* every intermediate value during a drag.
|
||||
*/
|
||||
private Slider getGuiScaleSlider()
|
||||
{
|
||||
if(mGuiScaleSlider == null)
|
||||
{
|
||||
mGuiScaleSlider = new Slider(ApplicationPreference.MIN_GUI_SCALE,
|
||||
ApplicationPreference.MAX_GUI_SCALE,
|
||||
mApplicationPreference.getGuiScale());
|
||||
mGuiScaleSlider.setMajorTickUnit(0.25d);
|
||||
mGuiScaleSlider.setMinorTickCount(1);
|
||||
mGuiScaleSlider.setSnapToTicks(true);
|
||||
mGuiScaleSlider.setShowTickMarks(true);
|
||||
mGuiScaleSlider.setShowTickLabels(false);
|
||||
mGuiScaleSlider.setBlockIncrement(0.1d);
|
||||
|
||||
//Update the live percentage label as the user drags.
|
||||
mGuiScaleSlider.valueProperty().addListener((obs, oldV, newV) -> {
|
||||
if(newV != null)
|
||||
{
|
||||
getGuiScaleValueLabel().setText(formatScale(newV.doubleValue()));
|
||||
}
|
||||
});
|
||||
|
||||
//Persist + apply only when the user finishes the drag - avoids reinstalling the LAF
|
||||
//on every intermediate frame.
|
||||
mGuiScaleSlider.valueChangingProperty().addListener((obs, wasChanging, changing) -> {
|
||||
if(wasChanging && !changing)
|
||||
{
|
||||
mApplicationPreference.setGuiScale(mGuiScaleSlider.getValue());
|
||||
}
|
||||
});
|
||||
|
||||
//Cover keyboard / scroll-wheel adjustments which never enter the changing=true state.
|
||||
mGuiScaleSlider.setOnMouseReleased(e -> {
|
||||
if(!mGuiScaleSlider.isValueChanging())
|
||||
{
|
||||
mApplicationPreference.setGuiScale(mGuiScaleSlider.getValue());
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return mGuiScaleSlider;
|
||||
}
|
||||
|
||||
private Label getGuiScaleValueLabel()
|
||||
{
|
||||
if(mGuiScaleValueLabel == null)
|
||||
{
|
||||
mGuiScaleValueLabel = new Label(formatScale(mApplicationPreference.getGuiScale()));
|
||||
mGuiScaleValueLabel.setMinWidth(50);
|
||||
}
|
||||
|
||||
return mGuiScaleValueLabel;
|
||||
}
|
||||
|
||||
private static String formatScale(double scale)
|
||||
{
|
||||
return Math.round(scale * 100) + "%";
|
||||
}
|
||||
}
|
||||
96
src/main/java/io/github/dsheirer/gui/theme/Theme.java
Normal file
96
src/main/java/io/github/dsheirer/gui/theme/Theme.java
Normal file
@@ -0,0 +1,96 @@
|
||||
/*
|
||||
* *****************************************************************************
|
||||
* 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),
|
||||
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),
|
||||
MATERIAL_DEEP_OCEAN("Material Deep Ocean", "com.formdev.flatlaf.intellijthemes.materialthemeuilite.FlatMaterialDeepOceanIJTheme", true),
|
||||
ARC_DARK("Arc Dark", "com.formdev.flatlaf.intellijthemes.FlatArcDarkIJTheme", 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;
|
||||
}
|
||||
}
|
||||
789
src/main/java/io/github/dsheirer/gui/theme/ThemeManager.java
Normal file
789
src/main/java/io/github/dsheirer/gui/theme/ThemeManager.java
Normal file
@@ -0,0 +1,789 @@
|
||||
/*
|
||||
* *****************************************************************************
|
||||
* 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;
|
||||
|
||||
import com.formdev.flatlaf.FlatLightLaf;
|
||||
import com.google.common.eventbus.Subscribe;
|
||||
import com.jidesoft.plaf.LookAndFeelFactory;
|
||||
import io.github.dsheirer.eventbus.MyEventBus;
|
||||
import io.github.dsheirer.preference.PreferenceType;
|
||||
import io.github.dsheirer.preference.UserPreferences;
|
||||
import java.awt.Color;
|
||||
import java.awt.EventQueue;
|
||||
import java.awt.Window;
|
||||
import java.lang.ref.WeakReference;
|
||||
import java.util.Iterator;
|
||||
import java.util.LinkedHashSet;
|
||||
import java.util.Set;
|
||||
import javafx.application.Platform;
|
||||
import javafx.scene.Scene;
|
||||
import javax.swing.LookAndFeel;
|
||||
import javax.swing.SwingUtilities;
|
||||
import javax.swing.UIDefaults;
|
||||
import javax.swing.UIManager;
|
||||
import javax.swing.plaf.ColorUIResource;
|
||||
import javax.swing.plaf.metal.MetalLookAndFeel;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
/**
|
||||
* Centralized theme manager that drives both Swing and JavaFX appearance from the user
|
||||
* preference selection.
|
||||
*
|
||||
* <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
|
||||
{
|
||||
private final static Logger mLog = LoggerFactory.getLogger(ThemeManager.class);
|
||||
private static final String DARK_STYLESHEET = "/sdrtrunk_dark.css";
|
||||
private static final ThemeManager INSTANCE = new ThemeManager();
|
||||
|
||||
private final Set<WeakReference<Scene>> mScenes = new LinkedHashSet<>();
|
||||
private final Set<WeakReference<java.awt.Component>> mSwingRoots = new LinkedHashSet<>();
|
||||
private final String mDarkStylesheetUrl;
|
||||
private volatile UserPreferences mUserPreferences;
|
||||
private volatile Theme mCurrentTheme = Theme.LIGHT;
|
||||
private volatile double mCurrentGuiScale = 1.0d;
|
||||
/** Data URL of the currently-active per-theme accent stylesheet, or null. */
|
||||
private volatile String mAccentStylesheetUrl;
|
||||
|
||||
private ThemeManager()
|
||||
{
|
||||
java.net.URL url = ThemeManager.class.getResource(DARK_STYLESHEET);
|
||||
mDarkStylesheetUrl = (url != null) ? url.toExternalForm() : null;
|
||||
|
||||
if(mDarkStylesheetUrl == null)
|
||||
{
|
||||
mLog.warn("Dark stylesheet [{}] not found on classpath - dark themes will only affect Swing.",
|
||||
DARK_STYLESHEET);
|
||||
}
|
||||
}
|
||||
|
||||
public static ThemeManager getInstance()
|
||||
{
|
||||
return INSTANCE;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
public void initialize(UserPreferences userPreferences)
|
||||
{
|
||||
if(userPreferences == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
boolean firstBind = (mUserPreferences == null);
|
||||
mUserPreferences = userPreferences;
|
||||
mCurrentTheme = userPreferences.getApplicationPreference().getTheme();
|
||||
mCurrentGuiScale = userPreferences.getApplicationPreference().getGuiScale();
|
||||
|
||||
if(firstBind)
|
||||
{
|
||||
MyEventBus.getGlobalEventBus().register(this);
|
||||
attachJavaFxWindowListener();
|
||||
}
|
||||
|
||||
//applyAll installs the Swing LAF *and* computes the per-theme accent stylesheet URL so it
|
||||
//is ready before any Scene.register call. At init time the FX-side scene walk is a no-op
|
||||
//because nothing has registered yet, but mAccentStylesheetUrl gets populated for later
|
||||
//register(...) calls.
|
||||
applyAll(mCurrentTheme);
|
||||
}
|
||||
|
||||
/**
|
||||
* Install a listener on {@link javafx.stage.Window#getWindows()} so that every JavaFX window
|
||||
* created in the application (including ad-hoc {@code Alert}, {@code TextInputDialog} and
|
||||
* other transient dialogs spawned by the playlist editor and similar) automatically picks up
|
||||
* the current theme without each call site needing to remember to register its Scene. Idle
|
||||
* cost is one listener invocation per window opened; the listener short-circuits when the FX
|
||||
* toolkit is not yet started.
|
||||
*/
|
||||
private void attachJavaFxWindowListener()
|
||||
{
|
||||
Runnable attach = () -> {
|
||||
try
|
||||
{
|
||||
javafx.stage.Window.getWindows().addListener(
|
||||
(javafx.collections.ListChangeListener<javafx.stage.Window>) change -> {
|
||||
while(change.next())
|
||||
{
|
||||
if(change.wasAdded())
|
||||
{
|
||||
for(javafx.stage.Window w: change.getAddedSubList())
|
||||
{
|
||||
themeWindowWhenSceneReady(w);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
//Also cover any windows that already existed when we installed the listener.
|
||||
for(javafx.stage.Window w: javafx.stage.Window.getWindows())
|
||||
{
|
||||
themeWindowWhenSceneReady(w);
|
||||
}
|
||||
}
|
||||
catch(Throwable t)
|
||||
{
|
||||
mLog.warn("Unable to attach JavaFX Window list listener for auto-theming", t);
|
||||
}
|
||||
};
|
||||
|
||||
if(Platform.isFxApplicationThread())
|
||||
{
|
||||
attach.run();
|
||||
}
|
||||
else
|
||||
{
|
||||
try
|
||||
{
|
||||
Platform.runLater(attach);
|
||||
}
|
||||
catch(IllegalStateException ignored)
|
||||
{
|
||||
//FX toolkit not started yet - first JFXPanel construction will start it; we
|
||||
//re-attempt the attach lazily inside applyAll() via the Window.getWindows() walk.
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void themeWindowWhenSceneReady(javafx.stage.Window window)
|
||||
{
|
||||
if(window == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
Scene scene = window.getScene();
|
||||
if(scene != null)
|
||||
{
|
||||
applyToScene(scene, isDarkMode());
|
||||
}
|
||||
else
|
||||
{
|
||||
//The Scene may be assigned after the Window is added to the list (the
|
||||
//typical Alert/Dialog lifecycle). Listen once for the assignment.
|
||||
window.sceneProperty().addListener((obs, oldScene, newScene) -> {
|
||||
if(newScene != null)
|
||||
{
|
||||
applyToScene(newScene, isDarkMode());
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @return the currently active theme.
|
||||
*/
|
||||
public Theme getCurrentTheme()
|
||||
{
|
||||
return mCurrentTheme;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return true if the currently active theme is dark.
|
||||
*/
|
||||
public boolean isDarkMode()
|
||||
{
|
||||
return mCurrentTheme != null && mCurrentTheme.isDark();
|
||||
}
|
||||
|
||||
public void register(Scene scene)
|
||||
{
|
||||
if(scene == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
synchronized(mScenes)
|
||||
{
|
||||
pruneDeadReferencesAndCheckPresent(scene);
|
||||
mScenes.add(new WeakReference<>(scene));
|
||||
}
|
||||
|
||||
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). 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)
|
||||
{
|
||||
if(component == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
synchronized(mSwingRoots)
|
||||
{
|
||||
Iterator<WeakReference<java.awt.Component>> it = mSwingRoots.iterator();
|
||||
while(it.hasNext())
|
||||
{
|
||||
java.awt.Component existing = it.next().get();
|
||||
if(existing == null || existing == component)
|
||||
{
|
||||
it.remove();
|
||||
}
|
||||
}
|
||||
mSwingRoots.add(new WeakReference<>(component));
|
||||
}
|
||||
}
|
||||
|
||||
@Subscribe
|
||||
public void onPreferenceUpdated(PreferenceType type)
|
||||
{
|
||||
if(type != PreferenceType.APPLICATION || mUserPreferences == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
Theme updated = mUserPreferences.getApplicationPreference().getTheme();
|
||||
double updatedScale = mUserPreferences.getApplicationPreference().getGuiScale();
|
||||
|
||||
if(updated == mCurrentTheme && Math.abs(updatedScale - mCurrentGuiScale) < 1e-6)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
mCurrentTheme = updated;
|
||||
mCurrentGuiScale = updatedScale;
|
||||
applyAll(updated);
|
||||
}
|
||||
|
||||
private void applyAll(Theme theme)
|
||||
{
|
||||
applySwingLookAndFeel(theme);
|
||||
|
||||
//Compute the per-theme accent stylesheet on the calling thread so the data URL is ready
|
||||
//before the FX apply runs. Capture the previous URL so we can remove it from each Scene.
|
||||
String previousAccentUrl = mAccentStylesheetUrl;
|
||||
String nextAccentUrl = buildAccentStylesheet(theme);
|
||||
mAccentStylesheetUrl = nextAccentUrl;
|
||||
|
||||
Runnable fxApply = () -> {
|
||||
synchronized(mScenes)
|
||||
{
|
||||
Iterator<WeakReference<Scene>> it = mScenes.iterator();
|
||||
while(it.hasNext())
|
||||
{
|
||||
Scene scene = it.next().get();
|
||||
if(scene == null)
|
||||
{
|
||||
it.remove();
|
||||
}
|
||||
else
|
||||
{
|
||||
applyToScene(scene, theme.isDark(), previousAccentUrl, nextAccentUrl);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for(javafx.stage.Window window: javafx.stage.Window.getWindows())
|
||||
{
|
||||
Scene scene = window.getScene();
|
||||
if(scene != null)
|
||||
{
|
||||
applyToScene(scene, theme.isDark(), previousAccentUrl, nextAccentUrl);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
if(Platform.isFxApplicationThread())
|
||||
{
|
||||
fxApply.run();
|
||||
}
|
||||
else
|
||||
{
|
||||
try
|
||||
{
|
||||
Platform.runLater(fxApply);
|
||||
}
|
||||
catch(IllegalStateException ignored)
|
||||
{
|
||||
//FX toolkit not started yet - scenes will be themed at registration time instead.
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Apply the current theme to a single Scene. Used when a new Scene is registered or appears
|
||||
* mid-session. The accent-stylesheet swap is handled by the (previousUrl, nextUrl) overload.
|
||||
*/
|
||||
private void applyToScene(Scene scene, boolean darkMode)
|
||||
{
|
||||
applyToScene(scene, darkMode, null, mAccentStylesheetUrl);
|
||||
}
|
||||
|
||||
/**
|
||||
* Update a Scene's stylesheets to match the current theme: ensures the dark-mode CSS is
|
||||
* present iff {@code darkMode}, removes {@code previousAccentUrl} if present, and ensures
|
||||
* {@code nextAccentUrl} is present so theme-specific accent colours are applied.
|
||||
*/
|
||||
private void applyToScene(Scene scene, boolean darkMode, String previousAccentUrl,
|
||||
String nextAccentUrl)
|
||||
{
|
||||
Runnable r = () -> {
|
||||
if(mDarkStylesheetUrl != null)
|
||||
{
|
||||
scene.getStylesheets().remove(mDarkStylesheetUrl);
|
||||
if(darkMode)
|
||||
{
|
||||
scene.getStylesheets().add(mDarkStylesheetUrl);
|
||||
}
|
||||
}
|
||||
|
||||
if(previousAccentUrl != null && !previousAccentUrl.equals(nextAccentUrl))
|
||||
{
|
||||
scene.getStylesheets().remove(previousAccentUrl);
|
||||
}
|
||||
|
||||
if(nextAccentUrl != null && !scene.getStylesheets().contains(nextAccentUrl))
|
||||
{
|
||||
scene.getStylesheets().add(nextAccentUrl);
|
||||
}
|
||||
};
|
||||
|
||||
if(Platform.isFxApplicationThread())
|
||||
{
|
||||
r.run();
|
||||
}
|
||||
else
|
||||
{
|
||||
Platform.runLater(r);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Build a small per-theme stylesheet that overrides Modena's accent and surface colours so
|
||||
* each theme reads visibly different in the JavaFX panels. Returns a {@code data:} URL that
|
||||
* can be added to a Scene's stylesheets list directly.
|
||||
*/
|
||||
private String buildAccentStylesheet(Theme theme)
|
||||
{
|
||||
try
|
||||
{
|
||||
UIDefaults laf = UIManager.getLookAndFeelDefaults();
|
||||
boolean dark = theme.isDark();
|
||||
|
||||
String base = hex(laf.getColor("Panel.background"), dark ? 0x2b2b2b : 0xf2f2f2);
|
||||
String inner = hex(laf.getColor("TextField.background"), dark ? 0x313335 : 0xffffff);
|
||||
String innerAlt = hex(laf.getColor("Table.alternateRowColor"), dark ? 0x353739 : 0xfafafa);
|
||||
String accent = hex(laf.getColor("Component.accentColor"),
|
||||
hexInt(laf.getColor("List.selectionBackground"), dark ? 0x4a90d9 : 0x2675bf));
|
||||
String selectionBg = hex(laf.getColor("List.selectionBackground"),
|
||||
dark ? 0x214283 : 0x2675bf);
|
||||
String selectionFg = hex(laf.getColor("List.selectionForeground"), 0xffffff);
|
||||
String text = hex(laf.getColor("Label.foreground"), dark ? 0xe6e6e6 : 0x1e1e1e);
|
||||
String border = hex(laf.getColor("Component.borderColor"),
|
||||
dark ? 0x4f5356 : 0xc4c4c4);
|
||||
|
||||
//JavaFX side of the GUI scale: setting -fx-font-size on .root cascades through
|
||||
//Modena because most sizes are em-relative. Modena's default is 12px; multiplying
|
||||
//by mCurrentGuiScale and rounding to one decimal gives a clean scaled baseline.
|
||||
double scaledFontSize = Math.round(12.0d * mCurrentGuiScale * 10.0d) / 10.0d;
|
||||
|
||||
StringBuilder css = new StringBuilder();
|
||||
css.append(".root {");
|
||||
css.append("-fx-font-size: ").append(scaledFontSize).append("px;");
|
||||
css.append("-fx-base: ").append(base).append(";");
|
||||
css.append("-fx-background: ").append(base).append(";");
|
||||
css.append("-fx-control-inner-background: ").append(inner).append(";");
|
||||
css.append("-fx-control-inner-background-alt: ").append(innerAlt).append(";");
|
||||
css.append("-fx-accent: ").append(accent).append(";");
|
||||
css.append("-fx-default-button: ").append(accent).append(";");
|
||||
css.append("-fx-focus-color: ").append(accent).append(";");
|
||||
css.append("-fx-faint-focus-color: ").append(accent).append("22;");
|
||||
css.append("-fx-selection-bar: ").append(selectionBg).append(";");
|
||||
css.append("-fx-selection-bar-text: ").append(selectionFg).append(";");
|
||||
css.append("-fx-selection-bar-non-focused: ").append(innerAlt).append(";");
|
||||
css.append("-fx-text-fill: ").append(text).append(";");
|
||||
css.append("-fx-text-base-color: ").append(text).append(";");
|
||||
css.append("-fx-text-background-color: ").append(text).append(";");
|
||||
css.append("-fx-mark-color: ").append(text).append(";");
|
||||
css.append("}");
|
||||
css.append(".table-row-cell:filled:selected, .list-cell:filled:selected,")
|
||||
.append(" .tree-cell:filled:selected, .tree-table-row-cell:filled:selected {")
|
||||
.append("-fx-background: ").append(selectionBg).append(";")
|
||||
.append("-fx-background-color: ").append(selectionBg).append(";")
|
||||
.append("-fx-text-fill: ").append(selectionFg).append(";")
|
||||
.append("-fx-table-cell-border-color: ").append(border).append(";")
|
||||
.append("}");
|
||||
css.append(".button:default {")
|
||||
.append("-fx-base: ").append(accent).append(";")
|
||||
.append("-fx-text-fill: ").append(selectionFg).append(";")
|
||||
.append("}");
|
||||
css.append(".scroll-bar .thumb {")
|
||||
.append("-fx-background-color: ").append(border).append(";")
|
||||
.append("-fx-background-radius: 3;")
|
||||
.append("}");
|
||||
css.append(".scroll-bar .thumb:hover {")
|
||||
.append("-fx-background-color: ").append(accent).append(";")
|
||||
.append("}");
|
||||
|
||||
//URL-encode the body; java.net.URLEncoder is fine for our restricted charset.
|
||||
String encoded = java.net.URLEncoder.encode(css.toString(),
|
||||
java.nio.charset.StandardCharsets.UTF_8);
|
||||
return "data:text/css," + encoded;
|
||||
}
|
||||
catch(Throwable t)
|
||||
{
|
||||
mLog.warn("Unable to build per-theme accent stylesheet for theme " + theme, t);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private static String hex(Color c, int fallbackRgb)
|
||||
{
|
||||
int rgb = (c != null) ? (c.getRGB() & 0xFFFFFF) : (fallbackRgb & 0xFFFFFF);
|
||||
return String.format("#%06x", rgb);
|
||||
}
|
||||
|
||||
private static int hexInt(Color c, int fallbackRgb)
|
||||
{
|
||||
return (c != null) ? (c.getRGB() & 0xFFFFFF) : (fallbackRgb & 0xFFFFFF);
|
||||
}
|
||||
|
||||
private void applySwingLookAndFeel(Theme theme)
|
||||
{
|
||||
//FlatLaf reads the flatlaf.uiScale system property when its LAF instance is initialised.
|
||||
//Set it before constructing the LAF so this theme apply picks up the user's zoom factor.
|
||||
//We also clear any previous UIManager-stored override so the system property takes effect.
|
||||
System.setProperty("flatlaf.uiScale", String.valueOf(mCurrentGuiScale));
|
||||
UIManager.put("flatlaf.uiScale", null);
|
||||
|
||||
//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
|
||||
{
|
||||
UIManager.setLookAndFeel(newLaf);
|
||||
}
|
||||
catch(Exception e)
|
||||
{
|
||||
mLog.error("Unable to apply Swing look-and-feel for theme = " + theme, e);
|
||||
return;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
//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.
|
||||
LookAndFeelFactory.installJideExtension(UIManager.getLookAndFeelDefaults(),
|
||||
new MetalLookAndFeel(), LookAndFeelFactory.VSNET_STYLE);
|
||||
}
|
||||
catch(Throwable t)
|
||||
{
|
||||
mLog.warn("Unable to install JIDE LAF extension after theme change", t);
|
||||
}
|
||||
|
||||
//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 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 = instantiate(theme).getDefaults();
|
||||
UIDefaults active = UIManager.getLookAndFeelDefaults();
|
||||
for(java.util.Map.Entry<Object, Object> entry: flatFresh.entrySet())
|
||||
{
|
||||
Object key = entry.getKey();
|
||||
Object value = entry.getValue();
|
||||
String keyName = String.valueOf(key);
|
||||
|
||||
if(keyName.startsWith("Jide") || keyName.contains(".Jide"))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if(value != null)
|
||||
{
|
||||
active.put(key, value);
|
||||
}
|
||||
}
|
||||
}
|
||||
catch(Throwable t)
|
||||
{
|
||||
mLog.warn("Unable to re-apply FlatLaf defaults after JIDE install", t);
|
||||
}
|
||||
|
||||
//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 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())
|
||||
{
|
||||
SwingUtilities.updateComponentTreeUI(window);
|
||||
}
|
||||
|
||||
synchronized(mSwingRoots)
|
||||
{
|
||||
Iterator<WeakReference<java.awt.Component>> it = mSwingRoots.iterator();
|
||||
while(it.hasNext())
|
||||
{
|
||||
java.awt.Component c = it.next().get();
|
||||
if(c == null)
|
||||
{
|
||||
it.remove();
|
||||
}
|
||||
else
|
||||
{
|
||||
SwingUtilities.updateComponentTreeUI(c);
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
if(EventQueue.isDispatchThread())
|
||||
{
|
||||
updateRealized.run();
|
||||
}
|
||||
else
|
||||
{
|
||||
EventQueue.invokeLater(updateRealized);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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)
|
||||
{
|
||||
try
|
||||
{
|
||||
Class<?> cls = Class.forName(theme.getLafClassName());
|
||||
return (LookAndFeel) cls.getDeclaredConstructor().newInstance();
|
||||
}
|
||||
catch(Throwable t)
|
||||
{
|
||||
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();
|
||||
|
||||
//CRITICAL: read from UIManager.getLookAndFeelDefaults() (LAF layer only) rather than
|
||||
//UIManager.getColor() / UIManager.getDefaults() (which is the merged view). The merged
|
||||
//view returns user-defaults entries from earlier applyExplicitOverrides calls instead of
|
||||
//the fresh LAF values, which means a second theme switch picks up the previous theme's
|
||||
//palette and the toggle appears broken after the first change.
|
||||
UIDefaults laf = UIManager.getLookAndFeelDefaults();
|
||||
|
||||
ColorUIResource bgPanel = uir(laf.getColor("Panel.background"),
|
||||
darkMode ? 0x2b2b2b : 0xf2f2f2);
|
||||
ColorUIResource bgRaised = uir(laf.getColor("Button.background"),
|
||||
darkMode ? 0x3c3f41 : 0xffffff);
|
||||
ColorUIResource bgInput = uir(laf.getColor("TextField.background"),
|
||||
darkMode ? 0x313335 : 0xffffff);
|
||||
ColorUIResource bgSelection = uir(laf.getColor("List.selectionBackground"),
|
||||
darkMode ? 0x214283 : 0x2675bf);
|
||||
ColorUIResource fgPrimary = uir(laf.getColor("Label.foreground"),
|
||||
darkMode ? 0xe6e6e6 : 0x1e1e1e);
|
||||
ColorUIResource fgSelection = uir(laf.getColor("List.selectionForeground"),
|
||||
0xffffff);
|
||||
ColorUIResource fgDisabled = uir(laf.getColor("Label.disabledForeground"),
|
||||
darkMode ? 0x6a6a6a : 0x8c8c8c);
|
||||
ColorUIResource border = uir(laf.getColor("Component.borderColor"),
|
||||
darkMode ? 0x4f5356 : 0xc4c4c4);
|
||||
ColorUIResource altRow = uir(laf.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 = {
|
||||
"Button", "ToggleButton", "CheckBox", "RadioButton",
|
||||
"Label", "Menu", "MenuItem", "CheckBoxMenuItem", "RadioButtonMenuItem",
|
||||
"MenuBar", "PopupMenu", "Panel", "Viewport", "ScrollPane",
|
||||
"Separator", "ToolBar", "ToolTip", "FormattedTextField",
|
||||
"TitledBorder", "TabbedPane", "OptionPane", "ProgressBar",
|
||||
"Spinner", "Slider", "ComboBox", "EditorPane", "PasswordField"
|
||||
};
|
||||
|
||||
for(String c: simpleComponents)
|
||||
{
|
||||
UIManager.put(c + ".background", bgPanel);
|
||||
UIManager.put(c + ".foreground", fgPrimary);
|
||||
UIManager.put(c + ".disabledForeground", fgDisabled);
|
||||
UIManager.put(c + ".disabledText", fgDisabled);
|
||||
}
|
||||
|
||||
for(String c: new String[]{"Button", "ToggleButton", "ComboBox", "Spinner"})
|
||||
{
|
||||
UIManager.put(c + ".background", bgRaised);
|
||||
}
|
||||
|
||||
for(String c: new String[]{"TextField", "TextArea", "TextPane", "EditorPane",
|
||||
"FormattedTextField", "PasswordField"})
|
||||
{
|
||||
UIManager.put(c + ".background", bgInput);
|
||||
UIManager.put(c + ".foreground", fgPrimary);
|
||||
UIManager.put(c + ".caretForeground", fgPrimary);
|
||||
UIManager.put(c + ".inactiveForeground", fgDisabled);
|
||||
UIManager.put(c + ".selectionBackground", bgSelection);
|
||||
UIManager.put(c + ".selectionForeground", fgSelection);
|
||||
}
|
||||
|
||||
for(String c: new String[]{"Table", "Tree", "List"})
|
||||
{
|
||||
UIManager.put(c + ".background", bgInput);
|
||||
UIManager.put(c + ".foreground", fgPrimary);
|
||||
UIManager.put(c + ".selectionBackground", bgSelection);
|
||||
UIManager.put(c + ".selectionForeground", fgSelection);
|
||||
}
|
||||
|
||||
UIManager.put("Table.gridColor", border);
|
||||
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));
|
||||
|
||||
//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);
|
||||
UIManager.put("TabbedPane.selected", bgRaised);
|
||||
UIManager.put("TabbedPane.contentAreaColor", bgPanel);
|
||||
UIManager.put("TabbedPane.darkShadow", border);
|
||||
UIManager.put("TabbedPane.shadow", border);
|
||||
|
||||
UIManager.put("MenuItem.selectionBackground", bgSelection);
|
||||
UIManager.put("MenuItem.selectionForeground", fgSelection);
|
||||
UIManager.put("Menu.selectionBackground", bgSelection);
|
||||
UIManager.put("Menu.selectionForeground", fgSelection);
|
||||
|
||||
UIManager.put("SplitPane.background", bgPanel);
|
||||
UIManager.put("SplitPane.dividerFocusColor", bgSelection);
|
||||
UIManager.put("SplitPaneDivider.draggingColor", bgSelection);
|
||||
UIManager.put("Component.borderColor", border);
|
||||
UIManager.put("Component.disabledBorderColor", border);
|
||||
UIManager.put("Separator.foreground", border);
|
||||
UIManager.put("Separator.background", bgPanel);
|
||||
|
||||
UIManager.put("ToolTip.background", bgRaised);
|
||||
UIManager.put("ToolTip.foreground", fgPrimary);
|
||||
|
||||
//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);
|
||||
UIManager.put("JideTabbedPane.selectedTabBackground", bgRaised);
|
||||
UIManager.put("JideTabbedPane.activeTabBackground", bgRaised);
|
||||
UIManager.put("JideTabbedPane.tabListBackground", bgRaised);
|
||||
UIManager.put("JideTabbedPane.selectedTabTextForeground", fgPrimary);
|
||||
UIManager.put("JideTabbedPane.unselectedTabTextForeground", fgPrimary);
|
||||
UIManager.put("JideTabbedPane.activeTabTextForeground", fgPrimary);
|
||||
UIManager.put("JideTabbedPane.shadow", border);
|
||||
UIManager.put("JideTabbedPane.darkShadow", border);
|
||||
UIManager.put("JideTabbedPane.light", bgRaised);
|
||||
UIManager.put("JideTabbedPane.highlight", bgRaised);
|
||||
|
||||
UIManager.put("JideSplitPane.background", bgPanel);
|
||||
UIManager.put("JideSplitPaneDivider.background", bgPanel);
|
||||
UIManager.put("JideSplitPane.dividerColor", border);
|
||||
|
||||
UIManager.put("JideButton.background", bgRaised);
|
||||
UIManager.put("JideButton.foreground", fgPrimary);
|
||||
UIManager.put("JideButton.selectedBackground", bgSelection);
|
||||
UIManager.put("JideButton.selectedForeground", fgSelection);
|
||||
UIManager.put("JideButton.shadow", border);
|
||||
UIManager.put("JideButton.darkShadow", border);
|
||||
UIManager.put("JideButton.light", bgRaised);
|
||||
UIManager.put("JideButton.highlight", bgRaised);
|
||||
|
||||
UIManager.put("JideLabel.background", bgPanel);
|
||||
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();
|
||||
while(it.hasNext())
|
||||
{
|
||||
Scene existing = it.next().get();
|
||||
if(existing == null)
|
||||
{
|
||||
it.remove();
|
||||
}
|
||||
else if(existing == scene)
|
||||
{
|
||||
it.remove();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
@@ -33,11 +34,21 @@ 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 static final String PREFERENCE_KEY_GUI_SCALE = "ui.gui.scale";
|
||||
|
||||
/** Hard bounds for the GUI zoom factor. */
|
||||
public static final double MIN_GUI_SCALE = 0.5d;
|
||||
public static final double MAX_GUI_SCALE = 2.0d;
|
||||
public static final double DEFAULT_GUI_SCALE = 1.0d;
|
||||
|
||||
private final static Logger mLog = LoggerFactory.getLogger(ApplicationPreference.class);
|
||||
private Preferences mPreferences = Preferences.userNodeForPackage(ApplicationPreference.class);
|
||||
private Integer mChannelAutoStartTimeout;
|
||||
private Boolean mAutomaticDiagnosticMonitoring;
|
||||
private Theme mTheme;
|
||||
private Double mGuiScale;
|
||||
|
||||
/**
|
||||
* Constructs an instance
|
||||
@@ -104,4 +115,88 @@ public class ApplicationPreference extends Preference
|
||||
mPreferences.putBoolean(PREFERENCE_KEY_CHANNEL_AUTO_DIAGNOSTIC_MONITORING, enabled);
|
||||
notifyPreferenceUpdated();
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 Theme getTheme()
|
||||
{
|
||||
if(mTheme == null)
|
||||
{
|
||||
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 mTheme;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the selected UI theme.
|
||||
*/
|
||||
public void setTheme(Theme theme)
|
||||
{
|
||||
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();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the GUI zoom factor. 1.0 = 100% (default). Bounded to
|
||||
* [{@link #MIN_GUI_SCALE}, {@link #MAX_GUI_SCALE}].
|
||||
*/
|
||||
public double getGuiScale()
|
||||
{
|
||||
if(mGuiScale == null)
|
||||
{
|
||||
mGuiScale = mPreferences.getDouble(PREFERENCE_KEY_GUI_SCALE, DEFAULT_GUI_SCALE);
|
||||
mGuiScale = clampScale(mGuiScale);
|
||||
}
|
||||
|
||||
return mGuiScale;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the GUI zoom factor. Out-of-range values are clamped to the supported bounds.
|
||||
*/
|
||||
public void setGuiScale(double scale)
|
||||
{
|
||||
double clamped = clampScale(scale);
|
||||
mGuiScale = clamped;
|
||||
mPreferences.putDouble(PREFERENCE_KEY_GUI_SCALE, clamped);
|
||||
notifyPreferenceUpdated();
|
||||
}
|
||||
|
||||
private static double clampScale(double scale)
|
||||
{
|
||||
if(Double.isNaN(scale) || scale < MIN_GUI_SCALE) return MIN_GUI_SCALE;
|
||||
if(scale > MAX_GUI_SCALE) return MAX_GUI_SCALE;
|
||||
return scale;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -274,7 +274,10 @@ public class TunerViewPanel extends JPanel
|
||||
}
|
||||
else if(tunerStatus == TunerStatus.DISABLED)
|
||||
{
|
||||
component.setForeground(Color.DARK_GRAY);
|
||||
//Use the LAF's disabled-foreground colour so the row reads correctly on any
|
||||
//theme - DARK_GRAY is invisible on dark backgrounds.
|
||||
Color disabled = javax.swing.UIManager.getColor("Label.disabledForeground");
|
||||
component.setForeground(disabled != null ? disabled : table.getForeground());
|
||||
}
|
||||
else
|
||||
{
|
||||
|
||||
458
src/main/resources/sdrtrunk_dark.css
Normal file
458
src/main/resources/sdrtrunk_dark.css
Normal file
@@ -0,0 +1,458 @@
|
||||
/*
|
||||
* *****************************************************************************
|
||||
* 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/>
|
||||
* ****************************************************************************
|
||||
*/
|
||||
|
||||
/* sdrtrunk dark theme - applied on top of the default Modena stylesheet. */
|
||||
|
||||
.root
|
||||
{
|
||||
-fx-base: #2b2b2b;
|
||||
-fx-background: #2b2b2b;
|
||||
-fx-control-inner-background: #313335;
|
||||
-fx-control-inner-background-alt: #3c3f41;
|
||||
-fx-accent: #4a90d9;
|
||||
-fx-default-button: #4a90d9;
|
||||
-fx-focus-color: #4a90d9;
|
||||
-fx-faint-focus-color: #4a90d922;
|
||||
-fx-text-fill: #ececec;
|
||||
-fx-text-base-color: #ececec;
|
||||
-fx-text-background-color: #ececec;
|
||||
-fx-mark-color: #ececec;
|
||||
-fx-mark-highlight-color: #ffffff;
|
||||
-fx-selection-bar: #214283;
|
||||
-fx-selection-bar-non-focused: #3a3d3f;
|
||||
-fx-cell-hover-color: #3a3d3f;
|
||||
}
|
||||
|
||||
.label
|
||||
{
|
||||
-fx-text-fill: #ececec;
|
||||
}
|
||||
|
||||
.text
|
||||
{
|
||||
-fx-fill: #ececec;
|
||||
}
|
||||
|
||||
.button, .toggle-button, .menu-button, .split-menu-button, .choice-box, .combo-box-base
|
||||
{
|
||||
-fx-background-color: #3c3f41, #4a4d4f, #3c3f41, #3c3f41;
|
||||
-fx-text-fill: #ececec;
|
||||
}
|
||||
|
||||
.button:hover, .toggle-button:hover, .menu-button:hover
|
||||
{
|
||||
-fx-background-color: #4a90d9, #5aa0e9, #4a90d9, #3c3f41;
|
||||
}
|
||||
|
||||
.button:armed, .toggle-button:selected
|
||||
{
|
||||
-fx-background-color: #214283, #3672c4, #214283, #214283;
|
||||
-fx-text-fill: #ffffff;
|
||||
}
|
||||
|
||||
.text-field, .text-area, .password-field
|
||||
{
|
||||
-fx-background-color: #313335;
|
||||
-fx-text-fill: #ececec;
|
||||
-fx-prompt-text-fill: #888888;
|
||||
-fx-highlight-fill: #214283;
|
||||
-fx-highlight-text-fill: #ffffff;
|
||||
-fx-border-color: #4f5356;
|
||||
-fx-border-width: 1;
|
||||
}
|
||||
|
||||
.text-area .content
|
||||
{
|
||||
-fx-background-color: #313335;
|
||||
}
|
||||
|
||||
.scroll-pane, .scroll-pane > .viewport
|
||||
{
|
||||
-fx-background-color: #2b2b2b;
|
||||
}
|
||||
|
||||
.scroll-bar
|
||||
{
|
||||
-fx-background-color: #2b2b2b;
|
||||
}
|
||||
|
||||
.scroll-bar .thumb
|
||||
{
|
||||
-fx-background-color: #555a5e;
|
||||
-fx-background-radius: 3;
|
||||
}
|
||||
|
||||
.scroll-bar .thumb:hover
|
||||
{
|
||||
-fx-background-color: #6a7075;
|
||||
}
|
||||
|
||||
.scroll-bar .increment-button, .scroll-bar .decrement-button
|
||||
{
|
||||
-fx-background-color: #2b2b2b;
|
||||
}
|
||||
|
||||
.scroll-bar .increment-arrow, .scroll-bar .decrement-arrow
|
||||
{
|
||||
-fx-background-color: #c0c0c0;
|
||||
}
|
||||
|
||||
.menu-bar
|
||||
{
|
||||
-fx-background-color: #3c3f41;
|
||||
}
|
||||
|
||||
.menu, .menu-item, .context-menu, .menu-bar > .container > .menu-button
|
||||
{
|
||||
-fx-background-color: transparent;
|
||||
}
|
||||
|
||||
.menu-item
|
||||
{
|
||||
-fx-background-color: #3c3f41;
|
||||
}
|
||||
|
||||
.menu-item:focused, .menu-item:hover
|
||||
{
|
||||
-fx-background-color: #4a90d9;
|
||||
}
|
||||
|
||||
.menu-item .label
|
||||
{
|
||||
-fx-text-fill: #ececec;
|
||||
}
|
||||
|
||||
.menu-item:focused .label, .menu-item:hover .label
|
||||
{
|
||||
-fx-text-fill: #ffffff;
|
||||
}
|
||||
|
||||
.context-menu
|
||||
{
|
||||
-fx-background-color: #3c3f41;
|
||||
-fx-border-color: #555a5e;
|
||||
}
|
||||
|
||||
.tab-pane > .tab-header-area > .headers-region > .tab
|
||||
{
|
||||
-fx-background-color: #3c3f41;
|
||||
}
|
||||
|
||||
.tab-pane > .tab-header-area > .headers-region > .tab:selected
|
||||
{
|
||||
-fx-background-color: #2b2b2b;
|
||||
}
|
||||
|
||||
.tab-pane > .tab-header-area > .headers-region > .tab .tab-label
|
||||
{
|
||||
-fx-text-fill: #b0b0b0;
|
||||
}
|
||||
|
||||
.tab-pane > .tab-header-area > .headers-region > .tab:selected .tab-label
|
||||
{
|
||||
-fx-text-fill: #ffffff;
|
||||
}
|
||||
|
||||
.tab-pane > .tab-header-area
|
||||
{
|
||||
-fx-background-color: #2b2b2b;
|
||||
}
|
||||
|
||||
.table-view, .tree-view, .list-view, .tree-table-view
|
||||
{
|
||||
-fx-background-color: #2b2b2b;
|
||||
-fx-control-inner-background: #313335;
|
||||
-fx-control-inner-background-alt: #353739;
|
||||
-fx-table-cell-border-color: #4f5356;
|
||||
-fx-border-color: #4f5356;
|
||||
-fx-border-width: 1;
|
||||
}
|
||||
|
||||
.table-view .column-header, .table-view .filler, .table-view .column-header-background,
|
||||
.tree-table-view .column-header, .tree-table-view .filler, .tree-table-view .column-header-background
|
||||
{
|
||||
-fx-background-color: #3c3f41;
|
||||
}
|
||||
|
||||
.table-view .column-header .label, .tree-table-view .column-header .label
|
||||
{
|
||||
-fx-text-fill: #ececec;
|
||||
}
|
||||
|
||||
.table-row-cell, .tree-table-row-cell, .list-cell, .tree-cell
|
||||
{
|
||||
-fx-text-fill: #ececec;
|
||||
-fx-background-color: -fx-control-inner-background;
|
||||
}
|
||||
|
||||
.table-row-cell:odd, .tree-table-row-cell:odd, .list-cell:odd, .tree-cell:odd
|
||||
{
|
||||
-fx-background-color: -fx-control-inner-background-alt;
|
||||
}
|
||||
|
||||
.table-row-cell:selected, .tree-table-row-cell:selected, .list-cell:selected, .tree-cell:selected
|
||||
{
|
||||
-fx-background-color: #214283;
|
||||
-fx-text-fill: #ffffff;
|
||||
}
|
||||
|
||||
.table-row-cell:filled:hover, .tree-table-row-cell:filled:hover, .list-cell:filled:hover, .tree-cell:filled:hover
|
||||
{
|
||||
-fx-background-color: #3a3d3f;
|
||||
}
|
||||
|
||||
.titled-pane > .title
|
||||
{
|
||||
-fx-background-color: #3c3f41;
|
||||
}
|
||||
|
||||
.titled-pane > .title > .text
|
||||
{
|
||||
-fx-fill: #ececec;
|
||||
}
|
||||
|
||||
.titled-pane > .content
|
||||
{
|
||||
-fx-background-color: #2b2b2b;
|
||||
-fx-border-color: #4f5356;
|
||||
-fx-border-width: 1;
|
||||
}
|
||||
|
||||
.separator .line
|
||||
{
|
||||
-fx-border-color: #4f5356;
|
||||
-fx-border-width: 1 0 0 0;
|
||||
}
|
||||
|
||||
.split-pane
|
||||
{
|
||||
-fx-background-color: #2b2b2b;
|
||||
}
|
||||
|
||||
.split-pane > .split-pane-divider
|
||||
{
|
||||
-fx-background-color: #4f5356;
|
||||
-fx-padding: 0 1 0 1;
|
||||
}
|
||||
|
||||
.tooltip
|
||||
{
|
||||
-fx-background-color: #3c3f41;
|
||||
-fx-text-fill: #ececec;
|
||||
-fx-border-color: #555a5e;
|
||||
}
|
||||
|
||||
.check-box .box, .radio-button .radio
|
||||
{
|
||||
-fx-background-color: #3c3f41, #4a4d4f;
|
||||
}
|
||||
|
||||
.check-box:selected .mark, .radio-button:selected .dot
|
||||
{
|
||||
-fx-background-color: #ffffff;
|
||||
}
|
||||
|
||||
.toggle-switch
|
||||
{
|
||||
-fx-text-fill: #ececec;
|
||||
}
|
||||
|
||||
.toggle-switch > .thumb-area
|
||||
{
|
||||
-fx-background-color: #3c3f41;
|
||||
-fx-border-color: #555a5e;
|
||||
}
|
||||
|
||||
.toggle-switch:selected > .thumb-area
|
||||
{
|
||||
-fx-background-color: #4a90d9;
|
||||
-fx-border-color: #4a90d9;
|
||||
}
|
||||
|
||||
.toggle-switch > .thumb
|
||||
{
|
||||
-fx-background-color: #ececec;
|
||||
}
|
||||
|
||||
.spinner
|
||||
{
|
||||
-fx-background-color: #313335;
|
||||
}
|
||||
|
||||
.spinner .text-field
|
||||
{
|
||||
-fx-background-color: #313335;
|
||||
-fx-text-fill: #ececec;
|
||||
}
|
||||
|
||||
.spinner .increment-arrow-button, .spinner .decrement-arrow-button
|
||||
{
|
||||
-fx-background-color: #3c3f41;
|
||||
}
|
||||
|
||||
.spinner .increment-arrow, .spinner .decrement-arrow
|
||||
{
|
||||
-fx-background-color: #ececec;
|
||||
}
|
||||
|
||||
.progress-bar > .track
|
||||
{
|
||||
-fx-background-color: #3c3f41;
|
||||
}
|
||||
|
||||
.progress-bar > .bar
|
||||
{
|
||||
-fx-background-color: #4a90d9;
|
||||
}
|
||||
|
||||
.combo-box .list-cell, .choice-box .label
|
||||
{
|
||||
-fx-text-fill: #ececec;
|
||||
-fx-background-color: transparent;
|
||||
}
|
||||
|
||||
.combo-box-popup .list-view
|
||||
{
|
||||
-fx-background-color: #3c3f41;
|
||||
-fx-border-color: #555a5e;
|
||||
}
|
||||
|
||||
.hyperlink
|
||||
{
|
||||
-fx-text-fill: #6ab0ff;
|
||||
}
|
||||
|
||||
.hyperlink:visited
|
||||
{
|
||||
-fx-text-fill: #9683c2;
|
||||
}
|
||||
|
||||
.dialog-pane
|
||||
{
|
||||
-fx-background-color: #2b2b2b;
|
||||
}
|
||||
|
||||
.dialog-pane > .button-bar > .container
|
||||
{
|
||||
-fx-background-color: #2b2b2b;
|
||||
}
|
||||
|
||||
.dialog-pane .header-panel
|
||||
{
|
||||
-fx-background-color: #3c3f41;
|
||||
}
|
||||
|
||||
.dialog-pane .header-panel .label
|
||||
{
|
||||
-fx-text-fill: #ffffff;
|
||||
}
|
||||
|
||||
.chart
|
||||
{
|
||||
-fx-background-color: #2b2b2b;
|
||||
-fx-padding: 5px;
|
||||
}
|
||||
|
||||
.chart-content
|
||||
{
|
||||
-fx-background-color: #2b2b2b;
|
||||
}
|
||||
|
||||
.chart-plot-background
|
||||
{
|
||||
-fx-background-color: #1c1c1c;
|
||||
}
|
||||
|
||||
.axis
|
||||
{
|
||||
-fx-tick-label-fill: #ececec;
|
||||
AXIS_COLOR: #4f5356;
|
||||
}
|
||||
|
||||
.axis .axis-tick-mark, .axis .axis-minor-tick-mark
|
||||
{
|
||||
-fx-stroke: #6a6f73;
|
||||
}
|
||||
|
||||
.axis-label
|
||||
{
|
||||
-fx-text-fill: #ececec;
|
||||
}
|
||||
|
||||
.chart-vertical-grid-lines, .chart-horizontal-grid-lines
|
||||
{
|
||||
-fx-stroke: #3c3f41;
|
||||
}
|
||||
|
||||
.chart-alternative-row-fill
|
||||
{
|
||||
-fx-fill: transparent;
|
||||
-fx-stroke: transparent;
|
||||
}
|
||||
|
||||
.chart-alternative-column-fill
|
||||
{
|
||||
-fx-fill: transparent;
|
||||
-fx-stroke: transparent;
|
||||
}
|
||||
|
||||
.chart-legend
|
||||
{
|
||||
-fx-background-color: #313335;
|
||||
-fx-text-fill: #ececec;
|
||||
-fx-border-color: #4f5356;
|
||||
-fx-border-width: 1;
|
||||
}
|
||||
|
||||
.chart-legend-item
|
||||
{
|
||||
-fx-text-fill: #ececec;
|
||||
}
|
||||
|
||||
.chart-title
|
||||
{
|
||||
-fx-text-fill: #ececec;
|
||||
}
|
||||
|
||||
/* Bright high-contrast palette for chart series lines on a dark plot background. */
|
||||
.default-color0.chart-series-line { -fx-stroke: #ff5555; }
|
||||
.default-color1.chart-series-line { -fx-stroke: #55aaff; }
|
||||
.default-color2.chart-series-line { -fx-stroke: #55ff7f; }
|
||||
.default-color3.chart-series-line { -fx-stroke: #ffd34f; }
|
||||
.default-color4.chart-series-line { -fx-stroke: #cf9bff; }
|
||||
.default-color5.chart-series-line { -fx-stroke: #4fe6ff; }
|
||||
.default-color6.chart-series-line { -fx-stroke: #ff7fbf; }
|
||||
.default-color7.chart-series-line { -fx-stroke: #ffffff; }
|
||||
|
||||
/* All chart data points render as bright white dots regardless of series. Lines still carry
|
||||
the per-series colour so they remain distinguishable. Only -fx-background-color is set so
|
||||
the symbol size and shape continue to match Modena's defaults. */
|
||||
.chart-symbol,
|
||||
.chart-line-symbol,
|
||||
.default-color0.chart-symbol, .default-color0.chart-line-symbol,
|
||||
.default-color1.chart-symbol, .default-color1.chart-line-symbol,
|
||||
.default-color2.chart-symbol, .default-color2.chart-line-symbol,
|
||||
.default-color3.chart-symbol, .default-color3.chart-line-symbol,
|
||||
.default-color4.chart-symbol, .default-color4.chart-line-symbol,
|
||||
.default-color5.chart-symbol, .default-color5.chart-line-symbol,
|
||||
.default-color6.chart-symbol, .default-color6.chart-line-symbol,
|
||||
.default-color7.chart-symbol, .default-color7.chart-line-symbol
|
||||
{
|
||||
-fx-background-color: #ffffff;
|
||||
}
|
||||
Reference in New Issue
Block a user