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.
This commit is contained in:
Akumas_Coffin_
2026-05-14 22:29:18 +10:00
parent 8c9bd9bf08
commit 856b17197f
2 changed files with 58 additions and 1 deletions

View File

@@ -102,6 +102,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();
@@ -197,6 +202,8 @@ 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(() -> {

View File

@@ -61,6 +61,7 @@ public class ThemeManager
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 boolean mDarkMode;
@@ -140,6 +141,36 @@ public class ThemeManager
applyToScene(scene, mDarkMode);
}
/**
* Register a Swing component that may be temporarily detached from any Window's component tree
* (e.g. swappable right-pane components in the Channel tab). Plain
* {@link SwingUtilities#updateComponentTreeUI(java.awt.Component)} walks of
* {@link Window#getWindows()} miss such components and they end up carrying stale colours from
* the LAF that was active when they were last attached. Registered components get
* {@code updateComponentTreeUI} called on them whenever the theme is applied.
*/
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));
}
}
/**
* Receives notification when any preference changes. Re-reads the dark-mode setting and
* propagates if it differs from the last known value.
@@ -309,12 +340,31 @@ public class ThemeManager
//near-white rather than FlatLaf's #bbbbbb so text reads sharply on the dark panels.
applyExplicitOverrides(darkMode);
//Re-rendering any already-realized Swing windows must happen on the EDT.
//Re-rendering any already-realized Swing windows must happen on the EDT. Also walk any
//Swing components explicitly registered via registerSwing(...) - they may be detached
//from any Window's tree at toggle time (e.g. swappable channel-tab right components).
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())