diff --git a/flatlaf-core/src/main/java/com/formdev/flatlaf/icons/FlatAbstractIcon.java b/flatlaf-core/src/main/java/com/formdev/flatlaf/icons/FlatAbstractIcon.java index 8448f1288..db19bcdd3 100644 --- a/flatlaf-core/src/main/java/com/formdev/flatlaf/icons/FlatAbstractIcon.java +++ b/flatlaf-core/src/main/java/com/formdev/flatlaf/icons/FlatAbstractIcon.java @@ -69,7 +69,13 @@ public void paintIcon( Component c, Graphics g, int x, int y ) { } } - protected abstract void paintIcon( Component c, Graphics2D g2 ); + /** + * Paint the icon at {@code [0,0]} location. + *

+ * The given graphics context is scaled. + * Use unscaled coordinates, width and height for painting. + */ + protected abstract void paintIcon( Component c, Graphics2D g ); @Override public int getIconWidth() { diff --git a/flatlaf-core/src/main/java/com/formdev/flatlaf/icons/FlatAnimatedIcon.java b/flatlaf-core/src/main/java/com/formdev/flatlaf/icons/FlatAnimatedIcon.java index 4d80f49b0..feff6f66e 100644 --- a/flatlaf-core/src/main/java/com/formdev/flatlaf/icons/FlatAnimatedIcon.java +++ b/flatlaf-core/src/main/java/com/formdev/flatlaf/icons/FlatAnimatedIcon.java @@ -21,6 +21,7 @@ import java.awt.Graphics; import java.awt.Graphics2D; import com.formdev.flatlaf.util.AnimatedIcon; +import com.formdev.flatlaf.util.AnimatedPainter; /** * Base class for animated icons that scales width and height, creates and initializes @@ -30,7 +31,7 @@ *

* This class does not store any state information (needed for animation) in its instance. * Instead a client property is set on the painted component. - * This makes it possible to use a share icon instance for multiple components. + * This makes it possible to use a shared icon instance for multiple components. * * @author Karl Tauber */ @@ -45,11 +46,34 @@ public FlatAnimatedIcon( int width, int height, Color color ) { @Override public void paintIcon( Component c, Graphics g, int x, int y ) { super.paintIcon( c, g, x, y ); - AnimatedIcon.AnimationSupport.saveIconLocation( this, c, x, y ); + AnimatedPainter.saveRepaintLocation( this, c, x, y ); } @Override protected void paintIcon( Component c, Graphics2D g ) { - AnimatedIcon.AnimationSupport.paintIcon( this, c, g, 0, 0 ); + paintWithAnimation( c, g, 0, 0, getIconWidth(), getIconHeight() ); } + + /** + * Delegates painting to {@link #paintIconAnimated(Component, Graphics2D, float[])}. + * Ignores the given bounds because {@code [x,y]} are always {@code [0,0]} and + * {@code [width,height]} are scaled, but painting code should use unscaled width + * and height because given graphics context is scaled. + * + * @since 2 + */ + @Override + public void paintAnimated( Component c, Graphics2D g, int x, int y, int width, int height, float[] animatedValues ) { + paintIconAnimated( c, g, animatedValues ); + } + + /** + * Paint the icon at {@code 0,0} location. + *

+ * The given graphics context is scaled. + * Use unscaled coordinates, width and height for painting. + * + * @since 2 + */ + protected abstract void paintIconAnimated( Component c, Graphics2D g, float[] animatedValues ); } diff --git a/flatlaf-core/src/main/java/com/formdev/flatlaf/util/AnimatedBorder.java b/flatlaf-core/src/main/java/com/formdev/flatlaf/util/AnimatedBorder.java new file mode 100644 index 000000000..aa0ec72bb --- /dev/null +++ b/flatlaf-core/src/main/java/com/formdev/flatlaf/util/AnimatedBorder.java @@ -0,0 +1,83 @@ +/* + * Copyright 2021 FormDev Software GmbH + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.formdev.flatlaf.util; + +import java.awt.Component; +import java.awt.Graphics; +import java.awt.Graphics2D; +import javax.swing.JComponent; +import javax.swing.border.Border; + +/** + * Border that automatically animates painting on component value changes. + *

+ * {@link #getAnimatableValues(Component)} returns the animatable value(s) of the component. + * If the value(s) have changed, then {@link #paintAnimated(Component, Graphics2D, int, int, int, int, float[])} + * is invoked multiple times with animated value(s) (from old value(s) to new value(s)). + * If {@link #getAnimatableValues(Component)} returns multiple values, then each value + * gets its own independent animation, which may start/end at different points in time, + * may have different duration, resolution and interpolator. + *

+ * Example for an animated border: + *

+ * private class MyAnimatedBorder
+ *     implements AnimatedBorder
+ * {
+ *     @Override
+ *     public float[] getAnimatableValues( Component c ) {
+ *         return new float[] { c.isFocusOwner() ? 1 : 0 };
+ *     }
+ *
+ *     @Override
+ *     public void paintAnimated( Component c, Graphics2D g, int x, int y, int width, int height, float[] animatedValues ) {
+ *         int lh = UIScale.scale( 2 );
+ *
+ *         g.setColor( Color.blue );
+ *         g.fillRect( x, y + height - lh, Math.round( width * animatedValues[0] ), lh );
+ *     }
+ *
+ *     @Override
+ *     public Insets getBorderInsets( Component c ) {
+ *         return UIScale.scale( new Insets( 4, 4, 4, 4 ) );
+ *     }
+ *
+ *     @Override public boolean isBorderOpaque() { return false; }
+ * }
+ *
+ * // sample usage
+ * JTextField textField = new JTextField();
+ * textField.setBorder( new MyAnimatedBorder() );
+ * 
+ * + * Animation works only if the component passed to {@link #paintBorder(Component, Graphics, int, int, int, int)} + * is a instance of {@link JComponent}. + * A client property is set on the component to store the animation state. + * + * @author Karl Tauber + * @since 2 + */ +public interface AnimatedBorder + extends Border, AnimatedPainter +{ + /** + * Invokes {@link #paintWithAnimation(Component, Graphics, int, int, int, int)}. + */ + @Override + default void paintBorder( Component c, Graphics g, int x, int y, int width, int height ) { + paintWithAnimation( c, g, x, y, width, height ); + } +} diff --git a/flatlaf-core/src/main/java/com/formdev/flatlaf/util/AnimatedIcon.java b/flatlaf-core/src/main/java/com/formdev/flatlaf/util/AnimatedIcon.java index da0b0a1ae..2381bb913 100644 --- a/flatlaf-core/src/main/java/com/formdev/flatlaf/util/AnimatedIcon.java +++ b/flatlaf-core/src/main/java/com/formdev/flatlaf/util/AnimatedIcon.java @@ -18,62 +18,87 @@ import java.awt.Component; import java.awt.Graphics; +import java.awt.Graphics2D; import javax.swing.Icon; import javax.swing.JComponent; -import com.formdev.flatlaf.util.Animator.Interpolator; /** * Icon that automatically animates painting on component value changes. *

- * {@link #getValue(Component)} returns the value of the component. - * If the value changes, then {@link #paintIconAnimated(Component, Graphics, int, int, float)} - * is invoked multiple times with animated value (from old value to new value). + * {@link #getAnimatableValues(Component)} returns the animatable value(s) of the component. + * If the value(s) have changed, then {@link #paintAnimated(Component, Graphics2D, int, int, int, int, float[])} + * is invoked multiple times with animated value(s) (from old value(s) to new value(s)). + * If {@link #getAnimatableValues(Component)} returns multiple values, then each value + * gets its own independent animation, which may start/end at different points in time, + * may have different duration, resolution and interpolator. *

* Example for an animated icon: *

- * private class AnimatedMinimalTestIcon
+ * private class MyAnimatedIcon
  *     implements AnimatedIcon
  * {
+ *     @Override
+ *     public float[] getAnimatableValues( Component c ) {
+ *         return new float[] { ((AbstractButton)c).isSelected() ? 1 : 0 };
+ *     }
+ *
  *     @Override public int getIconWidth() { return 100; }
  *     @Override public int getIconHeight() { return 20; }
  *
  *     @Override
- *     public void paintIconAnimated( Component c, Graphics g, int x, int y, float animatedValue ) {
- *         int w = getIconWidth();
- *         int h = getIconHeight();
- *
+ *     public void paintAnimated( Component c, Graphics2D g, int x, int y, int width, int height, float[] animatedValues ) {
  *         g.setColor( Color.red );
- *         g.drawRect( x, y, w - 1, h - 1 );
- *         g.fillRect( x, y, Math.round( w * animatedValue ), h );
- *     }
- *
- *     @Override
- *     public float getValue( Component c ) {
- *         return ((AbstractButton)c).isSelected() ? 1 : 0;
+ *         g.drawRect( x, y, width - 1, height - 1 );
+ *         g.fillRect( x, y, Math.round( width * animatedValues[0] ), height );
  *     }
  * }
  *
  * // sample usage
  * JCheckBox checkBox = new JCheckBox( "test" );
- * checkBox.setIcon( new AnimatedMinimalTestIcon() );
+ * checkBox.setIcon( new MyAnimatedIcon() );
  * 
* * Animation works only if the component passed to {@link #paintIcon(Component, Graphics, int, int)} - * is a instance of {@link JComponent}. + * is an instance of {@link JComponent}. * A client property is set on the component to store the animation state. * * @author Karl Tauber */ public interface AnimatedIcon - extends Icon + extends Icon, AnimatedPainter { + /** + * {@inheritDoc} + * + * @since 2 + */ + @Override + default float[] getAnimatableValues( Component c ) { + // for compatibility + return new float[] { getValue( c ) }; + } + + /** + * Invokes {@link #paintWithAnimation(Component, Graphics, int, int, int, int)}. + */ + @Override + default void paintIcon( Component c, Graphics g, int x, int y ) { + paintWithAnimation( c, g, x, y, getIconWidth(), getIconHeight() ); + } + + /** + * {@inheritDoc} + * + * @since 2 + */ @Override - public default void paintIcon( Component c, Graphics g, int x, int y ) { - AnimationSupport.paintIcon( this, c, g, x, y ); + default void paintAnimated( Component c, Graphics2D g, int x, int y, int width, int height, float[] animatedValues ) { + // for compatibility + paintIconAnimated( c, g, x, y, animatedValues[0] ); } /** - * Paints the icon for the given animated value. + * Paints the icon for the given (animated) value. * * @param c the component that this icon belongs to * @param g the graphics context @@ -82,52 +107,45 @@ public default void paintIcon( Component c, Graphics g, int x, int y ) { * @param animatedValue the animated value, which is either equal to what {@link #getValue(Component)} * returned, or somewhere between the previous value and the latest value * that {@link #getValue(Component)} returned + * + * @deprecated override {@link #paintAnimated(Component, Graphics2D, int, int, int, int, float[])} instead */ - void paintIconAnimated( Component c, Graphics g, int x, int y, float animatedValue ); + @Deprecated + default void paintIconAnimated( Component c, Graphics g, int x, int y, float animatedValue ) { + } /** - * Gets the value of the component. + * Gets the animatable value of the component. *

* This can be any value and depends on the component. * If the value changes, then this class animates from the old value to the new one. *

* For a toggle button this could be {@code 0} for off and {@code 1} for on. + * + * @deprecated override {@link #getAnimatableValues(Component)} instead */ - float getValue( Component c ); - - /** - * Returns whether animation is enabled for this icon (default is {@code true}). - */ - default boolean isAnimationEnabled() { - return true; - } - - /** - * Returns the duration of the animation in milliseconds (default is 150). - */ - default int getAnimationDuration() { - return 150; - } - - /** - * Returns the resolution of the animation in milliseconds (default is 10). - * Resolution is the amount of time between timing events. - */ - default int getAnimationResolution() { - return 10; + @Deprecated + default float getValue( Component c ) { + return 0; } /** - * Returns the interpolator for the animation. - * Default is {@link CubicBezierEasing#STANDARD_EASING}. + * {@inheritDoc} + * + * @since TODO */ - default Interpolator getAnimationInterpolator() { - return CubicBezierEasing.STANDARD_EASING; + @Override + default Object getAnimationClientPropertyKey() { + // for compatibility + return getClientPropertyKey(); } /** * Returns the client property key used to store the animation support. + * + * @deprecated override {@link #getAnimationClientPropertyKey()} instead */ + @Deprecated default Object getClientPropertyKey() { return getClass(); } @@ -135,115 +153,25 @@ default Object getClientPropertyKey() { //---- class AnimationSupport --------------------------------------------- /** - * Animation support class that stores the animation state and implements the animation. + * Animation support. */ + @Deprecated class AnimationSupport { - private float startValue; - private float targetValue; - private float animatedValue; - private float fraction; - - private Animator animator; - - // last x,y coordinates of the icon needed to repaint while animating - private int x; - private int y; - + /** + * @deprecated use {@link AnimatedPainter#paintWithAnimation(Component, Graphics, int, int, int, int)} instead + */ + @Deprecated public static void paintIcon( AnimatedIcon icon, Component c, Graphics g, int x, int y ) { - if( !isAnimationEnabled( icon, c ) ) { - // paint without animation if animation is disabled or - // component is not a JComponent and therefore does not support - // client properties, which are required to keep animation state - paintIconImpl( icon, c, g, x, y, null ); - return; - } - - JComponent jc = (JComponent) c; - Object key = icon.getClientPropertyKey(); - AnimationSupport as = (AnimationSupport) jc.getClientProperty( key ); - if( as == null ) { - // painted first time --> do not animate, but remember current component value - as = new AnimationSupport(); - as.startValue = as.targetValue = as.animatedValue = icon.getValue( c ); - as.x = x; - as.y = y; - jc.putClientProperty( key, as ); - } else { - // get component value - float value = icon.getValue( c ); - - if( value != as.targetValue ) { - // value changed --> (re)start animation - - if( as.animator == null ) { - // create animator - AnimationSupport as2 = as; - as.animator = new Animator( icon.getAnimationDuration(), fraction -> { - // check whether component was removed while animation is running - if( !c.isDisplayable() ) { - as2.animator.stop(); - return; - } - - // compute animated value - as2.animatedValue = as2.startValue + ((as2.targetValue - as2.startValue) * fraction); - as2.fraction = fraction; - - // repaint icon - c.repaint( as2.x, as2.y, icon.getIconWidth(), icon.getIconHeight() ); - }, () -> { - as2.startValue = as2.animatedValue = as2.targetValue; - as2.animator = null; - } ); - } - - if( as.animator.isRunning() ) { - // if animation is still running, restart it from the current - // animated value to the new target value with reduced duration - as.animator.cancel(); - int duration2 = (int) (icon.getAnimationDuration() * as.fraction); - if( duration2 > 0 ) - as.animator.setDuration( duration2 ); - as.startValue = as.animatedValue; - } else { - // new animation - as.animator.setDuration( icon.getAnimationDuration() ); - as.animator.setResolution( icon.getAnimationResolution() ); - as.animator.setInterpolator( icon.getAnimationInterpolator() ); - - as.animatedValue = as.startValue; - } - - as.targetValue = value; - as.animator.start(); - } - - as.x = x; - as.y = y; - } - - paintIconImpl( icon, c, g, x, y, as ); - } - - private static void paintIconImpl( AnimatedIcon icon, Component c, Graphics g, int x, int y, AnimationSupport as ) { - float value = (as != null) ? as.animatedValue : icon.getValue( c ); - icon.paintIconAnimated( c, g, x, y, value ); - } - - private static boolean isAnimationEnabled( AnimatedIcon icon, Component c ) { - return Animator.useAnimation() && icon.isAnimationEnabled() && c instanceof JComponent; + AnimatedPainterSupport.paint( icon, c, (Graphics2D) g, x, y, icon.getIconWidth(), icon.getIconHeight() ); } + /** + * @deprecated use {@link AnimatedPainter#saveRepaintLocation(AnimatedPainter, Component, int, int)} instead + */ + @Deprecated public static void saveIconLocation( AnimatedIcon icon, Component c, int x, int y ) { - if( !isAnimationEnabled( icon, c ) ) - return; - - AnimationSupport as = (AnimationSupport) ((JComponent)c).getClientProperty( icon.getClientPropertyKey() ); - if( as != null ) { - as.x = x; - as.y = y; - } + AnimatedPainterSupport.saveRepaintLocation( icon, c, x, y ); } } } diff --git a/flatlaf-core/src/main/java/com/formdev/flatlaf/util/AnimatedPainter.java b/flatlaf-core/src/main/java/com/formdev/flatlaf/util/AnimatedPainter.java new file mode 100644 index 000000000..58270a064 --- /dev/null +++ b/flatlaf-core/src/main/java/com/formdev/flatlaf/util/AnimatedPainter.java @@ -0,0 +1,181 @@ +/* + * Copyright 2021 FormDev Software GmbH + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.formdev.flatlaf.util; + +import java.awt.Component; +import java.awt.Graphics; +import java.awt.Graphics2D; +import javax.swing.JComponent; +import com.formdev.flatlaf.util.Animator.Interpolator; + +/** + * Painter that automatically animates painting on component value(s) changes. + *

+ * {@link #getAnimatableValues(Component)} returns the animatable value(s) of the component. + * If the value(s) have changed, then {@link #paintAnimated(Component, Graphics2D, int, int, int, int, float[])} + * is invoked multiple times with animated value(s) (from old value(s) to new value(s)). + * If {@link #getAnimatableValues(Component)} returns multiple values, then each value + * gets its own independent animation, which may start/end at different points in time, + * may have different duration, resolution and interpolator. + *

+ * See {@link AnimatedBorder} or {@link AnimatedIcon} for examples. + *

+ * Animation works only if the component passed to {@link #paintWithAnimation(Component, Graphics, int, int, int, int)} + * is an instance of {@link JComponent}. + * A client property is set on the component to store the animation state. + * + * @author Karl Tauber + * @since 2 + */ +public interface AnimatedPainter +{ + /** + * Gets the animatable value(s) of the component. + *

+ * This can be any value(s) and depends on the component. + * If the value(s) changes, then this class animates from the old value(s) to the new ones. + * If multiple values are returned, then each value gets its own independent animation. + *

+ * For a toggle button this could be {@code 0} for off and {@code 1} for on. + * A complex check box could return values for selected, hover, pressed and focused states. + * The painter then can show independent animations for those states. + */ + float[] getAnimatableValues( Component c ); + + /** + * Starts painting. + * Either invokes {@link #paintAnimated(Component, Graphics2D, int, int, int, int, float[])} + * once to paint current value(s) (see {@link #getAnimatableValues(Component)}. Or if value(s) has + * changed, compared to last painting, then it starts an animation and invokes + * {@link #paintAnimated(Component, Graphics2D, int, int, int, int, float[])} + * multiple times with animated value(s) (from old value(s) to new value(s)). + * + * @param c the component that this painter belongs to + * @param g the graphics context + * @param x the x coordinate of the paint area + * @param y the y coordinate of the paint area + * @param width the width of the paint area + * @param height the height of the paint area + */ + default void paintWithAnimation( Component c, Graphics g, int x, int y, int width, int height ) { + AnimatedPainterSupport.paint( this, c, (Graphics2D) g, x, y, width, height ); + } + + /** + * Paints the given (animated) value(s). + *

+ * Invoked from {@link #paintWithAnimation(Component, Graphics, int, int, int, int)}. + * + * @param c the component that this painter belongs to + * @param g the graphics context + * @param x the x coordinate of the paint area + * @param y the y coordinate of the paint area + * @param width the width of the paint area + * @param height the height of the paint area + * @param animatedValues the animated values, which are either equal to what {@link #getAnimatableValues(Component)} + * returned, or somewhere between the previous values and the latest values + * that {@link #getAnimatableValues(Component)} returned + */ + void paintAnimated( Component c, Graphics2D g, int x, int y, int width, int height, float[] animatedValues ); + + /** + * Invoked from animator to repaint an area. + *

+ * Useful to limit the repaint region. E.g. if only the bottom border is animated. + * If more than one border side is animated (e.g. bottom and right side), then it + * makes no sense to do separate repaints because the Swing repaint manager unions + * the regions and the whole component is repainted. + *

+ * The default implementation repaints the whole given area. + */ + default void repaintDuringAnimation( Component c, int x, int y, int width, int height ) { + c.repaint( x, y, width, height ); + } + + /** + * Returns whether animation is enabled for this painter (default is {@code true}). + */ + default boolean isAnimationEnabled() { + return true; + } + + /** + * Returns the duration of the animation in milliseconds (default is 150). + */ + default int getAnimationDuration() { + return 150; + } + + /** + * Returns the resolution of the animation in milliseconds (default is 10). + * Resolution is the amount of time between timing events. + */ + default int getAnimationResolution() { + return 10; + } + + /** + * Returns the interpolator for the animation. + * Default is {@link CubicBezierEasing#STANDARD_EASING}. + */ + default Interpolator getAnimationInterpolator() { + return CubicBezierEasing.STANDARD_EASING; + } + + /** + * Returns the duration of the animation in milliseconds (default is 150) + * for the given value index and value. + */ + default int getAnimationDuration( int valueIndex, float value ) { + return getAnimationDuration(); + } + + /** + * Returns the resolution of the animation in milliseconds (default is 10) + * for the given value index and value. + * Resolution is the amount of time between timing events. + */ + default int getAnimationResolution( int valueIndex, float value ) { + return getAnimationResolution(); + } + + /** + * Returns the interpolator for the animation + * for the given value index and value. + * Default is {@link CubicBezierEasing#STANDARD_EASING}. + */ + default Interpolator getAnimationInterpolator( int valueIndex, float value ) { + return getAnimationInterpolator(); + } + + /** + * Returns the client property key used to store the animation support. + */ + default Object getAnimationClientPropertyKey() { + return getClass(); + } + + /** + * Saves location for repainting animated area with + * {@link AnimatedPainter#repaintDuringAnimation(Component, int, int, int, int)}. + * Only needed when graphics context passed to {@link #paintWithAnimation(Component, Graphics, int, int, int, int)} + * uses transformed location. + */ + static void saveRepaintLocation( AnimatedPainter painter, Component c, int x, int y ) { + AnimatedPainterSupport.saveRepaintLocation( painter, c, x, y ); + } +} diff --git a/flatlaf-core/src/main/java/com/formdev/flatlaf/util/AnimatedPainterSupport.java b/flatlaf-core/src/main/java/com/formdev/flatlaf/util/AnimatedPainterSupport.java new file mode 100644 index 000000000..4a41fdbe6 --- /dev/null +++ b/flatlaf-core/src/main/java/com/formdev/flatlaf/util/AnimatedPainterSupport.java @@ -0,0 +1,185 @@ +/* + * Copyright 2021 FormDev Software GmbH + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.formdev.flatlaf.util; + +import java.awt.Component; +import java.awt.Graphics2D; +import javax.swing.JComponent; + +/** + * Animation support class that stores the animation state and implements the animation. + * + * @author Karl Tauber + * @since 2 + */ +class AnimatedPainterSupport +{ + private final int valueIndex; + + private float startValue; + private float targetValue; + private float animatedValue; + private float fraction; + + private Animator animator; + + // last bounds of the paint area needed to repaint while animating + private int x; + private int y; + private int width; + private int height; + + private AnimatedPainterSupport( int valueIndex ) { + this.valueIndex = valueIndex; + } + + static void paint( AnimatedPainter painter, Component c, Graphics2D g, + int x, int y, int width, int height ) + { + // get animatable component values + float[] values = painter.getAnimatableValues( c ); + + if( !isAnimationEnabled( painter, c ) ) { + // paint without animation if animation is disabled or + // component is not a JComponent and therefore does not support + // client properties, which are required to keep animation state + painter.paintAnimated( c, g, x, y, width, height, values ); + return; + } + + JComponent jc = (JComponent) c; + Object key = painter.getAnimationClientPropertyKey(); + AnimatedPainterSupport[] ass = (AnimatedPainterSupport[]) jc.getClientProperty( key ); + + // check whether length of values array has changed + if( ass != null && ass.length != values.length ) { + // cancel all running animations + for( int i = 0; i < ass.length; i++ ) { + AnimatedPainterSupport as = ass[i]; + if( as.animator != null ) + as.animator.cancel(); + } + ass = null; + } + + if( ass == null ) { + ass = new AnimatedPainterSupport[values.length]; + jc.putClientProperty( key, ass ); + } + + for( int i = 0; i < ass.length; i++ ) { + AnimatedPainterSupport as = ass[i]; + float value = values[i]; + + if( as == null ) { + // painted first time --> do not animate, but remember current component value + as = new AnimatedPainterSupport( i ); + as.startValue = as.targetValue = as.animatedValue = value; + ass[i] = as; + } else if( value != as.targetValue ) { + // value changed --> (re)start animation + + int animationDuration = painter.getAnimationDuration( as.valueIndex, value ); + + // do not animate if animation duration (for current value) is zero + if( animationDuration <= 0 ) { + if( as.animator != null ) { + as.animator.cancel(); + as.animator = null; + } + as.startValue = as.targetValue = as.animatedValue = value; + as.fraction = 0; + continue; + } + + if( as.animator == null ) { + // create animator + AnimatedPainterSupport as2 = as; + as.animator = new Animator( 1, fraction -> { + // check whether component was removed while animation is running + if( !c.isDisplayable() ) { + as2.animator.stop(); + return; + } + + // compute animated value + as2.animatedValue = as2.startValue + ((as2.targetValue - as2.startValue) * fraction); + as2.fraction = fraction; + + // repaint + painter.repaintDuringAnimation( c, as2.x, as2.y, as2.width, as2.height ); + }, () -> { + as2.startValue = as2.animatedValue = as2.targetValue; + as2.animator = null; + } ); + } + + if( as.animator.isRunning() ) { + // if animation is still running, restart it from the current + // animated value to the new target value with reduced duration + as.animator.cancel(); + int duration2 = (int) (animationDuration * as.fraction); + if( duration2 > 0 ) + as.animator.setDuration( duration2 ); + as.startValue = as.animatedValue; + } else { + // new animation + as.animator.setDuration( animationDuration ); + + as.animatedValue = as.startValue; + } + + // update animator for new value + as.animator.setResolution( painter.getAnimationResolution( as.valueIndex, value ) ); + as.animator.setInterpolator( painter.getAnimationInterpolator( as.valueIndex, value ) ); + + // start animation + as.targetValue = value; + as.animator.start(); + } + + as.x = x; + as.y = y; + as.width = width; + as.height = height; + } + + float[] animatedValues = new float[ass.length]; + for( int i = 0; i < ass.length; i++ ) + animatedValues[i] = ass[i].animatedValue; + + painter.paintAnimated( c, g, x, y, width, height, animatedValues ); + } + + private static boolean isAnimationEnabled( AnimatedPainter painter, Component c ) { + return Animator.useAnimation() && painter.isAnimationEnabled() && c instanceof JComponent; + } + + static void saveRepaintLocation( AnimatedPainter painter, Component c, int x, int y ) { + if( !isAnimationEnabled( painter, c ) ) + return; + + AnimatedPainterSupport[] ass = (AnimatedPainterSupport[]) ((JComponent)c).getClientProperty( painter.getAnimationClientPropertyKey() ); + if( ass != null ) { + for( int i = 0; i < ass.length; i++ ) { + AnimatedPainterSupport as = ass[i]; + as.x = x; + as.y = y; + } + } + } +} diff --git a/flatlaf-testing/src/main/java/com/formdev/flatlaf/testing/FlatAnimatedBorderTest.java b/flatlaf-testing/src/main/java/com/formdev/flatlaf/testing/FlatAnimatedBorderTest.java new file mode 100644 index 000000000..fa84f7c3c --- /dev/null +++ b/flatlaf-testing/src/main/java/com/formdev/flatlaf/testing/FlatAnimatedBorderTest.java @@ -0,0 +1,438 @@ +/* + * Copyright 2021 FormDev Software GmbH + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.formdev.flatlaf.testing; + +import java.awt.Color; +import java.awt.Component; +import java.awt.FontMetrics; +import java.awt.Graphics; +import java.awt.Graphics2D; +import java.awt.Insets; +import java.awt.geom.Rectangle2D; +import javax.swing.*; +import javax.swing.border.AbstractBorder; +import com.formdev.flatlaf.FlatClientProperties; +import com.formdev.flatlaf.ui.FlatUIUtils; +import com.formdev.flatlaf.util.AnimatedBorder; +import com.formdev.flatlaf.util.ColorFunctions; +import com.formdev.flatlaf.util.HiDPIUtils; +import com.formdev.flatlaf.util.UIScale; +import net.miginfocom.swing.*; + +/** + * @author Karl Tauber + */ +public class FlatAnimatedBorderTest + extends FlatTestPanel +{ + private static final Color CHART_FADE_1 = Color.blue; + private static final Color CHART_FADE_2 = Color.red; + private static final Color CHART_MATERIAL_1 = Color.green; + private static final Color CHART_MATERIAL_2 = Color.magenta; + private static final Color CHART_MATERIAL_3 = Color.pink; + private static final Color CHART_MATERIAL_4 = Color.cyan; + private static final Color CHART_MINIMAL = Color.orange; + + private static final String CHART_COLOR_KEY = "chartColor"; + + public static void main( String[] args ) { + SwingUtilities.invokeLater( () -> { + FlatTestFrame frame = FlatTestFrame.create( args, "FlatAnimatedBorderTest" ); + frame.showFrame( FlatAnimatedBorderTest::new ); + } ); + } + + FlatAnimatedBorderTest() { + initComponents(); + + fade1TextField.setBorder( new AnimatedFocusFadeBorder() ); + fade2TextField.setBorder( new AnimatedFocusFadeBorder() ); + + material1TextField.setBorder( new AnimatedMaterialBorder() ); + material2TextField.setBorder( new AnimatedMaterialBorder() ); + material3TextField.setBorder( new AnimatedMaterialLabeledBorder() ); + material4TextField.setBorder( new AnimatedMaterialLabeledBorder() ); + + minimalTextField.setBorder( new AnimatedMinimalTestBorder() ); + + fade1TextField.putClientProperty( CHART_COLOR_KEY, CHART_FADE_1 ); + fade2TextField.putClientProperty( CHART_COLOR_KEY, CHART_FADE_2 ); + material1TextField.putClientProperty( CHART_COLOR_KEY, CHART_MATERIAL_1 ); + material2TextField.putClientProperty( CHART_COLOR_KEY, CHART_MATERIAL_2 ); + material3TextField.putClientProperty( CHART_COLOR_KEY, CHART_MATERIAL_3 ); + material4TextField.putClientProperty( CHART_COLOR_KEY, CHART_MATERIAL_4 ); + minimalTextField.putClientProperty( CHART_COLOR_KEY, CHART_MINIMAL ); + + fade1ChartColor.setForeground( CHART_FADE_1 ); + fade2ChartColor.setForeground( CHART_FADE_2 ); + material1ChartColor.setForeground( CHART_MATERIAL_1 ); + material2ChartColor.setForeground( CHART_MATERIAL_2 ); + material3ChartColor.setForeground( CHART_MATERIAL_3 ); + material4ChartColor.setForeground( CHART_MATERIAL_4 ); + minimalChartColor.setForeground( CHART_MINIMAL ); + + material3TextField.putClientProperty( AnimatedMaterialLabeledBorder.LABEL_TEXT_KEY, "Label" ); + material4TextField.putClientProperty( AnimatedMaterialLabeledBorder.LABEL_TEXT_KEY, "Label" ); + material4TextField.setText( "Text" ); + } + + private void initComponents() { + // JFormDesigner - Component initialization - DO NOT MODIFY //GEN-BEGIN:initComponents + label3 = new JLabel(); + lineChartPanel = new LineChartPanel(); + fade1TextField = new JTextField(); + fade1ChartColor = new FlatAnimatorTest.JChartColor(); + fade2TextField = new JTextField(); + fade2ChartColor = new FlatAnimatorTest.JChartColor(); + label2 = new JLabel(); + material1TextField = new JTextField(); + material1ChartColor = new FlatAnimatorTest.JChartColor(); + material2TextField = new JTextField(); + material2ChartColor = new FlatAnimatorTest.JChartColor(); + material3TextField = new JTextField(); + material3ChartColor = new FlatAnimatorTest.JChartColor(); + material4TextField = new JTextField(); + material4ChartColor = new FlatAnimatorTest.JChartColor(); + label1 = new JLabel(); + minimalTextField = new JTextField(); + minimalChartColor = new FlatAnimatorTest.JChartColor(); + durationLabel = new JLabel(); + durationField = new JSpinner(); + + //======== this ======== + setLayout(new MigLayout( + "insets dialog,hidemode 3", + // columns + "[fill]" + + "[fill]para" + + "[grow,fill]", + // rows + "[]" + + "[]" + + "[]para" + + "[]" + + "[]" + + "[]" + + "[]" + + "[]para" + + "[]" + + "[]" + + "[grow]" + + "[]")); + + //---- label3 ---- + label3.setText("Fade:"); + add(label3, "cell 0 0"); + add(lineChartPanel, "cell 2 0 1 12,growy"); + add(fade1TextField, "cell 0 1"); + add(fade1ChartColor, "cell 1 1"); + add(fade2TextField, "cell 0 2"); + add(fade2ChartColor, "cell 1 2"); + + //---- label2 ---- + label2.setText("Material:"); + add(label2, "cell 0 3"); + add(material1TextField, "cell 0 4"); + add(material1ChartColor, "cell 1 4"); + add(material2TextField, "cell 0 5"); + add(material2ChartColor, "cell 1 5"); + + //---- material3TextField ---- + material3TextField.putClientProperty(FlatClientProperties.STYLE_CLASS, "large"); + add(material3TextField, "cell 0 6"); + add(material3ChartColor, "cell 1 6"); + + //---- material4TextField ---- + material4TextField.putClientProperty(FlatClientProperties.STYLE_CLASS, "large"); + add(material4TextField, "cell 0 7"); + add(material4ChartColor, "cell 1 7"); + + //---- label1 ---- + label1.setText("Minimal:"); + add(label1, "cell 0 8"); + add(minimalTextField, "cell 0 9"); + add(minimalChartColor, "cell 1 9"); + + //---- durationLabel ---- + durationLabel.setText("Duration:"); + add(durationLabel, "cell 0 11"); + + //---- durationField ---- + durationField.setModel(new SpinnerNumberModel(200, 0, null, 50)); + add(durationField, "cell 0 11"); + // JFormDesigner - End of component initialization //GEN-END:initComponents + } + + // JFormDesigner - Variables declaration - DO NOT MODIFY //GEN-BEGIN:variables + private JLabel label3; + private LineChartPanel lineChartPanel; + private JTextField fade1TextField; + private FlatAnimatorTest.JChartColor fade1ChartColor; + private JTextField fade2TextField; + private FlatAnimatorTest.JChartColor fade2ChartColor; + private JLabel label2; + private JTextField material1TextField; + private FlatAnimatorTest.JChartColor material1ChartColor; + private JTextField material2TextField; + private FlatAnimatorTest.JChartColor material2ChartColor; + private JTextField material3TextField; + private FlatAnimatorTest.JChartColor material3ChartColor; + private JTextField material4TextField; + private FlatAnimatorTest.JChartColor material4ChartColor; + private JLabel label1; + private JTextField minimalTextField; + private FlatAnimatorTest.JChartColor minimalChartColor; + private JLabel durationLabel; + private JSpinner durationField; + // JFormDesigner - End of variables declaration //GEN-END:variables + + //---- class AnimatedMaterialBorder --------------------------------------- + + /** + * Experimental text field border that: + * - animates focus indicator color and border width + */ + private class AnimatedFocusFadeBorder + extends AbstractBorder + implements AnimatedBorder + { + // needed because otherwise the empty paint method in superclass + // javax.swing.border.AbstractBorder would be used + @Override + public void paintBorder( Component c, Graphics g, int x, int y, int width, int height ) { + paintWithAnimation( c, g, x, y, width, height ); + } + + @Override + public void paintAnimated( Component c, Graphics2D g, int x, int y, int width, int height, float[] animatedValues ) { + float animatedValue = animatedValues[0]; + FlatUIUtils.setRenderingHints( g ); + + // border width is 1 if not focused and 2 if focused + float lw = UIScale.scale( 1 + animatedValue ); + + // paint border + Color color = ColorFunctions.mix( Color.red, Color.lightGray, animatedValue ); + FlatUIUtils.paintOutlinedComponent( g, x, y, width, height, 0, 0, 0, lw, 0, + null, color, null ); + + if( animatedValue != 0 && animatedValue != 1 ) { + Color chartColor = (Color) ((JComponent)c).getClientProperty( CHART_COLOR_KEY ); + lineChartPanel.addValue( chartColor, animatedValue, Integer.MIN_VALUE, "fade" ); + } + } + + @Override + public Insets getBorderInsets( Component c, Insets insets ) { + insets.top = insets.bottom = UIScale.scale( 3 ); + insets.left = insets.right = UIScale.scale( 7 ); + return insets; + } + + @Override + public float[] getAnimatableValues( Component c ) { + return new float[] { FlatUIUtils.isPermanentFocusOwner( c ) ? 1 : 0 }; + } + + @Override + public int getAnimationDuration() { + return (Integer) durationField.getValue(); + } + } + + //---- class AnimatedMaterialBorder --------------------------------------- + + /** + * Experimental text field border that: + * - paint border only at bottom + * - animates focus indicator at bottom + */ + private class AnimatedMaterialBorder + extends AbstractBorder + implements AnimatedBorder + { + // needed because otherwise the empty paint method in superclass + // javax.swing.border.AbstractBorder would be used + @Override + public void paintBorder( Component c, Graphics g, int x, int y, int width, int height ) { + paintWithAnimation( c, g, x, y, width, height ); + } + + @Override + public void paintAnimated( Component c, Graphics2D g, int x, int y, int width, int height, float[] animatedValues ) { + float animatedValue = animatedValues[0]; + FlatUIUtils.setRenderingHints( g ); + + // use paintAtScale1x() for consistent line thickness when scaled + HiDPIUtils.paintAtScale1x( g, x, y, width, height, + (g2d, x2, y2, width2, height2, scaleFactor) -> { + float lh = (float) (UIScale.scale( 1f ) * scaleFactor); + + g2d.setColor( Color.gray ); + g2d.fill( new Rectangle2D.Float( x2, y2 + height2 - lh, width2, lh ) ); + + if( animatedValue > 0 ) { + lh = (float) (UIScale.scale( 2f ) * scaleFactor); + int lw = Math.round( width2 * animatedValue ); + + g2d.setColor( Color.red ); + g2d.fill( new Rectangle2D.Float( x2 + (width2 - lw) / 2, y2 + height2 - lh, lw, lh ) ); + } + } ); + + if( animatedValue != 0 && animatedValue != 1 ) { + Color chartColor = (Color) ((JComponent)c).getClientProperty( CHART_COLOR_KEY ); + lineChartPanel.addValue( chartColor, animatedValue, Integer.MIN_VALUE, "material" ); + } + } + + @Override + public void repaintDuringAnimation( Component c, int x, int y, int width, int height ) { + // limit repaint to bottom border + int lh = UIScale.scale( 2 ); + c.repaint( x, y + height - lh, width, lh ); + } + + @Override + public Insets getBorderInsets( Component c, Insets insets ) { + insets.top = insets.bottom = UIScale.scale( 3 ); + insets.left = insets.right = UIScale.scale( 7 ); + return insets; + } + + @Override + public float[] getAnimatableValues( Component c ) { + return new float[] { FlatUIUtils.isPermanentFocusOwner( c ) ? 1 : 0 }; + } + + @Override + public int getAnimationDuration() { + return (Integer) durationField.getValue(); + } + } + + //---- class AnimatedMaterialLabeledBorder -------------------------------- + + /** + * Experimental text field border that: + * - paints a label above the text, or in center if text field is empty + * - paint border only at bottom + * - animates focus indicator at bottom + */ + private class AnimatedMaterialLabeledBorder + extends AnimatedMaterialBorder + { + static final String LABEL_TEXT_KEY = "JTextField.labelText"; + + private static final float LABEL_FONT_SCALE = 0.75f; + + @Override + public void paintAnimated( Component c, Graphics2D g, int x, int y, int width, int height, float[] animatedValues ) { + super.paintAnimated( c, g, x, y, width, height, animatedValues ); + + float animatedValue = animatedValues[0]; + JComponent jc = (JComponent) c; + String label = (String) jc.getClientProperty( LABEL_TEXT_KEY ); + if( label == null ) + return; + + FontMetrics fm = c.getFontMetrics( c.getFont() ); + int labelFontHeight = Math.round( fm.getHeight() * LABEL_FONT_SCALE ); + + int tx = UIScale.scale( 7 ); + int ty = y + labelFontHeight - UIScale.scale( 2 ); + float sf = LABEL_FONT_SCALE; + + if( ((JTextField)c).getDocument().getLength() == 0 ) { + // paint label in center of text field if it is empty + int ty2 = ((height - fm.getHeight()) / 2) + labelFontHeight; + ty += (ty2 - ty) * (1 - animatedValue); + sf += (1 - LABEL_FONT_SCALE) * (1 - animatedValue); + } + + Graphics2D g2 = (Graphics2D) g.create(); + try { + g2.translate( tx, ty ); + g2.scale( sf, sf ); + g2.setColor( ColorFunctions.mix( Color.red, Color.gray, animatedValue ) ); + + FlatUIUtils.drawString( jc, g2, label, 0, 0 ); + } finally { + g2.dispose(); + } + } + + @Override + public void repaintDuringAnimation( Component c, int x, int y, int width, int height ) { + c.repaint( x, y, width, height ); + } + + @Override + public Insets getBorderInsets( Component c, Insets insets ) { + super.getBorderInsets( c, insets ); + + FontMetrics fm = c.getFontMetrics( c.getFont() ); + int labelFontHeight = Math.round( fm.getHeight() * LABEL_FONT_SCALE ); + insets.top = labelFontHeight; + insets.bottom = UIScale.scale( 5 ); + return insets; + } + } + + //---- class AnimatedMinimalTestBorder ------------------------------------ + + /** + * Minimal example for an animated border. + */ + private class AnimatedMinimalTestBorder + implements AnimatedBorder + { + @Override + public void paintAnimated( Component c, Graphics2D g, int x, int y, int width, int height, float[] animatedValues ) { + float animatedValue = animatedValues[0]; + int lh = UIScale.scale( 2 ); + + g.setColor( Color.blue ); + g.fillRect( x, y + height - lh, Math.round( width * animatedValue ), lh ); + + if( animatedValue != 0 && animatedValue != 1 ) { + Color chartColor = (Color) ((JComponent)c).getClientProperty( CHART_COLOR_KEY ); + lineChartPanel.addValue( chartColor, animatedValue, Integer.MIN_VALUE, "minimal" ); + } + } + + @Override + public float[] getAnimatableValues( Component c ) { + return new float[] { FlatUIUtils.isPermanentFocusOwner( c ) ? 1 : 0 }; + } + + @Override + public int getAnimationDuration() { + return (Integer) durationField.getValue(); + } + + @Override + public Insets getBorderInsets( Component c ) { + return UIScale.scale( new Insets( 3, 7, 3, 7 ) ); + } + + @Override + public boolean isBorderOpaque() { + return false; + } + } +} diff --git a/flatlaf-testing/src/main/java/com/formdev/flatlaf/testing/FlatAnimatedBorderTest.jfd b/flatlaf-testing/src/main/java/com/formdev/flatlaf/testing/FlatAnimatedBorderTest.jfd new file mode 100644 index 000000000..8c0230827 --- /dev/null +++ b/flatlaf-testing/src/main/java/com/formdev/flatlaf/testing/FlatAnimatedBorderTest.jfd @@ -0,0 +1,128 @@ +JFDML JFormDesigner: "8.3" encoding: "UTF-8" + +new FormModel { + contentType: "form/swing" + root: new FormRoot { + add( new FormContainer( "com.formdev.flatlaf.testing.FlatTestPanel", new FormLayoutManager( class net.miginfocom.swing.MigLayout ) { + "$layoutConstraints": "insets dialog,hidemode 3" + "$columnConstraints": "[fill][fill]para[grow,fill]" + "$rowConstraints": "[][][]para[][][][][]para[][][grow][]" + } ) { + name: "this" + add( new FormComponent( "javax.swing.JLabel" ) { + name: "label3" + "text": "Fade:" + }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { + "value": "cell 0 0" + } ) + add( new FormComponent( "com.formdev.flatlaf.testing.LineChartPanel" ) { + name: "lineChartPanel" + }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { + "value": "cell 2 0 1 12,growy" + } ) + add( new FormComponent( "javax.swing.JTextField" ) { + name: "fade1TextField" + }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { + "value": "cell 0 1" + } ) + add( new FormComponent( "com.formdev.flatlaf.testing.FlatAnimatorTest$JChartColor" ) { + name: "fade1ChartColor" + }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { + "value": "cell 1 1" + } ) + add( new FormComponent( "javax.swing.JTextField" ) { + name: "fade2TextField" + }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { + "value": "cell 0 2" + } ) + add( new FormComponent( "com.formdev.flatlaf.testing.FlatAnimatorTest$JChartColor" ) { + name: "fade2ChartColor" + }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { + "value": "cell 1 2" + } ) + add( new FormComponent( "javax.swing.JLabel" ) { + name: "label2" + "text": "Material:" + }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { + "value": "cell 0 3" + } ) + add( new FormComponent( "javax.swing.JTextField" ) { + name: "material1TextField" + }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { + "value": "cell 0 4" + } ) + add( new FormComponent( "com.formdev.flatlaf.testing.FlatAnimatorTest$JChartColor" ) { + name: "material1ChartColor" + }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { + "value": "cell 1 4" + } ) + add( new FormComponent( "javax.swing.JTextField" ) { + name: "material2TextField" + }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { + "value": "cell 0 5" + } ) + add( new FormComponent( "com.formdev.flatlaf.testing.FlatAnimatorTest$JChartColor" ) { + name: "material2ChartColor" + }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { + "value": "cell 1 5" + } ) + add( new FormComponent( "javax.swing.JTextField" ) { + name: "material3TextField" + "$client.FlatLaf.styleClass": "large" + }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { + "value": "cell 0 6" + } ) + add( new FormComponent( "com.formdev.flatlaf.testing.FlatAnimatorTest$JChartColor" ) { + name: "material3ChartColor" + }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { + "value": "cell 1 6" + } ) + add( new FormComponent( "javax.swing.JTextField" ) { + name: "material4TextField" + "$client.FlatLaf.styleClass": "large" + }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { + "value": "cell 0 7" + } ) + add( new FormComponent( "com.formdev.flatlaf.testing.FlatAnimatorTest$JChartColor" ) { + name: "material4ChartColor" + }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { + "value": "cell 1 7" + } ) + add( new FormComponent( "javax.swing.JLabel" ) { + name: "label1" + "text": "Minimal:" + }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { + "value": "cell 0 8" + } ) + add( new FormComponent( "javax.swing.JTextField" ) { + name: "minimalTextField" + }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { + "value": "cell 0 9" + } ) + add( new FormComponent( "com.formdev.flatlaf.testing.FlatAnimatorTest$JChartColor" ) { + name: "minimalChartColor" + }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { + "value": "cell 1 9" + } ) + add( new FormComponent( "javax.swing.JLabel" ) { + name: "durationLabel" + "text": "Duration:" + }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { + "value": "cell 0 11" + } ) + add( new FormComponent( "javax.swing.JSpinner" ) { + name: "durationField" + "model": new javax.swing.SpinnerNumberModel { + minimum: 0 + stepSize: 50 + value: 200 + } + }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { + "value": "cell 0 11" + } ) + }, new FormLayoutConstraints( null ) { + "location": new java.awt.Point( 0, 0 ) + "size": new java.awt.Dimension( 725, 465 ) + } ) + } +} diff --git a/flatlaf-testing/src/main/java/com/formdev/flatlaf/testing/FlatAnimatedIconTest.java b/flatlaf-testing/src/main/java/com/formdev/flatlaf/testing/FlatAnimatedIconTest.java index 160caca65..3cf352c1c 100644 --- a/flatlaf-testing/src/main/java/com/formdev/flatlaf/testing/FlatAnimatedIconTest.java +++ b/flatlaf-testing/src/main/java/com/formdev/flatlaf/testing/FlatAnimatedIconTest.java @@ -18,13 +18,14 @@ import java.awt.Color; import java.awt.Component; -import java.awt.Graphics; import java.awt.Graphics2D; import java.awt.geom.Ellipse2D; +import java.awt.geom.Path2D; import javax.swing.*; import com.formdev.flatlaf.icons.FlatAnimatedIcon; import com.formdev.flatlaf.util.AnimatedIcon; import com.formdev.flatlaf.util.ColorFunctions; +import com.formdev.flatlaf.util.UIScale; import net.miginfocom.swing.*; /** @@ -33,6 +34,15 @@ public class FlatAnimatedIconTest extends FlatTestPanel { + private static final Color CHART_RADIO_BUTTON_1 = Color.blue; + private static final Color CHART_RADIO_BUTTON_2 = Color.red; + private static final Color CHART_RADIO_BUTTON_3 = Color.green; + private static final Color CHART_CHECK_BOX_1 = Color.magenta; + private static final Color CHART_CHECK_BOX_2 = Color.orange; + private static final Color[] CHART_SWITCH_EX = { Color.red, Color.green, Color.blue }; + + private static final String CHART_COLOR_KEY = "chartColor"; + public static void main( String[] args ) { SwingUtilities.invokeLater( () -> { FlatTestFrame frame = FlatTestFrame.create( args, "FlatAnimatedIconTest" ); @@ -49,16 +59,36 @@ public static void main( String[] args ) { radioButton3.setIcon( radioIcon ); checkBox1.setIcon( new AnimatedSwitchIcon() ); + checkBox3.setIcon( new AnimatedSwitchIconEx() ); checkBox2.setIcon( new AnimatedMinimalTestIcon() ); + + radioButton1.putClientProperty( CHART_COLOR_KEY, CHART_RADIO_BUTTON_1 ); + radioButton2.putClientProperty( CHART_COLOR_KEY, CHART_RADIO_BUTTON_2 ); + radioButton3.putClientProperty( CHART_COLOR_KEY, CHART_RADIO_BUTTON_3 ); + checkBox1.putClientProperty( CHART_COLOR_KEY, CHART_CHECK_BOX_1 ); + checkBox2.putClientProperty( CHART_COLOR_KEY, CHART_CHECK_BOX_2 ); + + radioButton1ChartColor.setForeground( CHART_RADIO_BUTTON_1 ); + radioButton2ChartColor.setForeground( CHART_RADIO_BUTTON_2 ); + radioButton3ChartColor.setForeground( CHART_RADIO_BUTTON_3 ); + checkBox1ChartColor.setForeground( CHART_CHECK_BOX_1 ); + checkBox2ChartColor.setForeground( CHART_CHECK_BOX_2 ); } private void initComponents() { // JFormDesigner - Component initialization - DO NOT MODIFY //GEN-BEGIN:initComponents radioButton1 = new JRadioButton(); + radioButton1ChartColor = new FlatAnimatorTest.JChartColor(); + lineChartPanel = new LineChartPanel(); radioButton2 = new JRadioButton(); + radioButton2ChartColor = new FlatAnimatorTest.JChartColor(); radioButton3 = new JRadioButton(); + radioButton3ChartColor = new FlatAnimatorTest.JChartColor(); checkBox1 = new JCheckBox(); + checkBox1ChartColor = new FlatAnimatorTest.JChartColor(); + checkBox3 = new JCheckBox(); checkBox2 = new JCheckBox(); + checkBox2ChartColor = new FlatAnimatorTest.JChartColor(); durationLabel = new JLabel(); durationField = new JSpinner(); @@ -66,14 +96,16 @@ private void initComponents() { setLayout(new MigLayout( "insets dialog,hidemode 3", // columns - "[]para" + - "[fill]", + "[]" + + "[fill]para" + + "[grow,fill]", // rows "[]" + "[]" + "[]para" + "[]" + "[]" + + "[]" + "[grow]" + "[]")); @@ -81,30 +113,40 @@ private void initComponents() { radioButton1.setText("radio 1"); radioButton1.setSelected(true); add(radioButton1, "cell 0 0"); + add(radioButton1ChartColor, "cell 1 0"); + add(lineChartPanel, "cell 2 0 1 8,growy"); //---- radioButton2 ---- radioButton2.setText("radio 2"); add(radioButton2, "cell 0 1"); + add(radioButton2ChartColor, "cell 1 1"); //---- radioButton3 ---- radioButton3.setText("radio 3"); add(radioButton3, "cell 0 2"); + add(radioButton3ChartColor, "cell 1 2"); //---- checkBox1 ---- checkBox1.setText("switch"); add(checkBox1, "cell 0 3"); + add(checkBox1ChartColor, "cell 1 3"); + + //---- checkBox3 ---- + checkBox3.setText("switch ex"); + add(checkBox3, "cell 0 4"); //---- checkBox2 ---- checkBox2.setText("minimal"); - add(checkBox2, "cell 0 4"); + add(checkBox2, "cell 0 5"); + add(checkBox2ChartColor, "cell 1 5"); //---- durationLabel ---- durationLabel.setText("Duration:"); - add(durationLabel, "cell 0 6 2 1"); + add(durationLabel, "cell 0 7 2 1"); //---- durationField ---- - durationField.setModel(new SpinnerNumberModel(200, 100, null, 50)); - add(durationField, "cell 0 6 2 1"); + durationField.setModel(new SpinnerNumberModel(200, 0, null, 50)); + add(durationField, "cell 0 7 2 1"); //---- buttonGroup1 ---- ButtonGroup buttonGroup1 = new ButtonGroup(); @@ -116,10 +158,17 @@ private void initComponents() { // JFormDesigner - Variables declaration - DO NOT MODIFY //GEN-BEGIN:variables private JRadioButton radioButton1; + private FlatAnimatorTest.JChartColor radioButton1ChartColor; + private LineChartPanel lineChartPanel; private JRadioButton radioButton2; + private FlatAnimatorTest.JChartColor radioButton2ChartColor; private JRadioButton radioButton3; + private FlatAnimatorTest.JChartColor radioButton3ChartColor; private JCheckBox checkBox1; + private FlatAnimatorTest.JChartColor checkBox1ChartColor; + private JCheckBox checkBox3; private JCheckBox checkBox2; + private FlatAnimatorTest.JChartColor checkBox2ChartColor; private JLabel durationLabel; private JSpinner durationField; // JFormDesigner - End of variables declaration //GEN-END:variables @@ -146,7 +195,8 @@ public AnimatedRadioButtonIcon() { } @Override - public void paintIconAnimated( Component c, Graphics g, int x, int y, float animatedValue ) { + public void paintIconAnimated( Component c, Graphics2D g, float[] animatedValues ) { + float animatedValue = animatedValues[0]; Color color = ColorFunctions.mix( onColor, offColor, animatedValue ); // border @@ -162,12 +212,17 @@ public void paintIconAnimated( Component c, Graphics g, int x, int y, float anim float dotDiameter = DOT_SIZE * animatedValue; float xy = (SIZE - dotDiameter) / 2f; g.setColor( color ); - ((Graphics2D)g).fill( new Ellipse2D.Float( xy, xy, dotDiameter, dotDiameter ) ); + g.fill( new Ellipse2D.Float( xy, xy, dotDiameter, dotDiameter ) ); + + if( animatedValue != 0 && animatedValue != 1 ) { + Color chartColor = (Color) ((JComponent)c).getClientProperty( CHART_COLOR_KEY ); + lineChartPanel.addValue( chartColor, animatedValue, Integer.MIN_VALUE, "radio" ); + } } @Override - public float getValue( Component c ) { - return ((JRadioButton)c).isSelected() ? 1 : 0; + public float[] getAnimatableValues( Component c ) { + return new float[] { ((JRadioButton)c).isSelected() ? 1 : 0 }; } @Override @@ -178,7 +233,7 @@ public int getAnimationDuration() { //---- class AnimatedSwitchIcon ------------------------------------------- - public class AnimatedSwitchIcon + private class AnimatedSwitchIcon extends FlatAnimatedIcon { private final Color offColor = Color.lightGray; @@ -189,22 +244,113 @@ public AnimatedSwitchIcon() { } @Override - public void paintIconAnimated( Component c, Graphics g, int x, int y, float animatedValue ) { + public void paintIconAnimated( Component c, Graphics2D g, float[] animatedValues ) { + float animatedValue = animatedValues[0]; Color color = ColorFunctions.mix( onColor, offColor, animatedValue ); + // paint track + g.setColor( color ); + g.fillRoundRect( 0, 0, width, height, height, height ); + + // paint thumb + int thumbSize = height - 4; + float thumbX = 2 + ((width - 4 - thumbSize) * animatedValue); + int thumbY = 2; + g.setColor( Color.white ); + g.fill( new Ellipse2D.Float( thumbX, thumbY, thumbSize, thumbSize ) ); + + if( animatedValue != 0 && animatedValue != 1 ) { + Color chartColor = (Color) ((JComponent)c).getClientProperty( CHART_COLOR_KEY ); + lineChartPanel.addValue( chartColor, animatedValue, Integer.MIN_VALUE, "switch" ); + } + } + + @Override + public float[] getAnimatableValues( Component c ) { + return new float[] { ((AbstractButton)c).isSelected() ? 1 : 0 }; + } + + @Override + public int getAnimationDuration() { + return (Integer) durationField.getValue(); + } + } + + //---- class AnimatedSwitchIconEx ----------------------------------------- + + private static final int HW = 8; + + private class AnimatedSwitchIconEx + extends FlatAnimatedIcon + { + private final Color offColor = Color.lightGray; + private final Color onColor = Color.red; + private final Color hoverColor = new Color( 0x4400cc00, true ); + private final Color pressedColor = new Color( 0x440000cc, true ); + + public AnimatedSwitchIconEx() { + super( 28 + HW, 16 + HW, null ); + } + + @Override + public void paintIconAnimated( Component c, Graphics2D g, float[] animatedValues ) { + Color color = ColorFunctions.mix( onColor, offColor, animatedValues[0] ); + + int hw2 = HW / 2; + int x = hw2; + int y = hw2; + int width = this.width - HW; + int height = this.height - HW; + + // paint track g.setColor( color ); g.fillRoundRect( x, y, width, height, height, height ); + // paint thumb int thumbSize = height - 4; - float thumbX = x + 2 + ((width - 4 - thumbSize) * animatedValue); + float thumbX = x + 2 + ((width - 4 - thumbSize) * animatedValues[0]); int thumbY = y + 2; g.setColor( Color.white ); - ((Graphics2D)g).fill( new Ellipse2D.Float( thumbX, thumbY, thumbSize, thumbSize ) ); + g.fill( new Ellipse2D.Float( thumbX, thumbY, thumbSize, thumbSize ) ); + + // paint hover + if( animatedValues[1] > 0 ) { + g.setColor( hoverColor ); + paintHoverOrPressed( g, thumbX, thumbY, thumbSize, animatedValues[1] ); + } + + // paint pressed + if( animatedValues[2] > 0 ) { + g.setColor( pressedColor ); + paintHoverOrPressed( g, thumbX, thumbY, thumbSize, animatedValues[2] ); + } + + for( int i = 0; i < animatedValues.length; i++ ) { + float animatedValue = animatedValues[i]; + if( animatedValue != 0 && animatedValue != 1 ) + lineChartPanel.addValue( CHART_SWITCH_EX[i], animatedValue, Integer.MIN_VALUE, "switch ex" ); + } + } + + private void paintHoverOrPressed( Graphics2D g, float thumbX, int thumbY, int thumbSize, float animatedValue ) { + float hw = (HW + 4) * animatedValue; + Path2D path = new Path2D.Float( Path2D.WIND_EVEN_ODD ); + path.append( new Ellipse2D.Float( thumbX - (hw / 2), thumbY - (hw / 2), + thumbSize + hw, thumbSize + hw ), false ); + path.append( new Ellipse2D.Float( thumbX, thumbY, thumbSize, thumbSize ), false ); + g.fill( path ); } @Override - public float getValue( Component c ) { - return ((AbstractButton)c).isSelected() ? 1 : 0; + public float[] getAnimatableValues( Component c ) { + AbstractButton b = (AbstractButton) c; + ButtonModel bm = b.getModel(); + + return new float[] { + b.isSelected() ? 1 : 0, + bm.isRollover() ? 1 : 0, + bm.isPressed() ? 1 : 0, + }; } @Override @@ -223,27 +369,31 @@ private class AnimatedMinimalTestIcon { @Override public int getIconWidth() { - return 100; + return UIScale.scale( 50 ); } @Override public int getIconHeight() { - return 20; + return UIScale.scale( 16 ); } @Override - public void paintIconAnimated( Component c, Graphics g, int x, int y, float animatedValue ) { - int w = getIconWidth(); - int h = getIconHeight(); + public void paintAnimated( Component c, Graphics2D g, int x, int y, int width, int height, float[] animatedValues ) { + float animatedValue = animatedValues[0]; g.setColor( Color.red ); - g.drawRect( x, y, w - 1, h - 1 ); - g.fillRect( x, y, Math.round( w * animatedValue ), h ); + g.drawRect( x, y, width - 1, height - 1 ); + g.fillRect( x, y, Math.round( width * animatedValue ), height ); + + if( animatedValue != 0 && animatedValue != 1 ) { + Color chartColor = (Color) ((JComponent)c).getClientProperty( CHART_COLOR_KEY ); + lineChartPanel.addValue( chartColor, animatedValue, Integer.MIN_VALUE, "minimal" ); + } } @Override - public float getValue( Component c ) { - return ((AbstractButton)c).isSelected() ? 1 : 0; + public float[] getAnimatableValues( Component c ) { + return new float[] { ((AbstractButton)c).isSelected() ? 1 : 0 }; } @Override diff --git a/flatlaf-testing/src/main/java/com/formdev/flatlaf/testing/FlatAnimatedIconTest.jfd b/flatlaf-testing/src/main/java/com/formdev/flatlaf/testing/FlatAnimatedIconTest.jfd index 11c55e811..dd7867fc6 100644 --- a/flatlaf-testing/src/main/java/com/formdev/flatlaf/testing/FlatAnimatedIconTest.jfd +++ b/flatlaf-testing/src/main/java/com/formdev/flatlaf/testing/FlatAnimatedIconTest.jfd @@ -1,12 +1,12 @@ -JFDML JFormDesigner: "7.0.2.0.298" Java: "15" encoding: "UTF-8" +JFDML JFormDesigner: "8.3" encoding: "UTF-8" new FormModel { contentType: "form/swing" root: new FormRoot { add( new FormContainer( "com.formdev.flatlaf.testing.FlatTestPanel", new FormLayoutManager( class net.miginfocom.swing.MigLayout ) { "$layoutConstraints": "insets dialog,hidemode 3" - "$columnConstraints": "[]para[fill]" - "$rowConstraints": "[][][]para[][][grow][]" + "$columnConstraints": "[][fill]para[grow,fill]" + "$rowConstraints": "[][][]para[][][][grow][]" } ) { name: "this" add( new FormComponent( "javax.swing.JRadioButton" ) { @@ -17,6 +17,16 @@ new FormModel { }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { "value": "cell 0 0" } ) + add( new FormComponent( "com.formdev.flatlaf.testing.FlatAnimatorTest$JChartColor" ) { + name: "radioButton1ChartColor" + }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { + "value": "cell 1 0" + } ) + add( new FormComponent( "com.formdev.flatlaf.testing.LineChartPanel" ) { + name: "lineChartPanel" + }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { + "value": "cell 2 0 1 8,growy" + } ) add( new FormComponent( "javax.swing.JRadioButton" ) { name: "radioButton2" "text": "radio 2" @@ -24,6 +34,11 @@ new FormModel { }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { "value": "cell 0 1" } ) + add( new FormComponent( "com.formdev.flatlaf.testing.FlatAnimatorTest$JChartColor" ) { + name: "radioButton2ChartColor" + }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { + "value": "cell 1 1" + } ) add( new FormComponent( "javax.swing.JRadioButton" ) { name: "radioButton3" "text": "radio 3" @@ -31,37 +46,58 @@ new FormModel { }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { "value": "cell 0 2" } ) + add( new FormComponent( "com.formdev.flatlaf.testing.FlatAnimatorTest$JChartColor" ) { + name: "radioButton3ChartColor" + }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { + "value": "cell 1 2" + } ) add( new FormComponent( "javax.swing.JCheckBox" ) { name: "checkBox1" "text": "switch" }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { "value": "cell 0 3" } ) + add( new FormComponent( "com.formdev.flatlaf.testing.FlatAnimatorTest$JChartColor" ) { + name: "checkBox1ChartColor" + }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { + "value": "cell 1 3" + } ) + add( new FormComponent( "javax.swing.JCheckBox" ) { + name: "checkBox3" + "text": "switch ex" + }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { + "value": "cell 0 4" + } ) add( new FormComponent( "javax.swing.JCheckBox" ) { name: "checkBox2" "text": "minimal" }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { - "value": "cell 0 4" + "value": "cell 0 5" + } ) + add( new FormComponent( "com.formdev.flatlaf.testing.FlatAnimatorTest$JChartColor" ) { + name: "checkBox2ChartColor" + }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { + "value": "cell 1 5" } ) add( new FormComponent( "javax.swing.JLabel" ) { name: "durationLabel" "text": "Duration:" }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { - "value": "cell 0 6 2 1" + "value": "cell 0 7 2 1" } ) add( new FormComponent( "javax.swing.JSpinner" ) { name: "durationField" "model": new javax.swing.SpinnerNumberModel { - minimum: 100 + minimum: 0 stepSize: 50 value: 200 } }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { - "value": "cell 0 6 2 1" + "value": "cell 0 7 2 1" } ) }, new FormLayoutConstraints( null ) { "location": new java.awt.Point( 0, 0 ) - "size": new java.awt.Dimension( 415, 350 ) + "size": new java.awt.Dimension( 810, 350 ) } ) add( new FormNonVisual( "javax.swing.ButtonGroup" ) { name: "buttonGroup1" diff --git a/flatlaf-testing/src/main/java/com/formdev/flatlaf/testing/FlatAnimatorTest.java b/flatlaf-testing/src/main/java/com/formdev/flatlaf/testing/FlatAnimatorTest.java index 831a132d7..7cb6e6516 100644 --- a/flatlaf-testing/src/main/java/com/formdev/flatlaf/testing/FlatAnimatorTest.java +++ b/flatlaf-testing/src/main/java/com/formdev/flatlaf/testing/FlatAnimatorTest.java @@ -20,6 +20,8 @@ import javax.swing.*; import com.formdev.flatlaf.util.Animator; import com.formdev.flatlaf.util.CubicBezierEasing; +import com.formdev.flatlaf.util.UIScale; +import com.formdev.flatlaf.util.Animator.Interpolator; import net.miginfocom.swing.*; /** @@ -28,8 +30,13 @@ public class FlatAnimatorTest extends FlatTestPanel { + private static final Color CHART_LINEAR = Color.blue; + private static final Color CHART_EASE_IN_OUT = Color.magenta; + private static final Color CHART_STANDARD_EASING = Color.red; + private Animator linearAnimator; private Animator easeInOutAnimator; + private Animator standardEasingAnimator; public static void main( String[] args ) { SwingUtilities.invokeLater( () -> { @@ -40,85 +47,132 @@ public static void main( String[] args ) { FlatAnimatorTest() { initComponents(); - } - private void start() { - startLinear(); - startEaseInOut(); + linearChartColor.setForeground( CHART_LINEAR ); + easeInOutChartColor.setForeground( CHART_EASE_IN_OUT ); + standardEasingChartColor.setForeground( CHART_STANDARD_EASING ); } - private void startLinear() { - if( linearAnimator != null ) { - linearAnimator.stop(); - linearAnimator.start(); - } else { - linearAnimator = new Animator( 1000, fraction -> { - linearScrollBar.setValue( Math.round( fraction * linearScrollBar.getMaximum() ) ); - } ); - linearAnimator.start(); - } + private void start() { + linearAnimator = start( linearAnimator, null, linearScrollBar, CHART_LINEAR ); + easeInOutAnimator = start( easeInOutAnimator, CubicBezierEasing.EASE_IN_OUT, easeInOutScrollBar, CHART_EASE_IN_OUT ); + standardEasingAnimator = start( standardEasingAnimator, CubicBezierEasing.STANDARD_EASING, standardEasingScrollBar, CHART_STANDARD_EASING ); } - private void startEaseInOut() { - if( easeInOutAnimator != null ) { - easeInOutAnimator.stop(); - easeInOutAnimator.start(); + private Animator start( Animator animator, Interpolator interpolator, JScrollBar scrollBar, Color chartColor ) { + if( animator != null ) { + animator.stop(); + animator.start(); } else { - easeInOutAnimator = new Animator( 1000, fraction -> { - easeInOutScrollBar.setValue( Math.round( fraction * easeInOutScrollBar.getMaximum() ) ); + animator = new Animator( 1000, fraction -> { + scrollBar.setValue( Math.round( fraction * scrollBar.getMaximum() ) ); + lineChartPanel.addValue( chartColor, fraction, Integer.MIN_VALUE, "animator" ); } ); - easeInOutAnimator.setInterpolator( CubicBezierEasing.EASE_IN_OUT ); - easeInOutAnimator.start(); + animator.setInterpolator( interpolator ); + animator.start(); } + return animator; } private void initComponents() { // JFormDesigner - Component initialization - DO NOT MODIFY //GEN-BEGIN:initComponents - JLabel label1 = new JLabel(); + linearLabel = new JLabel(); + linearChartColor = new FlatAnimatorTest.JChartColor(); linearScrollBar = new JScrollBar(); - JLabel label2 = new JLabel(); + easeInOutLabel = new JLabel(); + easeInOutChartColor = new FlatAnimatorTest.JChartColor(); easeInOutScrollBar = new JScrollBar(); + standardEasingLabel = new JLabel(); + standardEasingChartColor = new FlatAnimatorTest.JChartColor(); + standardEasingScrollBar = new JScrollBar(); startButton = new JButton(); + lineChartPanel = new LineChartPanel(); //======== this ======== setLayout(new MigLayout( "ltr,insets dialog,hidemode 3", // columns "[fill]" + + "[fill]" + "[grow,fill]", // rows "[]" + "[]" + - "[]")); + "[]" + + "[]para" + + "[400,grow,fill]")); - //---- label1 ---- - label1.setText("Linear:"); - add(label1, "cell 0 0"); + //---- linearLabel ---- + linearLabel.setText("Linear:"); + add(linearLabel, "cell 0 0"); + add(linearChartColor, "cell 1 0"); //---- linearScrollBar ---- linearScrollBar.setOrientation(Adjustable.HORIZONTAL); linearScrollBar.setBlockIncrement(1); - add(linearScrollBar, "cell 1 0"); + add(linearScrollBar, "cell 2 0"); - //---- label2 ---- - label2.setText("Ease in out:"); - add(label2, "cell 0 1"); + //---- easeInOutLabel ---- + easeInOutLabel.setText("Ease in out:"); + add(easeInOutLabel, "cell 0 1"); + add(easeInOutChartColor, "cell 1 1"); //---- easeInOutScrollBar ---- easeInOutScrollBar.setOrientation(Adjustable.HORIZONTAL); easeInOutScrollBar.setBlockIncrement(1); - add(easeInOutScrollBar, "cell 1 1"); + add(easeInOutScrollBar, "cell 2 1"); + + //---- standardEasingLabel ---- + standardEasingLabel.setText("Standard easing:"); + add(standardEasingLabel, "cell 0 2"); + add(standardEasingChartColor, "cell 1 2"); + + //---- standardEasingScrollBar ---- + standardEasingScrollBar.setOrientation(Adjustable.HORIZONTAL); + standardEasingScrollBar.setBlockIncrement(1); + add(standardEasingScrollBar, "cell 2 2"); //---- startButton ---- startButton.setText("Start"); startButton.addActionListener(e -> start()); - add(startButton, "cell 0 2"); + add(startButton, "cell 0 3"); + add(lineChartPanel, "cell 0 4 3 1"); // JFormDesigner - End of component initialization //GEN-END:initComponents } // JFormDesigner - Variables declaration - DO NOT MODIFY //GEN-BEGIN:variables + private JLabel linearLabel; + private FlatAnimatorTest.JChartColor linearChartColor; private JScrollBar linearScrollBar; + private JLabel easeInOutLabel; + private FlatAnimatorTest.JChartColor easeInOutChartColor; private JScrollBar easeInOutScrollBar; + private JLabel standardEasingLabel; + private FlatAnimatorTest.JChartColor standardEasingChartColor; + private JScrollBar standardEasingScrollBar; private JButton startButton; + private LineChartPanel lineChartPanel; // JFormDesigner - End of variables declaration //GEN-END:variables + + //---- class JChartColor -------------------------------------------------- + + static class JChartColor + extends JComponent + { + @Override + public Dimension getPreferredSize() { + return new Dimension( UIScale.scale( 24 ), UIScale.scale( 12 ) ); + } + + @Override + public Dimension getMinimumSize() { + return getPreferredSize(); + } + + @Override + protected void paintComponent( Graphics g ) { + g.setColor( getForeground() ); + g.fillRect( 0, 0, UIScale.scale( 24 ), UIScale.scale( 12 ) ); + } + } } diff --git a/flatlaf-testing/src/main/java/com/formdev/flatlaf/testing/FlatAnimatorTest.jfd b/flatlaf-testing/src/main/java/com/formdev/flatlaf/testing/FlatAnimatorTest.jfd index bb92a1299..c3bf86cf3 100644 --- a/flatlaf-testing/src/main/java/com/formdev/flatlaf/testing/FlatAnimatorTest.jfd +++ b/flatlaf-testing/src/main/java/com/formdev/flatlaf/testing/FlatAnimatorTest.jfd @@ -1,4 +1,4 @@ -JFDML JFormDesigner: "7.0.2.0.298" Java: "14.0.2" encoding: "UTF-8" +JFDML JFormDesigner: "8.3" encoding: "UTF-8" new FormModel { contentType: "form/swing" @@ -8,16 +8,27 @@ new FormModel { } add( new FormContainer( "com.formdev.flatlaf.testing.FlatTestPanel", new FormLayoutManager( class net.miginfocom.swing.MigLayout ) { "$layoutConstraints": "ltr,insets dialog,hidemode 3" - "$columnConstraints": "[fill][grow,fill]" - "$rowConstraints": "[][][]" + "$columnConstraints": "[fill][fill][grow,fill]" + "$rowConstraints": "[][][][]para[400,grow,fill]" } ) { name: "this" add( new FormComponent( "javax.swing.JLabel" ) { - name: "label1" + name: "linearLabel" "text": "Linear:" + auxiliary() { + "JavaCodeGenerator.variableLocal": false + } }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { "value": "cell 0 0" } ) + add( new FormComponent( "com.formdev.flatlaf.testing.FlatAnimatorTest$JChartColor" ) { + name: "linearChartColor" + auxiliary() { + "JavaCodeGenerator.variableLocal": false + } + }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { + "value": "cell 1 0" + } ) add( new FormComponent( "javax.swing.JScrollBar" ) { name: "linearScrollBar" "orientation": 0 @@ -26,14 +37,25 @@ new FormModel { "JavaCodeGenerator.variableLocal": false } }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { - "value": "cell 1 0" + "value": "cell 2 0" } ) add( new FormComponent( "javax.swing.JLabel" ) { - name: "label2" + name: "easeInOutLabel" "text": "Ease in out:" + auxiliary() { + "JavaCodeGenerator.variableLocal": false + } }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { "value": "cell 0 1" } ) + add( new FormComponent( "com.formdev.flatlaf.testing.FlatAnimatorTest$JChartColor" ) { + name: "easeInOutChartColor" + auxiliary() { + "JavaCodeGenerator.variableLocal": false + } + }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { + "value": "cell 1 1" + } ) add( new FormComponent( "javax.swing.JScrollBar" ) { name: "easeInOutScrollBar" "orientation": 0 @@ -42,7 +64,34 @@ new FormModel { "JavaCodeGenerator.variableLocal": false } }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { - "value": "cell 1 1" + "value": "cell 2 1" + } ) + add( new FormComponent( "javax.swing.JLabel" ) { + name: "standardEasingLabel" + "text": "Standard easing:" + auxiliary() { + "JavaCodeGenerator.variableLocal": false + } + }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { + "value": "cell 0 2" + } ) + add( new FormComponent( "com.formdev.flatlaf.testing.FlatAnimatorTest$JChartColor" ) { + name: "standardEasingChartColor" + auxiliary() { + "JavaCodeGenerator.variableLocal": false + } + }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { + "value": "cell 1 2" + } ) + add( new FormComponent( "javax.swing.JScrollBar" ) { + name: "standardEasingScrollBar" + "orientation": 0 + "blockIncrement": 1 + auxiliary() { + "JavaCodeGenerator.variableLocal": false + } + }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { + "value": "cell 2 2" } ) add( new FormComponent( "javax.swing.JButton" ) { name: "startButton" @@ -52,11 +101,19 @@ new FormModel { } addEvent( new FormEvent( "java.awt.event.ActionListener", "actionPerformed", "start", false ) ) }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { - "value": "cell 0 2" + "value": "cell 0 3" + } ) + add( new FormComponent( "com.formdev.flatlaf.testing.LineChartPanel" ) { + name: "lineChartPanel" + auxiliary() { + "JavaCodeGenerator.variableLocal": false + } + }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { + "value": "cell 0 4 3 1" } ) }, new FormLayoutConstraints( null ) { "location": new java.awt.Point( 0, 0 ) - "size": new java.awt.Dimension( 415, 350 ) + "size": new java.awt.Dimension( 625, 625 ) } ) } } diff --git a/flatlaf-testing/src/main/java/com/formdev/flatlaf/testing/LineChartPanel.java b/flatlaf-testing/src/main/java/com/formdev/flatlaf/testing/LineChartPanel.java new file mode 100644 index 000000000..436ab871e --- /dev/null +++ b/flatlaf-testing/src/main/java/com/formdev/flatlaf/testing/LineChartPanel.java @@ -0,0 +1,883 @@ +/* +/* + * Copyright 2023 FormDev Software GmbH + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.formdev.flatlaf.testing; + +import java.awt.Color; +import java.awt.Dimension; +import java.awt.EventQueue; +import java.awt.Graphics; +import java.awt.Graphics2D; +import java.awt.Point; +import java.awt.Rectangle; +import java.awt.event.HierarchyEvent; +import java.awt.event.MouseEvent; +import java.text.MessageFormat; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import javax.swing.*; +import com.formdev.flatlaf.FlatClientProperties; +import com.formdev.flatlaf.FlatLaf; +import com.formdev.flatlaf.ui.FlatUIUtils; +import com.formdev.flatlaf.util.HSLColor; +import com.formdev.flatlaf.util.HiDPIUtils; +import com.formdev.flatlaf.util.UIScale; +import net.miginfocom.swing.*; + +/** + * @author Karl Tauber + */ +class LineChartPanel + extends JPanel +{ + LineChartPanel() { + initComponents(); + + lineChartScrollPane.putClientProperty( FlatClientProperties.SCROLL_PANE_SMOOTH_SCROLLING, false ); + + oneSecondWidthChanged(); + updateChartDelayedChanged(); + + // clear chart on startup + addHierarchyListener( e -> { + if( (e.getChangeFlags() & HierarchyEvent.SHOWING_CHANGED) != 0 && isShowing() ) + EventQueue.invokeLater( this::clearChart ); + } ); + + // show chart tooltips immediately and forever + ToolTipManager.sharedInstance().setInitialDelay( 0 ); + ToolTipManager.sharedInstance().setDismissDelay( Integer.MAX_VALUE ); + } + + @Override + public void addNotify() { + super.addNotify(); + + // allow clearing chart with Alt+C without moving focus to button + getRootPane().registerKeyboardAction( + e -> clearChart(), + KeyStroke.getKeyStroke( "alt " + (char) clearChartButton.getMnemonic() ), + JComponent.WHEN_ANCESTOR_OF_FOCUSED_COMPONENT ); + } + + public boolean isYZeroAtTop() { + return lineChart.yZeroAtTop; + } + + public void setYZeroAtTop( boolean yZeroAtTop ) { + lineChart.yZeroAtTop = yZeroAtTop; + lineChart.repaint(); + } + + public boolean isAsynchron() { + return lineChart.asynchron; + } + + public void setAsynchron( boolean asynchron ) { + lineChart.asynchron = asynchron; + lineChart.repaint(); + } + + public boolean isTemporaryValueDetection() { + return lineChart.temporaryValueDetection; + } + + public void setTemporaryValueDetection( boolean temporaryValueDetection ) { + lineChart.temporaryValueDetection = temporaryValueDetection; + lineChart.repaint(); + } + + public String getLegendYValueText() { + return yValueLabel.getText(); + } + + public void setLegendYValueText( String s ) { + yValueLabel.setText( s ); + } + + public String getLegend1Text() { + return legend1Label.getText(); + } + + public void setLegend1Text( String s ) { + legend1Label.setText( s ); + } + + public String getLegend2Text() { + return legend2Label.getText(); + } + + public void setLegend2Text( String s ) { + legend2Label.setText( s ); + } + + public int getOneSecondWidth() { + return oneSecondWidthSlider.getValue(); + } + + public void setOneSecondWidth( int oneSecondWidth ) { + oneSecondWidthSlider.setValue( oneSecondWidth ); + } + + public boolean isUpdateChartDelayed() { + return updateChartDelayedCheckBox.isSelected(); + } + + public void setUpdateChartDelayed( boolean updateChartDelayed ) { + updateChartDelayedCheckBox.setSelected( updateChartDelayed ); + updateChartDelayedChanged(); + } + + void addValue( Color chartColor, double value, int ivalue, String name ) { + lineChart.addValue( chartColor, value, ivalue, null, false, name ); + } + + void addValueWithDot( Color chartColor, double value, int ivalue, Color dotColor, String name ) { + if( dotColor == null ) + dotColor = chartColor; + lineChart.addValue( chartColor, value, ivalue, dotColor, false, name ); + } + + void addDot( Color chartColor, double value, int ivalue, Color dotColor, String name ) { + if( dotColor == null ) + dotColor = chartColor; + lineChart.addValue( chartColor, value, ivalue, dotColor, true, name ); + } + + void addMethodHighlight( String classAndMethod, String highlightColor ) { + lineChart.methodHighlightMap.put( classAndMethod, highlightColor ); + } + + private void oneSecondWidthChanged() { + int oneSecondWidth = oneSecondWidthSlider.getValue(); + int msPerLineX = + oneSecondWidth <= 2000 ? 100 : + oneSecondWidth <= 4000 ? 50 : + oneSecondWidth <= 8000 ? 25 : + 10; + + lineChart.oneSecondWidth = oneSecondWidth; + lineChart.msPerLineX = msPerLineX; + lineChart.revalidate(); + lineChart.repaint(); + + if( xLabelText == null ) + xLabelText = xLabel.getText(); + xLabel.setText( MessageFormat.format( xLabelText, msPerLineX ) ); + } + private String xLabelText; + + private void updateChartDelayedChanged() { + lineChart.updateDelayed = updateChartDelayedCheckBox.isSelected(); + } + + private void clearChart() { + lineChart.clear(); + } + + private void initComponents() { + // JFormDesigner - Component initialization - DO NOT MODIFY //GEN-BEGIN:initComponents @formatter:off + lineChartScrollPane = new JScrollPane(); + lineChart = new LineChartPanel.LineChart(); + JPanel legendPanel = new JPanel(); + xLabel = new JLabel(); + legend1Label = new JLabel(); + JLabel yLabel = new JLabel(); + yValueLabel = new JLabel(); + JLabel yLabel2 = new JLabel(); + JPanel hSpacer1 = new JPanel(null); + legend2Label = new JLabel(); + JLabel oneSecondWidthLabel = new JLabel(); + oneSecondWidthSlider = new JSlider(); + updateChartDelayedCheckBox = new JCheckBox(); + clearChartButton = new JButton(); + + //======== this ======== + setLayout(new MigLayout( + "hidemode 3", + // columns + "[grow,fill]", + // rows + "[100:300,grow,fill]" + + "[]")); + + //======== lineChartScrollPane ======== + { + lineChartScrollPane.setViewportView(lineChart); + } + add(lineChartScrollPane, "cell 0 0"); + + //======== legendPanel ======== + { + legendPanel.setLayout(new MigLayout( + "insets 0,hidemode 3,gapy 0", + // columns + "[fill]para" + + "[fill]", + // rows + "[]" + + "[]")); + + //---- xLabel ---- + xLabel.setText("X: time ({0}ms per line)"); + legendPanel.add(xLabel, "cell 0 0"); + legendPanel.add(legend1Label, "cell 1 0"); + + //---- yLabel ---- + yLabel.setText("Y: "); + legendPanel.add(yLabel, "cell 0 1,gapx 0 0"); + + //---- yValueLabel ---- + yValueLabel.setText("value"); + legendPanel.add(yValueLabel, "cell 0 1,gapx 0 0"); + + //---- yLabel2 ---- + yLabel2.setText(" (10% per line)"); + legendPanel.add(yLabel2, "cell 0 1,gapx 0 0"); + legendPanel.add(hSpacer1, "cell 0 1,growx"); + legendPanel.add(legend2Label, "cell 1 1"); + } + add(legendPanel, "cell 0 1"); + + //---- oneSecondWidthLabel ---- + oneSecondWidthLabel.setText("Scale X:"); + oneSecondWidthLabel.setDisplayedMnemonic('A'); + oneSecondWidthLabel.setLabelFor(oneSecondWidthSlider); + add(oneSecondWidthLabel, "cell 0 1,alignx right,growx 0"); + + //---- oneSecondWidthSlider ---- + oneSecondWidthSlider.setMinimum(100); + oneSecondWidthSlider.setMaximum(10000); + oneSecondWidthSlider.setSnapToTicks(true); + oneSecondWidthSlider.setMajorTickSpacing(100); + oneSecondWidthSlider.setValue(500); + oneSecondWidthSlider.addChangeListener(e -> oneSecondWidthChanged()); + add(oneSecondWidthSlider, "cell 0 1,alignx right,growx 0"); + + //---- updateChartDelayedCheckBox ---- + updateChartDelayedCheckBox.setText("Update chart delayed"); + updateChartDelayedCheckBox.setMnemonic('P'); + updateChartDelayedCheckBox.addActionListener(e -> updateChartDelayedChanged()); + add(updateChartDelayedCheckBox, "cell 0 1,alignx right,growx 0"); + + //---- clearChartButton ---- + clearChartButton.setText("Clear Chart"); + clearChartButton.setMnemonic('C'); + clearChartButton.addActionListener(e -> clearChart()); + add(clearChartButton, "cell 0 1,alignx right,growx 0"); + // JFormDesigner - End of component initialization //GEN-END:initComponents @formatter:on + } + + // JFormDesigner - Variables declaration - DO NOT MODIFY //GEN-BEGIN:variables @formatter:off + private JScrollPane lineChartScrollPane; + private LineChartPanel.LineChart lineChart; + private JLabel xLabel; + private JLabel legend1Label; + private JLabel yValueLabel; + private JLabel legend2Label; + private JSlider oneSecondWidthSlider; + private JCheckBox updateChartDelayedCheckBox; + private JButton clearChartButton; + // JFormDesigner - End of variables declaration //GEN-END:variables @formatter:on + + + //---- class LineChart ---------------------------------------------------- + + private static class LineChart + extends JComponent + implements Scrollable + { + private static final int UPDATE_DELAY_MS = 20; + private static final int NEW_SEQUENCE_TIME_LAG_MS = 500; + private static final int NEW_SEQUENCE_GAP_MS = 100; + private static final int HIT_OFFSET = 4; + + private static final boolean TEST = false; + + // asynchron means that chart for each color starts at x=0 + private boolean asynchron; + private boolean temporaryValueDetection; + private boolean yZeroAtTop; + private int oneSecondWidth = 500; + private int msPerLineX = 100; + private final HashMap methodHighlightMap = new HashMap<>(); + + private static class Data { + final double value; + final int ivalue; + final Color chartColor; + final Color dotColor; + final boolean dotOnly; + final long time; // in milliseconds + final String name; + final Exception stack; + + Data( double value, int ivalue, Color chartColor, Color dotColor, + boolean dotOnly, long time, String name, Exception stack ) + { + this.value = value; + this.ivalue = ivalue; + this.chartColor = chartColor; + this.dotColor = dotColor; + this.dotOnly = dotOnly; + this.time = time; + this.name = name; + this.stack = stack; + } + + @Override + public String toString() { + // for debugging + return "value=" + value + ", ivalue=" + ivalue + ", dotColor=" + dotColor + + ", dotOnly=" + dotOnly + ", time=" + time + ", name=" + name; + } + } + + private final List syncChartData = new ArrayList<>(); + private final Map> asyncColor2dataMap = new HashMap<>(); + private final Timer repaintTime; + private Color lastUsedChartColor; + private boolean updateDelayed; + + private final List lastPoints = new ArrayList<>(); + private final List lastDatas = new ArrayList<>(); + private double lastSystemScaleFactor = 1; + private String lastToolTipPrinted; + + LineChart() { + repaintTime = new Timer( UPDATE_DELAY_MS, e -> repaintAndRevalidate() ); + repaintTime.setRepeats( false ); + + ToolTipManager.sharedInstance().registerComponent( this ); + + if( TEST ) + initTestData(); + } + + void addValue( Color chartColor, double value, int ivalue, Color dotColor, boolean dotOnly, String name ) { + if( TEST ) + return; + + List chartData = asyncColor2dataMap.computeIfAbsent( chartColor, k -> new ArrayList<>() ); + Data data = new Data( value, ivalue, chartColor, dotColor, dotOnly, System.nanoTime() / 1_000_000, name, new Exception() ); + if( asynchron ) + chartData.add( data ); + else + syncChartData.add( data ); + + lastUsedChartColor = chartColor; + + if( updateDelayed ) { + repaintTime.stop(); + repaintTime.start(); + } else + repaintAndRevalidate(); + } + + void clear() { + if( TEST ) { + repaint(); + return; + } + + syncChartData.clear(); + asyncColor2dataMap.clear(); + lastUsedChartColor = null; + + repaint(); + revalidate(); + } + + private void repaintAndRevalidate() { + repaint(); + revalidate(); + + // scroll horizontally + if( lastUsedChartColor != null ) { + // compute chart width of last used color and start of last sequence + int[] lastSeqX = new int[1]; + int cw = chartWidth( asynchron ? asyncColor2dataMap.get( lastUsedChartColor ) : syncChartData, lastSeqX ); + + // scroll to end of last sequence (of last used color) + int lastSeqWidth = cw - lastSeqX[0]; + int width = Math.min( lastSeqWidth, getParent().getWidth() ); + int x = cw - width; + scrollRectToVisible( new Rectangle( x, 0, width, getHeight() ) ); + } + } + + @Override + protected void paintComponent( Graphics g ) { + Graphics g2 = g.create(); + try { + HiDPIUtils.paintAtScale1x( (Graphics2D) g2, this, this::paintAt1x ); + } finally { + g2.dispose(); + } + } + + private void paintAt1x( Graphics2D g, int x, int y, int width, int height, double scaleFactor ) { + FlatUIUtils.setRenderingHints( g ); + + int oneSecondWidth = (int) (UIScale.scale( this.oneSecondWidth ) * scaleFactor); + int seqGapWidth = (oneSecondWidth * NEW_SEQUENCE_GAP_MS) / 1000; + int hitOffset = (int) Math.round( UIScale.scale( HIT_OFFSET ) * scaleFactor ); + + Color lineColor = FlatUIUtils.getUIColor( "Component.borderColor", Color.lightGray ); + Color lineColor2 = FlatLaf.isLafDark() + ? new HSLColor( lineColor ).adjustTone( 30 ) + : new HSLColor( lineColor ).adjustShade( 30 ); + + g.translate( x, y ); + + // fill background + g.setColor( UIManager.getColor( "Table.background" ) ); + g.fillRect( x, y, width, height ); + + // paint horizontal lines + for( int i = 1; i < 10; i++ ) { + int hy = (height * i) / 10; + g.setColor( (i != 5) ? lineColor : lineColor2 ); + g.drawLine( 0, hy, width, hy ); + } + + // paint vertical lines + int perLineXWidth = Math.round( (oneSecondWidth / 1000f) * msPerLineX ); + for( int i = 1, xv = perLineXWidth; xv < width; xv += perLineXWidth, i++ ) { + g.setColor( (i % 5 != 0) ? lineColor : lineColor2 ); + g.drawLine( xv, 0, xv, height ); + } + + lastPoints.clear(); + lastDatas.clear(); + lastSystemScaleFactor = scaleFactor; + + // paint lines + for( Map.Entry> e : asyncColor2dataMap.entrySet() ) { + List chartData = asynchron ? e.getValue() : syncChartData; + Color chartColor = e.getKey(); + if( FlatLaf.isLafDark() ) + chartColor = new HSLColor( chartColor ).adjustTone( 50 ); + Color temporaryValueColor = fade( chartColor, FlatLaf.isLafDark() ? 0.7f : 0.3f ); + Color dataPointColor = fade( chartColor, FlatLaf.isLafDark() ? 0.6f : 0.2f ); + + // sequence start time and x coordinate + long seqStartTime = 0; + int seqStartX = 0; + + // "previous" data point time and x coordinate (used for "new sequence" detection) + long ptime = Long.MIN_VALUE; + int px = 0; + + // "line" data point x/y coordinates + int lx = -1; + int ly = -1; + + boolean isTemporaryValue = false; + int lastTemporaryValueIndex = -1; + + int size = chartData.size(); + for( int i = 0; i < size; i++ ) { + Data data = chartData.get( i ); + boolean useData = (data.chartColor == chartColor); + + // start new sequence if there is a larger time gap to previous data point + boolean newSeq = (data.time > ptime + NEW_SEQUENCE_TIME_LAG_MS); + ptime = data.time; + + if( newSeq ) { + // start new sequence + seqStartTime = data.time; + seqStartX = (i > 0) ? px + seqGapWidth : 0; + px = seqStartX; + lx = -1; + ly = -1; + isTemporaryValue = false; + } + + // x/y coordinates of current data point + int dx = (int) (seqStartX + (((data.time - seqStartTime) / 1000.) * oneSecondWidth)); + int dy = (int) ((height - 1) * data.value); + if( !yZeroAtTop ) + dy = height - 1 - dy; + + // remember x coordinate for "new sequence" detection + px = dx; + + if( !useData ) + continue; + + // remember data point for tooltip + lastPoints.add( new Point( dx, dy ) ); + lastDatas.add( data ); + + // paint rectangle to indicate data point + g.setColor( dataPointColor ); + g.drawRect( dx - hitOffset, dy - hitOffset, hitOffset * 2, hitOffset * 2 ); + + // paint dot + if( data.dotColor != null ) { + int s1 = (int) Math.round( UIScale.scale( 1 ) * scaleFactor ); + int s3 = (int) Math.round( UIScale.scale( 3 ) * scaleFactor ); + g.setColor( data.dotColor ); + g.fillRect( dx - s1, dy - s1, s3, s3 ); + + if( data.dotOnly ) + continue; + } + + // start of line? + if( lx < 0 ) { + // remember x/y coordinates for first line + lx = dx; + ly = dy; + continue; + } + + if( isTemporaryValue && i > lastTemporaryValueIndex ) + isTemporaryValue = false; + + // draw line in sequence + g.setColor( isTemporaryValue ? temporaryValueColor : chartColor ); + g.drawLine( lx, ly, dx, dy ); + + // remember x/y coordinates for next line + lx = dx; + ly = dy; + + // check next data points for "temporary" value(s) + if( temporaryValueDetection && !isTemporaryValue ) { + // one or two values between two equal values are considered "temporary", + // which means that they are the target value for the following scroll animation + int stage = 0; + for( int j = i + 1; j < size && stage <= 2 && !isTemporaryValue; j++ ) { + Data nextData = chartData.get( j ); + if( nextData.dotOnly ) + continue; // ignore dots + + // check whether next data point is within 10 milliseconds + if( nextData.time > data.time + 10 ) + break; + + if( stage >= 1 && stage <= 2 && nextData.value == data.value ) { + isTemporaryValue = true; + lastTemporaryValueIndex = j; + } + stage++; + } + } + } + } + } + + private int chartWidth() { + int width = 0; + if( asynchron ) { + for( List chartData : asyncColor2dataMap.values() ) + width = Math.max( width, chartWidth( chartData, null ) ); + } else + width = Math.max( width, chartWidth( syncChartData, null ) ); + return width; + } + + private int chartWidth( List chartData, int[] lastSeqX ) { + long seqTime = 0; + int seqX = 0; + long ptime = 0; + int px = 0; + int oneSecondWidth = UIScale.scale( this.oneSecondWidth ); + int seqGapWidth = (oneSecondWidth * NEW_SEQUENCE_GAP_MS) / 1000; + + int size = chartData.size(); + for( int i = 0; i < size; i++ ) { + Data data = chartData.get( i ); + + if( data.time > ptime + NEW_SEQUENCE_TIME_LAG_MS ) { + // start new sequence + seqTime = data.time; + seqX = (i > 0) ? px + seqGapWidth : 0; + px = seqX; + } else { + // line in sequence + int dx = (int) (seqX + (((data.time - seqTime) / 1000.) * oneSecondWidth )); + px = dx; + } + + ptime = data.time; + } + + if( lastSeqX != null ) + lastSeqX[0] = seqX; + + return px; + } + + @Override + public Dimension getPreferredSize() { + return new Dimension( chartWidth(), 200 ); + } + + @Override + public Dimension getPreferredScrollableViewportSize() { + return new Dimension( chartWidth(), 200 ); + } + + @Override + public int getScrollableUnitIncrement( Rectangle visibleRect, int orientation, int direction ) { + return UIScale.scale( oneSecondWidth ); + } + + @Override + public int getScrollableBlockIncrement( Rectangle visibleRect, int orientation, int direction ) { + JViewport viewport = (JViewport) SwingUtilities.getAncestorOfClass( JViewport.class, this ); + return (viewport != null) ? viewport.getWidth() : 200; + } + + @Override + public boolean getScrollableTracksViewportWidth() { + JViewport viewport = (JViewport) SwingUtilities.getAncestorOfClass( JViewport.class, this ); + return (viewport != null) ? viewport.getWidth() > chartWidth() : true; + } + + @Override + public boolean getScrollableTracksViewportHeight() { + return true; + } + + @Override + public String getToolTipText( MouseEvent e ) { + int x = (int) Math.round( e.getX() * lastSystemScaleFactor ); + int y = (int) Math.round( e.getY() * lastSystemScaleFactor ); + int hitOffset = (int) Math.round( UIScale.scale( HIT_OFFSET ) * lastSystemScaleFactor ); + StringBuilder buf = null; + + int pointsCount = lastPoints.size(); + for( int i = 0; i < pointsCount; i++ ) { + Point pt = lastPoints.get( i ); + + // check X/Y coordinates + if( x < pt.x - hitOffset || x > pt.x + hitOffset || + y < pt.y - hitOffset || y > pt.y + hitOffset ) + continue; + + if( buf == null ) { + buf = new StringBuilder( 5000 ); + buf.append( "" ); + } + + Data data = lastDatas.get( i ); + buf.append( "

" ); + if( data.dotOnly ) + buf.append( "DOT: " ); + buf.append( data.name ); + if( data.ivalue != Integer.MIN_VALUE ) + buf.append( ' ' ).append( data.ivalue ); + buf.append( " (" ).append( String.format( "%.3f", data.value ) ).append( ')' ); + buf.append( "

" ); + + StackTraceElement[] stackTrace = data.stack.getStackTrace(); + for( int j = 0; j < stackTrace.length; j++ ) { + StackTraceElement stackElement = stackTrace[j]; + String className = stackElement.getClassName(); + String methodName = stackElement.getMethodName(); + String classAndMethod = className + '.' + methodName; + + // ignore methods from this class + if( className.startsWith( LineChartPanel.class.getName() ) ) + continue; + + int repeatCount = 0; + for( int k = j + 1; k < stackTrace.length; k++ ) { + if( !stackElement.equals( stackTrace[k] ) ) + break; + repeatCount++; + } + j += repeatCount; + + String highlight = methodHighlightMap.get( classAndMethod ); + if( highlight == null ) + highlight = methodHighlightMap.get( className ); + if( highlight == null ) + highlight = methodHighlightMap.get( methodName ); + if( highlight != null ) + buf.append( "" ); + + // append method + buf.append( className ) + .append( "." ) + .append( methodName ) + .append( "" ); + if( highlight != null ) + buf.append( "" ); + + // append source + buf.append( " " ); + if( stackElement.getFileName() != null ) { + buf.append( '(' ); + buf.append( stackElement.getFileName() ); + if( stackElement.getLineNumber() >= 0 ) + buf.append( ':' ).append( stackElement.getLineNumber() ); + buf.append( ')' ); + } else + buf.append( "(Unknown Source)" ); + buf.append( "" ); + + // append repeat count + if( repeatCount > 0 ) + buf.append( " " ).append( repeatCount + 1 ).append( "x" ); + buf.append( "
" ); + + // break at some methods to make stack smaller + if( classAndMethod.equals( "java.awt.event.InvocationEvent.dispatch" ) || + classAndMethod.equals( "java.awt.Component.processMouseEvent" ) || + classAndMethod.equals( "java.awt.Component.processMouseWheelEvent" ) || + classAndMethod.equals( "java.awt.Component.processMouseMotionEvent" ) || + classAndMethod.equals( "javax.swing.JComponent.processKeyBinding" ) || + classAndMethod.equals( "javax.swing.JComponent.paintComponent" ) || + classAndMethod.equals( "com.formdev.flatlaf.util.Animator.timingEvent" ) ) + break; + } + buf.append( "..." ); + } + + if( buf == null ) + return null; + + buf.append( "" ); + String toolTip = buf.toString(); + + // print to console + if( !Objects.equals( toolTip, lastToolTipPrinted ) ) { + lastToolTipPrinted = toolTip; + + System.out.println( toolTip + .replace( "
", "\n" ) + .replace( "

", "\n---- " ) + .replace( "

", " ----\n" ) + .replaceAll( "<[^>]+>", "" ) ); + } + + return buf.toString(); + } + + private void initTestData() { +// asynchron = true; + + addTestSimpleLine( Color.red, 0.0, "red" ); + addTestSimpleLine( Color.green, 0.1, "green" ); + addTestSimpleLine( Color.blue, 0.2, "blue" ); + addTestSimpleLine( Color.magenta, 0.3, "magenta" ); + + addTestMiddleDotOnly( Color.red, 0.0, "red" ); + addTestMiddleDotOnly( Color.green, 0.1, "green" ); + addTestMiddleDotOnly( Color.blue, 0.2, "blue" ); + addTestMiddleDotOnly( Color.magenta, 0.3, "magenta" ); + + addTestLeadingDotOnly( Color.red, 0.0, "red" ); + addTestLeadingDotOnly( Color.green, 0.1, "green" ); + addTestLeadingDotOnly( Color.blue, 0.2, "blue" ); + addTestLeadingDotOnly( Color.magenta, 0.3, "magenta" ); + + addTestTrailingDotOnly( Color.red, 0.0, "red" ); + addTestTrailingDotOnly( Color.green, 0.1, "green" ); + addTestTrailingDotOnly( Color.blue, 0.2, "blue" ); + addTestTrailingDotOnly( Color.magenta, 0.3, "magenta" ); + + addTestSingleData( Color.red, 0.0, "red" ); + addTestSingleData( Color.green, 0.1, "green" ); + addTestSingleData( Color.blue, 0.2, "blue" ); + addTestSingleData( Color.magenta, 0.3, "magenta" ); + + temporaryValueDetection = true; + addTestWithTemporaryValues( Color.red, 0.0, "red" ); + addTestWithTemporaryValues( Color.green, 0.1, "green" ); + addTestWithTemporaryValues( Color.blue, 0.2, "blue" ); + addTestWithTemporaryValues( Color.magenta, 0.3, "magenta" ); + } + + private void addTestSimpleLine( Color chartColor, double baseValue, String name ) { + addTestValue( 0, chartColor, baseValue + 0.0, null, false, name ); + addTestValue( 50, chartColor, baseValue + 0.1, null, false, name ); + addTestValue( 50, chartColor, baseValue + 0.4, null, false, name ); + testTime += 1000; + } + + private void addTestMiddleDotOnly( Color chartColor, double baseValue, String name ) { + addTestValue( 0, chartColor, baseValue + 0.0, null, false, name ); + addTestValue( 20, chartColor, baseValue + 0.3, chartColor, true, name ); + addTestValue( 30, chartColor, baseValue + 0.1, null, false, name ); + addTestValue( 20, chartColor, baseValue + 0.05, chartColor, true, name ); + addTestValue( 30, chartColor, baseValue + 0.4, null, false, name ); + testTime += 1000; + } + + private void addTestLeadingDotOnly( Color chartColor, double baseValue, String name ) { + addTestValue( 0, chartColor, baseValue + 0.05, chartColor, true, name ); + addTestValue( 20, chartColor, baseValue + 0.0, null, false, name ); + addTestValue( 50, chartColor, baseValue + 0.1, null, false, name ); + addTestValue( 30, chartColor, baseValue + 0.4, null, false, name ); + testTime += 1000; + } + + private void addTestTrailingDotOnly( Color chartColor, double baseValue, String name ) { + addTestValue( 0, chartColor, baseValue + 0.0, null, false, name ); + addTestValue( 50, chartColor, baseValue + 0.1, null, false, name ); + addTestValue( 30, chartColor, baseValue + 0.4, null, false, name ); + addTestValue( 20, chartColor, baseValue + 0.05, chartColor, true, name ); + testTime += 1000; + } + + private void addTestSingleData( Color chartColor, double baseValue, String name ) { + addTestValue( 0, chartColor, baseValue + 0.15, chartColor, false, name ); + testTime += 1000; + } + + private void addTestWithTemporaryValues( Color chartColor, double baseValue, String name ) { + addTestValue( 0, chartColor, baseValue + 0.0, null, false, name ); + addTestValue( 50, chartColor, baseValue + 0.1, null, false, name ); + addTestValue( 5, chartColor, baseValue + 0.4, null, false, name ); + addTestValue( 5, chartColor, baseValue + 0.1, null, false, name ); + addTestValue( 40, chartColor, baseValue + 0.3, null, false, name ); + testTime += 1000; + } + + private void addTestValue( int timeDelta, Color chartColor, double value, Color dotColor, boolean dotOnly, String name ) { + testTime += timeDelta; + + List chartData = asyncColor2dataMap.computeIfAbsent( chartColor, k -> new ArrayList<>() ); + Data data = new Data( value, testIValue++, chartColor, dotColor, dotOnly, testTime, name, new Exception() ); + if( asynchron ) + chartData.add( data ); + else + syncChartData.add( data ); + + lastUsedChartColor = chartColor; + } + + private int testIValue; + private long testTime; + + //TODO remove and use ColorFunctions.fade() when merging to main + private static Color fade( Color color, float amount ) { + int newAlpha = Math.round( 255 * amount ); + return new Color( (color.getRGB() & 0xffffff) | (newAlpha << 24), true ); + } + } +} diff --git a/flatlaf-testing/src/main/java/com/formdev/flatlaf/testing/LineChartPanel.jfd b/flatlaf-testing/src/main/java/com/formdev/flatlaf/testing/LineChartPanel.jfd new file mode 100644 index 000000000..2f66cb4a2 --- /dev/null +++ b/flatlaf-testing/src/main/java/com/formdev/flatlaf/testing/LineChartPanel.jfd @@ -0,0 +1,123 @@ +JFDML JFormDesigner: "8.3" encoding: "UTF-8" + +new FormModel { + contentType: "form/swing" + root: new FormRoot { + add( new FormContainer( "javax.swing.JPanel", new FormLayoutManager( class net.miginfocom.swing.MigLayout ) { + "$layoutConstraints": "hidemode 3" + "$columnConstraints": "[grow,fill]" + "$rowConstraints": "[100:300,grow,fill][]" + } ) { + name: "this" + add( new FormContainer( "javax.swing.JScrollPane", new FormLayoutManager( class javax.swing.JScrollPane ) ) { + name: "lineChartScrollPane" + add( new FormComponent( "com.formdev.flatlaf.testing.LineChartPanel$LineChart" ) { + name: "lineChart" + } ) + }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { + "value": "cell 0 0" + } ) + add( new FormContainer( "javax.swing.JPanel", new FormLayoutManager( class net.miginfocom.swing.MigLayout ) { + "$layoutConstraints": "insets 0,hidemode 3,gapy 0" + "$columnConstraints": "[fill]para[fill]" + "$rowConstraints": "[][]" + } ) { + name: "legendPanel" + auxiliary() { + "JavaCodeGenerator.variableLocal": true + } + add( new FormComponent( "javax.swing.JLabel" ) { + name: "xLabel" + "text": "X: time ({0}ms per line)" + }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { + "value": "cell 0 0" + } ) + add( new FormComponent( "javax.swing.JLabel" ) { + name: "legend1Label" + }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { + "value": "cell 1 0" + } ) + add( new FormComponent( "javax.swing.JLabel" ) { + name: "yLabel" + "text": "Y: " + auxiliary() { + "JavaCodeGenerator.variableLocal": true + } + }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { + "value": "cell 0 1,gapx 0 0" + } ) + add( new FormComponent( "javax.swing.JLabel" ) { + name: "yValueLabel" + "text": "value" + }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { + "value": "cell 0 1,gapx 0 0" + } ) + add( new FormComponent( "javax.swing.JLabel" ) { + name: "yLabel2" + "text": " (10% per line)" + auxiliary() { + "JavaCodeGenerator.variableLocal": true + } + }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { + "value": "cell 0 1,gapx 0 0" + } ) + add( new FormComponent( "com.jformdesigner.designer.wrapper.HSpacer" ) { + name: "hSpacer1" + auxiliary() { + "JavaCodeGenerator.variableLocal": true + } + }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { + "value": "cell 0 1,growx" + } ) + add( new FormComponent( "javax.swing.JLabel" ) { + name: "legend2Label" + }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { + "value": "cell 1 1" + } ) + }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { + "value": "cell 0 1" + } ) + add( new FormComponent( "javax.swing.JLabel" ) { + name: "oneSecondWidthLabel" + "text": "Scale X:" + "displayedMnemonic": 65 + "labelFor": new FormReference( "oneSecondWidthSlider" ) + auxiliary() { + "JavaCodeGenerator.variableLocal": true + } + }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { + "value": "cell 0 1,alignx right,growx 0" + } ) + add( new FormComponent( "javax.swing.JSlider" ) { + name: "oneSecondWidthSlider" + "minimum": 100 + "maximum": 10000 + "snapToTicks": true + "majorTickSpacing": 100 + "value": 500 + addEvent( new FormEvent( "javax.swing.event.ChangeListener", "stateChanged", "oneSecondWidthChanged", false ) ) + }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { + "value": "cell 0 1,alignx right,growx 0" + } ) + add( new FormComponent( "javax.swing.JCheckBox" ) { + name: "updateChartDelayedCheckBox" + "text": "Update chart delayed" + "mnemonic": 80 + addEvent( new FormEvent( "java.awt.event.ActionListener", "actionPerformed", "updateChartDelayedChanged", false ) ) + }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { + "value": "cell 0 1,alignx right,growx 0" + } ) + add( new FormComponent( "javax.swing.JButton" ) { + name: "clearChartButton" + "text": "Clear Chart" + "mnemonic": 67 + addEvent( new FormEvent( "java.awt.event.ActionListener", "actionPerformed", "clearChart", false ) ) + }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { + "value": "cell 0 1,alignx right,growx 0" + } ) + }, new FormLayoutConstraints( null ) { + "location": new java.awt.Point( 0, 0 ) + "size": new java.awt.Dimension( 880, 300 ) + } ) + } +}