diff --git a/src/main/java/io/github/dsheirer/audio/playback/AudioPanel.java b/src/main/java/io/github/dsheirer/audio/playback/AudioPanel.java index 467efcf6..52b1ec26 100644 --- a/src/main/java/io/github/dsheirer/audio/playback/AudioPanel.java +++ b/src/main/java/io/github/dsheirer/audio/playback/AudioPanel.java @@ -32,7 +32,7 @@ import java.awt.EventQueue; import java.awt.event.MouseEvent; import java.awt.event.MouseListener; import jiconfont.icons.font_awesome.FontAwesome; -import jiconfont.swing.IconFontSwing; +import io.github.dsheirer.gui.theme.ThemedFontIcon; import net.miginfocom.swing.MigLayout; import javax.sound.sampled.FloatControl; @@ -152,7 +152,7 @@ public class AudioPanel extends JPanel implements Listener { JPopupMenu popup = new JPopupMenu(); JMenuItem outputMenu = new JMenuItem("Audio Playback Device ..."); - Icon icon = IconFontSwing.buildIcon(FontAwesome.COG, 14); + Icon icon = new ThemedFontIcon(FontAwesome.COG, 14); outputMenu.setIcon(icon); outputMenu.addActionListener(e -> MyEventBus.getGlobalEventBus() .post(new ViewUserPreferenceEditorRequest(PreferenceEditorType.AUDIO_OUTPUT))); @@ -164,7 +164,7 @@ public class AudioPanel extends JPanel implements Listener popup.add(new JPopupMenu.Separator()); JMenuItem volume = new JMenuItem("Audio Volume"); volume.setEnabled(false); - Icon volumeIcon = IconFontSwing.buildIcon(FontAwesome.VOLUME_UP, 14); + Icon volumeIcon = new ThemedFontIcon(FontAwesome.VOLUME_UP, 14); volume.setIcon(volumeIcon); popup.add(volume); popup.add(new VolumeSlider(mAudioPlaybackManager.getAudioOutput().getGainControl())); diff --git a/src/main/java/io/github/dsheirer/controller/ControllerPanel.java b/src/main/java/io/github/dsheirer/controller/ControllerPanel.java index 79a7e707..c68e9570 100644 --- a/src/main/java/io/github/dsheirer/controller/ControllerPanel.java +++ b/src/main/java/io/github/dsheirer/controller/ControllerPanel.java @@ -32,10 +32,9 @@ import io.github.dsheirer.preference.UserPreferences; import io.github.dsheirer.settings.SettingsManager; import io.github.dsheirer.source.tuner.manager.TunerManager; import io.github.dsheirer.source.tuner.ui.TunerViewPanel; -import java.awt.Color; import java.awt.Dimension; import jiconfont.icons.font_awesome.FontAwesome; -import jiconfont.swing.IconFontSwing; +import io.github.dsheirer.gui.theme.ThemedFontIcon; import net.miginfocom.swing.MigLayout; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -104,7 +103,7 @@ public class ControllerPanel extends JPanel mTabbedPane.addTab("Map", mMapPanel); mTabbedPane.addTab("Tuners", mTunerManagerPanel); - Icon playIcon = IconFontSwing.buildIcon(FontAwesome.PLAY_CIRCLE_O, 20, Color.DARK_GRAY); + Icon playIcon = new ThemedFontIcon(FontAwesome.PLAY_CIRCLE_O, 20); mTabbedPane.addTab("Playlist Editor", playIcon, new JLabel("Show Playlist Manager")); mSettingsTabIndex = mTabbedPane.getTabCount() - 1; diff --git a/src/main/java/io/github/dsheirer/gui/SDRTrunk.java b/src/main/java/io/github/dsheirer/gui/SDRTrunk.java index 1585ec12..137b4f21 100644 --- a/src/main/java/io/github/dsheirer/gui/SDRTrunk.java +++ b/src/main/java/io/github/dsheirer/gui/SDRTrunk.java @@ -38,6 +38,7 @@ 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.theme.ThemedFontIcon; import io.github.dsheirer.gui.viewer.ViewRecordingViewerRequest; import io.github.dsheirer.icon.IconModel; import io.github.dsheirer.log.ApplicationLog; @@ -468,14 +469,14 @@ public class SDRTrunk implements Listener JMenu viewMenu = new JMenu("View"); JMenuItem viewPlaylistItem = new JMenuItem("Playlist Editor"); - viewPlaylistItem.setIcon(IconFontSwing.buildIcon(FontAwesome.PLAY_CIRCLE_O, 12)); + viewPlaylistItem.setIcon(new ThemedFontIcon(FontAwesome.PLAY_CIRCLE_O, 12)); viewPlaylistItem.addActionListener(e -> MyEventBus.getGlobalEventBus().post(new ViewPlaylistRequest())); viewMenu.add(viewPlaylistItem); viewMenu.add(new JSeparator()); JMenuItem viewApplicationLogsMenu = new JMenuItem("Application Log Files"); - viewApplicationLogsMenu.setIcon(IconFontSwing.buildIcon(FontAwesome.FOLDER_OPEN_O, 12)); + viewApplicationLogsMenu.setIcon(new ThemedFontIcon(FontAwesome.FOLDER_OPEN_O, 12)); viewApplicationLogsMenu.addActionListener(arg0 -> { File logsDirectory = mUserPreferences.getDirectoryPreference().getDirectoryApplicationLog().toFile(); try @@ -495,7 +496,7 @@ public class SDRTrunk implements Listener viewMenu.add(viewApplicationLogsMenu); JMenuItem viewRecordingsMenuItem = new JMenuItem("Audio Recordings"); - viewRecordingsMenuItem.setIcon(IconFontSwing.buildIcon(FontAwesome.FOLDER_OPEN_O, 12)); + viewRecordingsMenuItem.setIcon(new ThemedFontIcon(FontAwesome.FOLDER_OPEN_O, 12)); viewRecordingsMenuItem.addActionListener(arg0 -> { File recordingsDirectory = mUserPreferences.getDirectoryPreference().getDirectoryRecording().toFile(); @@ -517,7 +518,7 @@ public class SDRTrunk implements Listener viewMenu.add(viewRecordingsMenuItem); JMenuItem viewEventLogsMenu = new JMenuItem("Channel Event Log Files"); - viewEventLogsMenu.setIcon(IconFontSwing.buildIcon(FontAwesome.FOLDER_OPEN_O, 12)); + viewEventLogsMenu.setIcon(new ThemedFontIcon(FontAwesome.FOLDER_OPEN_O, 12)); viewEventLogsMenu.addActionListener(arg0 -> { File eventLogsDirectory = mUserPreferences.getDirectoryPreference().getDirectoryEventLog().toFile(); try @@ -537,17 +538,17 @@ public class SDRTrunk implements Listener viewMenu.add(viewEventLogsMenu); JMenuItem iconManagerMenu = new JMenuItem("Icon Manager"); - iconManagerMenu.setIcon(IconFontSwing.buildIcon(FontAwesome.PICTURE_O, 12)); + iconManagerMenu.setIcon(new ThemedFontIcon(FontAwesome.PICTURE_O, 12)); iconManagerMenu.addActionListener(arg0 -> MyEventBus.getGlobalEventBus().post(new ViewIconManagerRequest())); viewMenu.add(iconManagerMenu); JMenuItem recordingViewerMenu = new JMenuItem("Message Recording Viewer (.bits)"); - recordingViewerMenu.setIcon(IconFontSwing.buildIcon(FontAwesome.BRAILLE, 12)); + recordingViewerMenu.setIcon(new ThemedFontIcon(FontAwesome.BRAILLE, 12)); recordingViewerMenu.addActionListener(e -> MyEventBus.getGlobalEventBus().post(new ViewRecordingViewerRequest())); viewMenu.add(recordingViewerMenu); JMenuItem viewScreenCapturesMenu = new JMenuItem("Screen Captures"); - viewScreenCapturesMenu.setIcon(IconFontSwing.buildIcon(FontAwesome.FOLDER_OPEN_O, 12)); + viewScreenCapturesMenu.setIcon(new ThemedFontIcon(FontAwesome.FOLDER_OPEN_O, 12)); viewScreenCapturesMenu.addActionListener(arg0 -> { File screenCapturesDirectory = mUserPreferences.getDirectoryPreference().getDirectoryScreenCapture().toFile(); try @@ -567,7 +568,7 @@ public class SDRTrunk implements Listener viewMenu.add(viewScreenCapturesMenu); JMenuItem preferencesItem = new JMenuItem("User Preferences"); - preferencesItem.setIcon(IconFontSwing.buildIcon(FontAwesome.COG, 12)); + preferencesItem.setIcon(new ThemedFontIcon(FontAwesome.COG, 12)); preferencesItem.addActionListener(e -> MyEventBus.getGlobalEventBus().post(new ViewUserPreferenceEditorRequest())); viewMenu.add(preferencesItem); @@ -582,7 +583,7 @@ public class SDRTrunk implements Listener menuBar.add(viewMenu); JMenuItem screenCaptureItem = new JMenuItem("Screen Capture"); - screenCaptureItem.setIcon(IconFontSwing.buildIcon(FontAwesome.CAMERA, 12)); + screenCaptureItem.setIcon(new ThemedFontIcon(FontAwesome.CAMERA, 12)); screenCaptureItem.setMnemonic(KeyEvent.VK_C); screenCaptureItem.setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_C, ActionEvent.ALT_MASK)); screenCaptureItem.setMaximumSize(screenCaptureItem.getPreferredSize()); diff --git a/src/main/java/io/github/dsheirer/gui/theme/ThemedFontIcon.java b/src/main/java/io/github/dsheirer/gui/theme/ThemedFontIcon.java new file mode 100644 index 00000000..aa9aecf6 --- /dev/null +++ b/src/main/java/io/github/dsheirer/gui/theme/ThemedFontIcon.java @@ -0,0 +1,178 @@ +/* + * ***************************************************************************** + * 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 + * **************************************************************************** + */ + +package io.github.dsheirer.gui.theme; + +import com.formdev.flatlaf.FlatLaf; +import java.awt.Color; +import java.awt.Component; +import java.awt.Graphics; +import javax.swing.ButtonModel; +import javax.swing.Icon; +import javax.swing.JMenu; +import javax.swing.JMenuItem; +import javax.swing.UIManager; +import jiconfont.IconCode; +import jiconfont.swing.IconFontSwing; + +/** + * Swing font-glyph icon whose colour follows the active theme. + * + *

{@link IconFontSwing#buildIcon(IconCode, float)} bakes a fixed colour (black by default) into + * the rendered image, so those icons never change when the user switches themes and disappear on + * dark palettes. This icon resolves its colour from {@link UIManager} each time it is painted and + * only re-renders the glyph when that colour changes, so it automatically tracks theme changes + * applied by {@link ThemeManager}. + * + *

Colour resolution: + *

    + *
  • Disabled component: {@code Label.disabledForeground}
  • + *
  • Armed/selected menu item: {@code MenuItem.selectionForeground}
  • + *
  • Otherwise: the configured foreground key (default {@code Label.foreground})
  • + *
+ */ +public class ThemedFontIcon implements Icon, FlatLaf.DisabledIconProvider +{ + private static final String DEFAULT_FOREGROUND_KEY = "Label.foreground"; + private static final String DISABLED_FOREGROUND_KEY = "Label.disabledForeground"; + private static final String SELECTION_FOREGROUND_KEY = "MenuItem.selectionForeground"; + + private final IconCode mIconCode; + private final float mSize; + private final String mForegroundKey; + private final boolean mForceDisabled; + private Color mCachedColor; + private Icon mCachedIcon; + private ThemedFontIcon mDisabledIcon; + + /** + * Constructs a theme-aware glyph icon using the standard label foreground colour. + * @param iconCode glyph to render + * @param size in points + */ + public ThemedFontIcon(IconCode iconCode, float size) + { + this(iconCode, size, DEFAULT_FOREGROUND_KEY); + } + + /** + * Constructs a theme-aware glyph icon using the specified UIManager colour key. + * @param iconCode glyph to render + * @param size in points + * @param foregroundKey UIManager colour key used for the normal (enabled) state + */ + public ThemedFontIcon(IconCode iconCode, float size, String foregroundKey) + { + this(iconCode, size, foregroundKey, false); + } + + private ThemedFontIcon(IconCode iconCode, float size, String foregroundKey, boolean forceDisabled) + { + mIconCode = iconCode; + mSize = size; + mForegroundKey = foregroundKey; + mForceDisabled = forceDisabled; + } + + @Override + public void paintIcon(Component c, Graphics g, int x, int y) + { + getIcon(resolveColor(c)).paintIcon(c, g, x, y); + } + + //Glyph dimensions do not depend on colour, so size queries reuse whatever is cached rather than + //forcing a re-render in a different colour between paints. + @Override + public int getIconWidth() + { + return getSizingIcon().getIconWidth(); + } + + @Override + public int getIconHeight() + { + return getSizingIcon().getIconHeight(); + } + + private synchronized Icon getSizingIcon() + { + return mCachedIcon != null ? mCachedIcon : getIcon(resolveColor(null)); + } + + /** + * Disabled variant used by FlatLaf when the owning component is disabled. + */ + @Override + public Icon getDisabledIcon() + { + if(mDisabledIcon == null) + { + mDisabledIcon = new ThemedFontIcon(mIconCode, mSize, mForegroundKey, true); + } + + return mDisabledIcon; + } + + /** + * Determines the colour to paint with from the current theme and component state. + */ + private Color resolveColor(Component c) + { + if(mForceDisabled || (c != null && !c.isEnabled())) + { + return color(DISABLED_FOREGROUND_KEY, Color.GRAY); + } + + //Menu items render their text in the selection foreground while armed (hovered) - match it + //so the glyph stays legible against the selection background. Top-level menus on the menu + //bar report as 'selected' while their popup is open. + if(c instanceof JMenuItem menuItem) + { + ButtonModel model = menuItem.getModel(); + boolean highlighted = model.isArmed() || (menuItem instanceof JMenu && model.isSelected()); + + if(highlighted) + { + return color(SELECTION_FOREGROUND_KEY, color(mForegroundKey, Color.BLACK)); + } + } + + return color(mForegroundKey, Color.BLACK); + } + + private static Color color(String key, Color fallback) + { + Color color = UIManager.getColor(key); + return color != null ? color : fallback; + } + + /** + * Returns the glyph rendered in the requested colour, re-rendering only when the colour changes. + */ + private synchronized Icon getIcon(Color color) + { + if(mCachedIcon == null || !color.equals(mCachedColor)) + { + mCachedIcon = IconFontSwing.buildIcon(mIconCode, mSize, color); + mCachedColor = color; + } + + return mCachedIcon; + } +} diff --git a/src/main/java/io/github/dsheirer/spectrum/DisableSpectrumWaterfallMenuItem.java b/src/main/java/io/github/dsheirer/spectrum/DisableSpectrumWaterfallMenuItem.java index 7fcc22b7..3d6a302e 100644 --- a/src/main/java/io/github/dsheirer/spectrum/DisableSpectrumWaterfallMenuItem.java +++ b/src/main/java/io/github/dsheirer/spectrum/DisableSpectrumWaterfallMenuItem.java @@ -20,7 +20,7 @@ package io.github.dsheirer.spectrum; import io.github.dsheirer.properties.SystemProperties; import jiconfont.icons.font_awesome.FontAwesome; -import jiconfont.swing.IconFontSwing; +import io.github.dsheirer.gui.theme.ThemedFontIcon; import javax.swing.JMenuItem; @@ -38,7 +38,7 @@ public class DisableSpectrumWaterfallMenuItem extends JMenuItem public DisableSpectrumWaterfallMenuItem(SpectralDisplayPanel spectralDisplayPanel) { super("Disable Spectrum & Waterfall"); - setIcon(IconFontSwing.buildIcon(FontAwesome.EYE_SLASH, 12)); + setIcon(new ThemedFontIcon(FontAwesome.EYE_SLASH, 12)); mSpectralDisplayPanel = spectralDisplayPanel;