diff --git a/flatlaf-demo/src/main/java/com/formdev/flatlaf/demo/extras/ExtrasPanel.java b/flatlaf-demo/src/main/java/com/formdev/flatlaf/demo/extras/ExtrasPanel.java index 37cfec1f..34d79a7d 100644 --- a/flatlaf-demo/src/main/java/com/formdev/flatlaf/demo/extras/ExtrasPanel.java +++ b/flatlaf-demo/src/main/java/com/formdev/flatlaf/demo/extras/ExtrasPanel.java @@ -18,8 +18,13 @@ package com.formdev.flatlaf.demo.extras; import javax.swing.*; import com.formdev.flatlaf.extras.*; +import com.formdev.flatlaf.extras.FlatSVGIcon.ColorFilter; import com.formdev.flatlaf.extras.components.FlatTriStateCheckBox; +import com.formdev.flatlaf.util.HSLColor; import net.miginfocom.swing.*; +import java.awt.*; +import java.awt.event.HierarchyEvent; +import java.util.function.Function; /** * @author Karl Tauber @@ -27,6 +32,9 @@ import net.miginfocom.swing.*; public class ExtrasPanel extends JPanel { + private Timer rainbowIconTimer; + private int rainbowCounter = 0; + public ExtrasPanel() { initComponents(); @@ -50,6 +58,34 @@ public class ExtrasPanel addSVGIcon( "errorDialog.svg" ); addSVGIcon( "informationDialog.svg" ); addSVGIcon( "warningDialog.svg" ); + + initRainbowIcon(); + } + + private void initRainbowIcon() { + FlatSVGIcon icon = new FlatSVGIcon( "com/formdev/flatlaf/demo/extras/svg/informationDialog.svg" ); + icon.setColorFilter( new ColorFilter( color -> { + rainbowCounter += 1; + rainbowCounter %= 255; + return Color.getHSBColor( rainbowCounter / 255f, 1, 1 ); + } ) ); + rainbowIcon.setIcon( icon ); + + rainbowIconTimer = new Timer( 30, e -> { + rainbowIcon.repaint(); + } ); + + // start rainbow timer only if panel is shown ("Extras" tab is active) + addHierarchyListener( e -> { + if( e.getID() == HierarchyEvent.HIERARCHY_CHANGED && + (e.getChangeFlags() & HierarchyEvent.SHOWING_CHANGED) != 0 ) + { + if( isShowing() ) + rainbowIconTimer.start(); + else + rainbowIconTimer.stop(); + } + } ); } private void addSVGIcon( String name ) { @@ -60,6 +96,36 @@ public class ExtrasPanel triStateLabel1.setText( triStateCheckBox1.getState().toString() ); } + private void redChanged() { + brighterToggleButton.setSelected( false ); + + Function mapper = null; + if( redToggleButton.isSelected() ) { + float[] redHSL = HSLColor.fromRGB( Color.red ); + mapper = color -> { + float[] hsl = HSLColor.fromRGB( color ); + return HSLColor.toRGB( redHSL[0], 70, hsl[2] ); + }; + } + FlatSVGIcon.ColorFilter.getInstance().setMapper( mapper ); + + // repaint whole application window because global color filter also affects + // icons in menubar, toolbar, etc. + SwingUtilities.windowForComponent( this ).repaint(); + } + + private void brighterChanged() { + redToggleButton.setSelected( false ); + + FlatSVGIcon.ColorFilter.getInstance().setMapper( brighterToggleButton.isSelected() + ? color -> color.brighter().brighter() + : null ); + + // repaint whole application window because global color filter also affects + // icons in menubar, toolbar, etc. + SwingUtilities.windowForComponent( this ).repaint(); + } + private void initComponents() { // JFormDesigner - Component initialization - DO NOT MODIFY //GEN-BEGIN:initComponents label4 = new JLabel(); @@ -69,6 +135,13 @@ public class ExtrasPanel label2 = new JLabel(); svgIconsPanel = new JPanel(); label3 = new JLabel(); + separator1 = new JSeparator(); + label5 = new JLabel(); + label6 = new JLabel(); + rainbowIcon = new JLabel(); + label7 = new JLabel(); + redToggleButton = new JToggleButton(); + brighterToggleButton = new JToggleButton(); //======== this ======== setLayout(new MigLayout( @@ -81,6 +154,10 @@ public class ExtrasPanel "[]para" + "[]" + "[]" + + "[]" + + "[]" + + "[]" + + "[]" + "[]")); //---- label4 ---- @@ -119,6 +196,30 @@ public class ExtrasPanel //---- label3 ---- label3.setText("The icons may change colors when switching to another theme."); add(label3, "cell 1 3 2 1"); + add(separator1, "cell 1 4 2 1,growx"); + + //---- label5 ---- + label5.setText("Color filters can be also applied to icons. Globally or for each instance."); + add(label5, "cell 1 5 2 1"); + + //---- label6 ---- + label6.setText("Rainbow color filter"); + add(label6, "cell 1 6 2 1"); + add(rainbowIcon, "cell 1 6 2 1"); + + //---- label7 ---- + label7.setText("Global icon color filter"); + add(label7, "cell 1 7 2 1"); + + //---- redToggleButton ---- + redToggleButton.setText("Toggle RED"); + redToggleButton.addActionListener(e -> redChanged()); + add(redToggleButton, "cell 1 7 2 1"); + + //---- brighterToggleButton ---- + brighterToggleButton.setText("Toggle brighter"); + brighterToggleButton.addActionListener(e -> brighterChanged()); + add(brighterToggleButton, "cell 1 7 2 1"); // JFormDesigner - End of component initialization //GEN-END:initComponents } @@ -130,5 +231,12 @@ public class ExtrasPanel private JLabel label2; private JPanel svgIconsPanel; private JLabel label3; + private JSeparator separator1; + private JLabel label5; + private JLabel label6; + private JLabel rainbowIcon; + private JLabel label7; + private JToggleButton redToggleButton; + private JToggleButton brighterToggleButton; // JFormDesigner - End of variables declaration //GEN-END:variables } diff --git a/flatlaf-demo/src/main/java/com/formdev/flatlaf/demo/extras/ExtrasPanel.jfd b/flatlaf-demo/src/main/java/com/formdev/flatlaf/demo/extras/ExtrasPanel.jfd index a121bc65..8fe606f3 100644 --- a/flatlaf-demo/src/main/java/com/formdev/flatlaf/demo/extras/ExtrasPanel.jfd +++ b/flatlaf-demo/src/main/java/com/formdev/flatlaf/demo/extras/ExtrasPanel.jfd @@ -1,4 +1,4 @@ -JFDML JFormDesigner: "7.0.2.0.298" Java: "14" encoding: "UTF-8" +JFDML JFormDesigner: "7.0.3.1.342" Java: "16" encoding: "UTF-8" new FormModel { contentType: "form/swing" @@ -6,7 +6,7 @@ new FormModel { add( new FormContainer( "javax.swing.JPanel", new FormLayoutManager( class net.miginfocom.swing.MigLayout ) { "$layoutConstraints": "insets dialog,hidemode 3" "$columnConstraints": "[][][left]" - "$rowConstraints": "[]para[][][]" + "$rowConstraints": "[]para[][][][][][][]" } ) { name: "this" add( new FormComponent( "javax.swing.JLabel" ) { @@ -56,6 +56,48 @@ new FormModel { }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { "value": "cell 1 3 2 1" } ) + add( new FormComponent( "javax.swing.JSeparator" ) { + name: "separator1" + }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { + "value": "cell 1 4 2 1,growx" + } ) + add( new FormComponent( "javax.swing.JLabel" ) { + name: "label5" + "text": "Color filters can be also applied to icons. Globally or for each instance." + }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { + "value": "cell 1 5 2 1" + } ) + add( new FormComponent( "javax.swing.JLabel" ) { + name: "label6" + "text": "Rainbow color filter" + }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { + "value": "cell 1 6 2 1" + } ) + add( new FormComponent( "javax.swing.JLabel" ) { + name: "rainbowIcon" + }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { + "value": "cell 1 6 2 1" + } ) + add( new FormComponent( "javax.swing.JLabel" ) { + name: "label7" + "text": "Global icon color filter" + }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { + "value": "cell 1 7 2 1" + } ) + add( new FormComponent( "javax.swing.JToggleButton" ) { + name: "redToggleButton" + "text": "Toggle RED" + addEvent( new FormEvent( "java.awt.event.ActionListener", "actionPerformed", "redChanged", false ) ) + }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { + "value": "cell 1 7 2 1" + } ) + add( new FormComponent( "javax.swing.JToggleButton" ) { + name: "brighterToggleButton" + "text": "Toggle brighter" + addEvent( new FormEvent( "java.awt.event.ActionListener", "actionPerformed", "brighterChanged", false ) ) + }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { + "value": "cell 1 7 2 1" + } ) }, new FormLayoutConstraints( null ) { "location": new java.awt.Point( 0, 0 ) "size": new java.awt.Dimension( 500, 300 ) diff --git a/flatlaf-extras/src/main/java/com/formdev/flatlaf/extras/FlatSVGIcon.java b/flatlaf-extras/src/main/java/com/formdev/flatlaf/extras/FlatSVGIcon.java index ff7655b4..f97fa5a3 100644 --- a/flatlaf-extras/src/main/java/com/formdev/flatlaf/extras/FlatSVGIcon.java +++ b/flatlaf-extras/src/main/java/com/formdev/flatlaf/extras/FlatSVGIcon.java @@ -29,6 +29,7 @@ import java.awt.image.BufferedImage; import java.awt.image.RGBImageFilter; import java.net.URISyntaxException; import java.net.URL; +import java.util.Collections; import java.util.HashMap; import java.util.Map; import java.util.function.Function; @@ -66,6 +67,8 @@ public class FlatSVGIcon private final boolean disabled; private final ClassLoader classLoader; + private ColorFilter colorFilter; + private SVGDiagram diagram; private boolean dark; @@ -159,13 +162,76 @@ public class FlatSVGIcon this( name, -1, -1, scale, false, classLoader ); } - private FlatSVGIcon( String name, int width, int height, float scale, boolean disabled, ClassLoader classLoader ) { + protected FlatSVGIcon( String name, int width, int height, float scale, boolean disabled, ClassLoader classLoader ) { this.name = name; - this.classLoader = classLoader; this.width = width; this.height = height; this.scale = scale; this.disabled = disabled; + this.classLoader = classLoader; + } + + /** + * Returns the name of the SVG resource (a '/'-separated path). + * + * @since 1.2 + */ + public String getName() { + return name; + } + + /** + * Returns the custom icon width specified in {@link #FlatSVGIcon(String, int, int)}, + * {@link #FlatSVGIcon(String, int, int, ClassLoader)} or {@link #derive(int, int)}. + * Otherwise {@code -1} is returned. + *

+ * To get the painted icon width, use {@link #getIconWidth()}. + * + * @since 1.2 + */ + public int getWidth() { + return width; + } + + /** + * Returns the custom icon height specified in {@link #FlatSVGIcon(String, int, int)}, + * {@link #FlatSVGIcon(String, int, int, ClassLoader)} or {@link #derive(int, int)}. + * Otherwise {@code -1} is returned. + *

+ * To get the painted icon height, use {@link #getIconHeight()}. + * + * @since 1.2 + */ + public int getHeight() { + return height; + } + + /** + * Returns the amount by which the icon size is scaled. Usually {@code 1}. + * + * @since 1.2 + */ + public float getScale() { + return scale; + } + + /** + * Returns whether the icon is pained in "disabled" state. + * + * @see #getDisabledIcon() + * @since 1.2 + */ + public boolean isDisabled() { + return disabled; + } + + /** + * Returns the class loader used to load the SVG resource. + * + * @since 1.2 + */ + public ClassLoader getClassLoader() { + return classLoader; } /** @@ -179,7 +245,7 @@ public class FlatSVGIcon if( width == this.width && height == this.height ) return this; - FlatSVGIcon icon = new FlatSVGIcon( name, width, height, scale, false, classLoader ); + FlatSVGIcon icon = new FlatSVGIcon( name, width, height, scale, disabled, classLoader ); icon.diagram = diagram; icon.dark = dark; return icon; @@ -195,7 +261,7 @@ public class FlatSVGIcon if( scale == this.scale ) return this; - FlatSVGIcon icon = new FlatSVGIcon( name, width, height, scale, false, classLoader ); + FlatSVGIcon icon = new FlatSVGIcon( name, width, height, scale, disabled, classLoader ); icon.diagram = diagram; icon.dark = dark; return icon; @@ -217,6 +283,36 @@ public class FlatSVGIcon return icon; } + /** + * Returns the currently active color filter or {@code null}. + * + * @since 1.2 + */ + public ColorFilter getColorFilter() { + return colorFilter; + } + + /** + * Sets a color filter that can freely modify colors of this icon during painting. + *

+ * This method accepts a {@link ColorFilter}. Usually you would want to use a ColorFilter created using the + * {@link ColorFilter#ColorFilter(Function)} constructor. + *

+ * This can be used to brighten colors of the icon: + *

icon.setColorFilter( new FlatSVGIcon.ColorFilter( color -> color.brighter() ) );
+ *

+ * Using a filter, icons can also be turned monochrome (painted with a single color): + *

icon.setColorFilter( new FlatSVGIcon.ColorFilter( color -> Color.RED ) );
+ *

+ * Note: If a filter is already set, it will be replaced. + * + * @param colorFilter The color filter + * @since 1.2 + */ + public void setColorFilter( ColorFilter colorFilter ) { + this.colorFilter = colorFilter; + } + private void update() { if( dark == isDarkLaf() && diagram != null ) return; @@ -303,7 +399,7 @@ public class FlatSVGIcon : GrayFilter.createDisabledIconFilter( dark ); } - Graphics2D g2 = new GraphicsFilter( (Graphics2D) g.create(), ColorFilter.getInstance(), grayFilter ); + Graphics2D g2 = new GraphicsFilter( (Graphics2D) g.create(), colorFilter, ColorFilter.getInstance(), grayFilter ); try { FlatUIUtils.setRenderingHints( g2 ); @@ -383,7 +479,7 @@ public class FlatSVGIcon private static Boolean darkLaf; - private static boolean isDarkLaf() { + public static boolean isDarkLaf() { if( darkLaf == null ) { lafChanged(); @@ -401,53 +497,253 @@ public class FlatSVGIcon //---- class ColorFilter -------------------------------------------------- + /** + * A color filter that can modify colors of a painted {@link FlatSVGIcon}. + *

+ * The ColorFilter modifies color in two ways. + * Either using a color map, where specific colors are mapped to different ones. + * And/or by modifying the colors in a mapper function. + *

+ * When filtering a color, mappings are applied first, then the mapper function is applied. + *

+ * Global {@link FlatSVGIcon} ColorFilter can be retrieved using the {@link ColorFilter#getInstance()} method. + */ public static class ColorFilter { private static ColorFilter instance; - private final Map rgb2keyMap = new HashMap<>(); - private final Map color2colorMap = new HashMap<>(); + private Map rgb2keyMap; + private Map colorMap; + private Map darkColorMap; + private Function mapper; + /** + * Returns the global ColorFilter that is applied to all icons. + */ public static ColorFilter getInstance() { - if( instance == null ) + if( instance == null ) { instance = new ColorFilter(); + + // add default color palette + instance.rgb2keyMap = new HashMap<>(); + for( FlatIconColors c : FlatIconColors.values() ) + instance.rgb2keyMap.put( c.rgb, c.key ); + } return instance; } + /** + * Creates an empty color filter. + */ public ColorFilter() { - for( FlatIconColors c : FlatIconColors.values() ) - rgb2keyMap.put( c.rgb, c.key ); } - public void addAll( Map from2toMap ) { - color2colorMap.putAll( from2toMap ); + /** + * Creates a color filter with a color modifying function that changes painted colors. + * The {@link Function} gets passed the original color and returns a modified one. + *

+ * Examples: + * A ColorFilter can be used to brighten colors of the icon: + *

new ColorFilter( color -> color.brighter() );
+ *

+ * Using a ColorFilter, icons can also be turned monochrome (painted with a single color): + *

new ColorFilter( color -> Color.RED );
+ * + * @param mapper The color mapper function + * @since 1.2 + */ + public ColorFilter( Function mapper ) { + setMapper( mapper ); } - public void add( Color from, Color to ) { - color2colorMap.put( from, to ); + /** + * Returns a color modifying function or {@code null} + * + * @since 1.2 + */ + public Function getMapper() { + return mapper; } - public void remove( Color from ) { - color2colorMap.remove( from ); + /** + * Sets a color modifying function that changes painted colors. + * The {@link Function} gets passed the original color and returns a modified one. + *

+ * Examples: + * A ColorFilter can be used to brighten colors of the icon: + *

filter.setMapper( color -> color.brighter() );
+ *

+ * Using a ColorFilter, icons can also be turned monochrome (painted with a single color): + *

filter.setMapper( color -> Color.RED );
+ * + * @param mapper The color mapper function + * @since 1.2 + */ + public void setMapper( Function mapper ) { + this.mapper = mapper; + } + + /** + * Returns the color mappings used for light themes. + * + * @since 1.2 + */ + public Map getLightColorMap() { + return (colorMap != null) + ? Collections.unmodifiableMap( colorMap ) + : Collections.emptyMap(); + } + + /** + * Returns the color mappings used for dark themes. + * + * @since 1.2 + */ + public Map getDarkColorMap() { + return (darkColorMap != null) + ? Collections.unmodifiableMap( darkColorMap ) + : getLightColorMap(); + } + + /** + * Adds color mappings. Used for light and dark themes. + */ + public ColorFilter addAll( Map from2toMap ) { + ensureColorMap(); + + colorMap.putAll( from2toMap ); + if( darkColorMap != null ) + darkColorMap.putAll( from2toMap ); + return this; + } + + /** + * Adds a color mappings, which has different colors for light and dark themes. + * + * @since 1.2 + */ + public ColorFilter addAll( Map from2toLightMap, Map from2toDarkMap ) { + ensureColorMap(); + ensureDarkColorMap(); + + colorMap.putAll( from2toLightMap ); + darkColorMap.putAll( from2toDarkMap ); + return this; + } + + /** + * Adds a color mapping. Used for light and dark themes. + */ + public ColorFilter add( Color from, Color to ) { + ensureColorMap(); + + colorMap.put( from, to ); + if( darkColorMap != null ) + darkColorMap.put( from, to ); + return this; + } + + /** + * Adds a color mapping, which has different colors for light and dark themes. + * + * @since 1.2 + */ + public ColorFilter add( Color from, Color toLight, Color toDark ) { + ensureColorMap(); + ensureDarkColorMap(); + + if( toLight != null ) + colorMap.put( from, toLight ); + if( toDark != null ) + darkColorMap.put( from, toDark ); + return this; + } + + /** + * Removes a specific color mapping. + */ + public ColorFilter remove( Color from ) { + if( colorMap != null ) + colorMap.remove( from ); + if( darkColorMap != null ) + darkColorMap.remove( from ); + return this; + } + + /** + * Removes all color mappings. + * + * @since 1.2 + */ + public ColorFilter removeAll() { + colorMap = null; + darkColorMap = null; + return this; + } + + private void ensureColorMap() { + if( colorMap == null ) + colorMap = new HashMap<>(); + } + + private void ensureDarkColorMap() { + if( darkColorMap == null ) + darkColorMap = new HashMap<>( colorMap ); } public Color filter( Color color ) { - Color newColor = color2colorMap.get( color ); - if( newColor != null ) - return newColor; + // apply mappings + color = applyMappings( color ); - String colorKey = rgb2keyMap.get( color.getRGB() & 0xffffff ); - if( colorKey == null ) - return color; + // apply mapper function + if( mapper != null ) + color = mapper.apply( color ); - newColor = UIManager.getColor( colorKey ); - if( newColor == null ) - return color; - - return (newColor.getAlpha() != color.getAlpha()) - ? new Color( (newColor.getRGB() & 0x00ffffff) | (color.getRGB() & 0xff000000) ) - : newColor; + return color; }; + + private Color applyMappings( Color color ) { + if( colorMap != null ) { + Map map = (darkColorMap != null && isDarkLaf()) ? darkColorMap : colorMap; + Color newColor = map.get( color ); + if( newColor != null ) + return newColor; + } + + if( rgb2keyMap != null ) { + // RGB is mapped to a key in UI defaults, which contains the real color. + // IntelliJ themes define such theme specific icon colors in .theme.json files. + String colorKey = rgb2keyMap.get( color.getRGB() & 0xffffff ); + if( colorKey == null ) + return color; + + Color newColor = UIManager.getColor( colorKey ); + if( newColor == null ) + return color; + + // preserve alpha of original color + return (newColor.getAlpha() != color.getAlpha()) + ? new Color( (newColor.getRGB() & 0x00ffffff) | (color.getRGB() & 0xff000000) ) + : newColor; + } + + return color; + } + + /** + * Creates a color modifying function that uses {@link RGBImageFilter#filterRGB(int, int, int)}. + * Can be set to a {@link ColorFilter} using {@link ColorFilter#setMapper(Function)}. + * + * @see GrayFilter + * @since 1.2 + */ + public static Function createRGBImageFilterFunction( RGBImageFilter rgbImageFilter ) { + return color -> { + int oldRGB = color.getRGB(); + int newRGB = rgbImageFilter.filterRGB( 0, 0, oldRGB ); + return (newRGB != oldRGB) ? new Color( newRGB, true ) : color; + }; + } } //---- class GraphicsFilter ----------------------------------------------- @@ -456,11 +752,15 @@ public class FlatSVGIcon extends Graphics2DProxy { private final ColorFilter colorFilter; + private final ColorFilter globalColorFilter; private final RGBImageFilter grayFilter; - public GraphicsFilter( Graphics2D delegate, ColorFilter colorFilter, RGBImageFilter grayFilter ) { + GraphicsFilter( Graphics2D delegate, ColorFilter colorFilter, + ColorFilter globalColorFilter, RGBImageFilter grayFilter ) + { super( delegate ); this.colorFilter = colorFilter; + this.globalColorFilter = globalColorFilter; this.grayFilter = grayFilter; } @@ -477,8 +777,14 @@ public class FlatSVGIcon } private Color filterColor( Color color ) { - if( colorFilter != null ) - color = colorFilter.filter( color ); + if( colorFilter != null ) { + Color newColor = colorFilter.filter( color ); + color = (newColor != color) + ? newColor + : globalColorFilter.filter( color ); + } else + color = globalColorFilter.filter( color ); + if( grayFilter != null ) { int oldRGB = color.getRGB(); int newRGB = grayFilter.filterRGB( 0, 0, oldRGB );