From 7f226a27422010cf4a87cba273b97df2cb405674 Mon Sep 17 00:00:00 2001 From: Karl Tauber Date: Sat, 25 Jul 2020 10:58:18 +0200 Subject: [PATCH 01/30] ScrollBar: use smooth scrolling when clicking on track or on arrow button (issue #50) --- .../formdev/flatlaf/ui/FlatScrollBarUI.java | 119 ++++++++++++++++++ 1 file changed, 119 insertions(+) diff --git a/flatlaf-core/src/main/java/com/formdev/flatlaf/ui/FlatScrollBarUI.java b/flatlaf-core/src/main/java/com/formdev/flatlaf/ui/FlatScrollBarUI.java index cb1b7c6c1..ae45fc188 100644 --- a/flatlaf-core/src/main/java/com/formdev/flatlaf/ui/FlatScrollBarUI.java +++ b/flatlaf-core/src/main/java/com/formdev/flatlaf/ui/FlatScrollBarUI.java @@ -28,6 +28,8 @@ import java.lang.invoke.MethodHandles; import java.util.Map; import java.util.Objects; +import javax.swing.BoundedRangeModel; +import javax.swing.DefaultBoundedRangeModel; import javax.swing.InputMap; import javax.swing.JButton; import javax.swing.JComponent; @@ -35,6 +37,7 @@ import javax.swing.JScrollPane; import javax.swing.SwingUtilities; import javax.swing.UIManager; +import javax.swing.event.ChangeListener; import javax.swing.plaf.ComponentUI; import javax.swing.plaf.basic.BasicScrollBarUI; import com.formdev.flatlaf.FlatClientProperties; @@ -43,6 +46,8 @@ import com.formdev.flatlaf.ui.FlatStylingSupport.StyleableField; import com.formdev.flatlaf.ui.FlatStylingSupport.StyleableLookupProvider; import com.formdev.flatlaf.ui.FlatStylingSupport.StyleableUI; +import com.formdev.flatlaf.util.Animator; +import com.formdev.flatlaf.util.CubicBezierEasing; import com.formdev.flatlaf.util.LoggingFacade; import com.formdev.flatlaf.util.SystemInfo; import com.formdev.flatlaf.util.UIScale; @@ -431,6 +436,120 @@ public boolean getSupportsAbsolutePositioning() { return allowsAbsolutePositioning; } + @Override + protected void scrollByBlock( int direction ) { + runAndSetValueAnimated( () -> { + super.scrollByBlock( direction ); + } ); + } + + @Override + protected void scrollByUnit( int direction ) { + runAndSetValueAnimated( () -> { + super.scrollByUnit( direction ); + } ); + } + + protected void runAndSetValueAnimated( Runnable r ) { + if( !isSmoothScrollingEnabled() ) { + r.run(); + return; + } + + if( animator != null ) + animator.cancel(); + + int[] newValue = new int[1]; + + runWithoutValueChangeEvents( scrollbar, () -> { + // if invoked while animation is running, calculation of new value + // should start at the previous target value + if( targetValue != Integer.MIN_VALUE ) + scrollbar.setValue( targetValue ); + + int oldValue = scrollbar.getValue(); + + r.run(); + + newValue[0] = scrollbar.getValue(); + scrollbar.setValue( oldValue ); + } ); + + setValueAnimated( newValue[0] ); + } + + private Animator animator; + private int targetValue = Integer.MIN_VALUE; + private int delta; + + protected void setValueAnimated( int value ) { + // create animator + if( animator == null ) { + int duration = FlatUIUtils.getUIInt( "ScrollPane.smoothScrolling.duration", 200 ); + int resolution = FlatUIUtils.getUIInt( "ScrollPane.smoothScrolling.resolution", 10 ); + Object interpolator = UIManager.get( "ScrollPane.smoothScrolling.interpolator" ); + + animator = new Animator( duration, fraction -> { + if( !scrollbar.isShowing() ) { + animator.cancel(); + return; + } + + scrollbar.setValue( targetValue - delta + Math.round( delta * fraction ) ); + }, () -> { + targetValue = Integer.MIN_VALUE; + }); + + animator.setResolution( resolution ); + animator.setInterpolator( (interpolator instanceof Animator.Interpolator) + ? (Animator.Interpolator) interpolator + : new CubicBezierEasing( 0.5f, 0.5f, 0.5f, 1 ) ); + } + + targetValue = value; + delta = targetValue - scrollbar.getValue(); + + animator.cancel(); + animator.start(); + } + + protected boolean isSmoothScrollingEnabled() { + // if scroll bar is child of scroll pane, check only client property of scroll pane + Container parent = scrollbar.getParent(); + JComponent c = (parent instanceof JScrollPane) ? (JScrollPane) parent : scrollbar; + Object smoothScrolling = c.getClientProperty( FlatClientProperties.SCROLL_PANE_SMOOTH_SCROLLING ); + if( smoothScrolling instanceof Boolean ) + return (Boolean) smoothScrolling; + + // Note: Getting UI value "ScrollPane.smoothScrolling" here to allow + // applications to turn smooth scrolling on or off at any time + // (e.g. in application options dialog). + return UIManager.getBoolean( "ScrollPane.smoothScrolling" ); + } + + protected static void runWithoutValueChangeEvents( JScrollBar scrollBar, Runnable r ) { + BoundedRangeModel model = scrollBar.getModel(); + if( !(model instanceof DefaultBoundedRangeModel) ) { + r.run(); + return; + } + + DefaultBoundedRangeModel m = (DefaultBoundedRangeModel) model; + + // remove all listeners + ChangeListener[] changeListeners = m.getChangeListeners(); + for( ChangeListener l : changeListeners ) + m.removeChangeListener( l ); + + try { + r.run(); + } finally { + // add all listeners + for( ChangeListener l : changeListeners ) + m.addChangeListener( l ); + } + } + //---- class ScrollBarHoverListener --------------------------------------- // using static field to disabling hover for other scroll bars From b67b701d1ec3bff0f48a27bd3aeba33615da4e83 Mon Sep 17 00:00:00 2001 From: Karl Tauber Date: Sat, 25 Jul 2020 13:11:52 +0200 Subject: [PATCH 02/30] ScrollPane: use smooth scrolling when rotating the mouse wheel (issue #50) --- .../formdev/flatlaf/ui/FlatScrollBarUI.java | 56 +++++-------------- .../formdev/flatlaf/ui/FlatScrollPaneUI.java | 44 +++++++++++---- 2 files changed, 47 insertions(+), 53 deletions(-) diff --git a/flatlaf-core/src/main/java/com/formdev/flatlaf/ui/FlatScrollBarUI.java b/flatlaf-core/src/main/java/com/formdev/flatlaf/ui/FlatScrollBarUI.java index ae45fc188..301f413dc 100644 --- a/flatlaf-core/src/main/java/com/formdev/flatlaf/ui/FlatScrollBarUI.java +++ b/flatlaf-core/src/main/java/com/formdev/flatlaf/ui/FlatScrollBarUI.java @@ -28,8 +28,6 @@ import java.lang.invoke.MethodHandles; import java.util.Map; import java.util.Objects; -import javax.swing.BoundedRangeModel; -import javax.swing.DefaultBoundedRangeModel; import javax.swing.InputMap; import javax.swing.JButton; import javax.swing.JComponent; @@ -37,7 +35,6 @@ import javax.swing.JScrollPane; import javax.swing.SwingUtilities; import javax.swing.UIManager; -import javax.swing.event.ChangeListener; import javax.swing.plaf.ComponentUI; import javax.swing.plaf.basic.BasicScrollBarUI; import com.formdev.flatlaf.FlatClientProperties; @@ -450,7 +447,11 @@ protected void scrollByUnit( int direction ) { } ); } - protected void runAndSetValueAnimated( Runnable r ) { + /** + * Runs the given runnable, which should modify the scroll bar value, + * and then animate scroll bar value from old value to new value. + */ + public void runAndSetValueAnimated( Runnable r ) { if( !isSmoothScrollingEnabled() ) { r.run(); return; @@ -459,30 +460,26 @@ protected void runAndSetValueAnimated( Runnable r ) { if( animator != null ) animator.cancel(); - int[] newValue = new int[1]; + // if invoked while animation is running, calculation of new value + // should start at the previous target value + if( targetValue != Integer.MIN_VALUE ) + scrollbar.setValue( targetValue ); - runWithoutValueChangeEvents( scrollbar, () -> { - // if invoked while animation is running, calculation of new value - // should start at the previous target value - if( targetValue != Integer.MIN_VALUE ) - scrollbar.setValue( targetValue ); + int oldValue = scrollbar.getValue(); - int oldValue = scrollbar.getValue(); + r.run(); - r.run(); - - newValue[0] = scrollbar.getValue(); - scrollbar.setValue( oldValue ); - } ); + int newValue = scrollbar.getValue(); + scrollbar.setValue( oldValue ); - setValueAnimated( newValue[0] ); + setValueAnimated( newValue ); } private Animator animator; private int targetValue = Integer.MIN_VALUE; private int delta; - protected void setValueAnimated( int value ) { + public void setValueAnimated( int value ) { // create animator if( animator == null ) { int duration = FlatUIUtils.getUIInt( "ScrollPane.smoothScrolling.duration", 200 ); @@ -527,29 +524,6 @@ protected boolean isSmoothScrollingEnabled() { return UIManager.getBoolean( "ScrollPane.smoothScrolling" ); } - protected static void runWithoutValueChangeEvents( JScrollBar scrollBar, Runnable r ) { - BoundedRangeModel model = scrollBar.getModel(); - if( !(model instanceof DefaultBoundedRangeModel) ) { - r.run(); - return; - } - - DefaultBoundedRangeModel m = (DefaultBoundedRangeModel) model; - - // remove all listeners - ChangeListener[] changeListeners = m.getChangeListeners(); - for( ChangeListener l : changeListeners ) - m.removeChangeListener( l ); - - try { - r.run(); - } finally { - // add all listeners - for( ChangeListener l : changeListeners ) - m.addChangeListener( l ); - } - } - //---- class ScrollBarHoverListener --------------------------------------- // using static field to disabling hover for other scroll bars diff --git a/flatlaf-core/src/main/java/com/formdev/flatlaf/ui/FlatScrollPaneUI.java b/flatlaf-core/src/main/java/com/formdev/flatlaf/ui/FlatScrollPaneUI.java index a62e83698..551faaad4 100644 --- a/flatlaf-core/src/main/java/com/formdev/flatlaf/ui/FlatScrollPaneUI.java +++ b/flatlaf-core/src/main/java/com/formdev/flatlaf/ui/FlatScrollPaneUI.java @@ -135,12 +135,25 @@ protected MouseWheelListener createMouseWheelListener() { MouseWheelListener superListener = super.createMouseWheelListener(); return e -> { if( isSmoothScrollingEnabled() && - scrollpane.isWheelScrollingEnabled() && - e.getScrollType() == MouseWheelEvent.WHEEL_UNIT_SCROLL && - e.getPreciseWheelRotation() != 0 && - e.getPreciseWheelRotation() != e.getWheelRotation() ) + scrollpane.isWheelScrollingEnabled() ) { - mouseWheelMovedSmooth( e ); + if( e.getScrollType() == MouseWheelEvent.WHEEL_UNIT_SCROLL && + e.getPreciseWheelRotation() != 0 && + e.getPreciseWheelRotation() != e.getWheelRotation() ) + { + // precise scrolling + mouseWheelMovedPrecise( e ); + } else { + // smooth scrolling + JScrollBar scrollBar = findScrollBarToScroll( e ); + if( scrollBar != null && scrollBar.getUI() instanceof FlatScrollBarUI ) { + FlatScrollBarUI ui = (FlatScrollBarUI) scrollBar.getUI(); + ui.runAndSetValueAnimated( () -> { + superListener.mouseWheelMoved( e ); + } ); + } else + superListener.mouseWheelMoved( e ); + } } else superListener.mouseWheelMoved( e ); }; @@ -157,19 +170,16 @@ protected boolean isSmoothScrollingEnabled() { return UIManager.getBoolean( "ScrollPane.smoothScrolling" ); } - private void mouseWheelMovedSmooth( MouseWheelEvent e ) { + private void mouseWheelMovedPrecise( MouseWheelEvent e ) { // return if there is no viewport JViewport viewport = scrollpane.getViewport(); if( viewport == null ) return; // find scrollbar to scroll - JScrollBar scrollbar = scrollpane.getVerticalScrollBar(); - if( scrollbar == null || !scrollbar.isVisible() || e.isShiftDown() ) { - scrollbar = scrollpane.getHorizontalScrollBar(); - if( scrollbar == null || !scrollbar.isVisible() ) - return; - } + JScrollBar scrollbar = findScrollBarToScroll( e ); + if( scrollbar == null ) + return; // consume event e.consume(); @@ -262,6 +272,16 @@ else if( rotation < 0 ) */ } + private JScrollBar findScrollBarToScroll( MouseWheelEvent e ) { + JScrollBar scrollBar = scrollpane.getVerticalScrollBar(); + if( scrollBar == null || !scrollBar.isVisible() || e.isShiftDown() ) { + scrollBar = scrollpane.getHorizontalScrollBar(); + if( scrollBar == null || !scrollBar.isVisible() ) + return null; + } + return scrollBar; + } + @Override protected PropertyChangeListener createPropertyChangeListener() { PropertyChangeListener superListener = super.createPropertyChangeListener(); From 82514ccbfc4d86fd1124529e3c5878b314328258 Mon Sep 17 00:00:00 2001 From: Karl Tauber Date: Sat, 25 Jul 2020 13:36:42 +0200 Subject: [PATCH 03/30] Demo: added "Options > Smooth Scrolling" to menu (issue #50) --- .../java/com/formdev/flatlaf/demo/DemoFrame.java | 12 ++++++++++++ .../main/java/com/formdev/flatlaf/demo/DemoFrame.jfd | 9 +++++++++ 2 files changed, 21 insertions(+) diff --git a/flatlaf-demo/src/main/java/com/formdev/flatlaf/demo/DemoFrame.java b/flatlaf-demo/src/main/java/com/formdev/flatlaf/demo/DemoFrame.java index bfedf7a57..5cb20c525 100644 --- a/flatlaf-demo/src/main/java/com/formdev/flatlaf/demo/DemoFrame.java +++ b/flatlaf-demo/src/main/java/com/formdev/flatlaf/demo/DemoFrame.java @@ -266,6 +266,10 @@ private void alwaysShowMnemonics() { repaint(); } + private void smoothScrollingChanged() { + UIManager.put( "ScrollPane.smoothScrolling", smoothScrollingMenuItem.isSelected() ); + } + private void animatedLafChangeChanged() { System.setProperty( "flatlaf.animatedLafChange", String.valueOf( animatedLafChangeMenuItem.isSelected() ) ); } @@ -505,6 +509,7 @@ private void initComponents() { showTitleBarIconMenuItem = new JCheckBoxMenuItem(); underlineMenuSelectionMenuItem = new JCheckBoxMenuItem(); alwaysShowMnemonicsMenuItem = new JCheckBoxMenuItem(); + smoothScrollingMenuItem = new JCheckBoxMenuItem(); animatedLafChangeMenuItem = new JCheckBoxMenuItem(); JMenuItem showHintsMenuItem = new JMenuItem(); JMenuItem showUIDefaultsInspectorMenuItem = new JMenuItem(); @@ -793,6 +798,12 @@ private void initComponents() { alwaysShowMnemonicsMenuItem.addActionListener(e -> alwaysShowMnemonics()); optionsMenu.add(alwaysShowMnemonicsMenuItem); + //---- smoothScrollingMenuItem ---- + smoothScrollingMenuItem.setText("Smooth Scrolling"); + smoothScrollingMenuItem.setSelected(true); + smoothScrollingMenuItem.addActionListener(e -> smoothScrollingChanged()); + optionsMenu.add(smoothScrollingMenuItem); + //---- animatedLafChangeMenuItem ---- animatedLafChangeMenuItem.setText("Animated Laf Change"); animatedLafChangeMenuItem.setSelected(true); @@ -981,6 +992,7 @@ private void unsupported( JCheckBoxMenuItem menuItem ) { private JCheckBoxMenuItem showTitleBarIconMenuItem; private JCheckBoxMenuItem underlineMenuSelectionMenuItem; private JCheckBoxMenuItem alwaysShowMnemonicsMenuItem; + private JCheckBoxMenuItem smoothScrollingMenuItem; private JCheckBoxMenuItem animatedLafChangeMenuItem; private JMenuItem aboutMenuItem; private JToolBar toolBar; diff --git a/flatlaf-demo/src/main/java/com/formdev/flatlaf/demo/DemoFrame.jfd b/flatlaf-demo/src/main/java/com/formdev/flatlaf/demo/DemoFrame.jfd index ba3f2d4fb..c44f5993f 100644 --- a/flatlaf-demo/src/main/java/com/formdev/flatlaf/demo/DemoFrame.jfd +++ b/flatlaf-demo/src/main/java/com/formdev/flatlaf/demo/DemoFrame.jfd @@ -418,6 +418,15 @@ new FormModel { } addEvent( new FormEvent( "java.awt.event.ActionListener", "actionPerformed", "alwaysShowMnemonics", false ) ) } ) + add( new FormComponent( "javax.swing.JCheckBoxMenuItem" ) { + name: "smoothScrollingMenuItem" + "text": "Smooth Scrolling" + "selected": true + auxiliary() { + "JavaCodeGenerator.variableLocal": false + } + addEvent( new FormEvent( "java.awt.event.ActionListener", "actionPerformed", "smoothScrollingChanged", false ) ) + } ) add( new FormComponent( "javax.swing.JCheckBoxMenuItem" ) { name: "animatedLafChangeMenuItem" "text": "Animated Laf Change" From 889b5ea56aa8cd685d491f8ae3cf1f287f710186 Mon Sep 17 00:00:00 2001 From: Karl Tauber Date: Sat, 25 Jul 2020 19:53:52 +0200 Subject: [PATCH 04/30] ScrollBar: fixed smooth scrolling issues when continuously scrolling (issue #50) --- .../formdev/flatlaf/ui/FlatScrollBarUI.java | 50 +++++++++++++++++-- 1 file changed, 47 insertions(+), 3 deletions(-) diff --git a/flatlaf-core/src/main/java/com/formdev/flatlaf/ui/FlatScrollBarUI.java b/flatlaf-core/src/main/java/com/formdev/flatlaf/ui/FlatScrollBarUI.java index 301f413dc..a0b3c47db 100644 --- a/flatlaf-core/src/main/java/com/formdev/flatlaf/ui/FlatScrollBarUI.java +++ b/flatlaf-core/src/main/java/com/formdev/flatlaf/ui/FlatScrollBarUI.java @@ -22,6 +22,7 @@ import java.awt.Graphics; import java.awt.Insets; import java.awt.Rectangle; +import java.awt.event.ActionEvent; import java.awt.event.MouseAdapter; import java.awt.event.MouseEvent; import java.beans.PropertyChangeListener; @@ -205,6 +206,16 @@ protected void uninstallDefaults() { oldStyleValues = null; } + @Override + protected TrackListener createTrackListener() { + return new FlatTrackListener(); + } + + @Override + protected ScrollListener createScrollListener() { + return new FlatScrollListener(); + } + @Override protected PropertyChangeListener createPropertyChangeListener() { PropertyChangeListener superListener = super.createPropertyChangeListener(); @@ -452,11 +463,13 @@ protected void scrollByUnit( int direction ) { * and then animate scroll bar value from old value to new value. */ public void runAndSetValueAnimated( Runnable r ) { - if( !isSmoothScrollingEnabled() ) { + if( inRunAndSetValueAnimated || !isSmoothScrollingEnabled() ) { r.run(); return; } + inRunAndSetValueAnimated = true; + if( animator != null ) animator.cancel(); @@ -470,11 +483,16 @@ public void runAndSetValueAnimated( Runnable r ) { r.run(); int newValue = scrollbar.getValue(); - scrollbar.setValue( oldValue ); + if( newValue != oldValue ) { + scrollbar.setValue( oldValue ); + + setValueAnimated( newValue ); + } - setValueAnimated( newValue ); + inRunAndSetValueAnimated = false; } + private boolean inRunAndSetValueAnimated; private Animator animator; private int targetValue = Integer.MIN_VALUE; private int delta; @@ -524,6 +542,32 @@ protected boolean isSmoothScrollingEnabled() { return UIManager.getBoolean( "ScrollPane.smoothScrolling" ); } + //---- class FlatTrackListener -------------------------------------------- + + protected class FlatTrackListener + extends TrackListener + { + @Override + public void mousePressed( MouseEvent e ) { + runAndSetValueAnimated( () -> { + super.mousePressed( e ); + } ); + } + } + + //---- class FlatScrollListener ------------------------------------------- + + protected class FlatScrollListener + extends ScrollListener + { + @Override + public void actionPerformed( ActionEvent e ) { + runAndSetValueAnimated( () -> { + super.actionPerformed( e ); + } ); + } + } + //---- class ScrollBarHoverListener --------------------------------------- // using static field to disabling hover for other scroll bars From 736305849a10009651dbdb5b11fcb64517f3d77f Mon Sep 17 00:00:00 2001 From: Karl Tauber Date: Tue, 4 Aug 2020 21:30:44 +0200 Subject: [PATCH 05/30] ScrollBar: set valueIsAdjusting property to true while smooth scrolling animation is running (issue #50) --- .../formdev/flatlaf/ui/FlatScrollBarUI.java | 13 +- .../testing/FlatSmoothScrollingTest.java | 144 ++++++++++++++++++ .../testing/FlatSmoothScrollingTest.jfd | 43 ++++++ 3 files changed, 199 insertions(+), 1 deletion(-) create mode 100644 flatlaf-testing/src/main/java/com/formdev/flatlaf/testing/FlatSmoothScrollingTest.java create mode 100644 flatlaf-testing/src/main/java/com/formdev/flatlaf/testing/FlatSmoothScrollingTest.jfd diff --git a/flatlaf-core/src/main/java/com/formdev/flatlaf/ui/FlatScrollBarUI.java b/flatlaf-core/src/main/java/com/formdev/flatlaf/ui/FlatScrollBarUI.java index a0b3c47db..732e0fa39 100644 --- a/flatlaf-core/src/main/java/com/formdev/flatlaf/ui/FlatScrollBarUI.java +++ b/flatlaf-core/src/main/java/com/formdev/flatlaf/ui/FlatScrollBarUI.java @@ -473,6 +473,8 @@ public void runAndSetValueAnimated( Runnable r ) { if( animator != null ) animator.cancel(); + scrollbar.setValueIsAdjusting( true ); + // if invoked while animation is running, calculation of new value // should start at the previous target value if( targetValue != Integer.MIN_VALUE ) @@ -487,7 +489,8 @@ public void runAndSetValueAnimated( Runnable r ) { scrollbar.setValue( oldValue ); setValueAnimated( newValue ); - } + } else + scrollbar.setValueIsAdjusting( false ); inRunAndSetValueAnimated = false; } @@ -498,6 +501,8 @@ public void runAndSetValueAnimated( Runnable r ) { private int delta; public void setValueAnimated( int value ) { + scrollbar.setValueIsAdjusting( true ); + // create animator if( animator == null ) { int duration = FlatUIUtils.getUIInt( "ScrollPane.smoothScrolling.duration", 200 ); @@ -510,9 +515,15 @@ public void setValueAnimated( int value ) { return; } + // re-enable valueIsAdjusting if disabled while animation is running + // (e.g. in mouse released listener) + if( !scrollbar.getValueIsAdjusting() ) + scrollbar.setValueIsAdjusting( true ); + scrollbar.setValue( targetValue - delta + Math.round( delta * fraction ) ); }, () -> { targetValue = Integer.MIN_VALUE; + scrollbar.setValueIsAdjusting( false ); }); animator.setResolution( resolution ); diff --git a/flatlaf-testing/src/main/java/com/formdev/flatlaf/testing/FlatSmoothScrollingTest.java b/flatlaf-testing/src/main/java/com/formdev/flatlaf/testing/FlatSmoothScrollingTest.java new file mode 100644 index 000000000..2c8e915c8 --- /dev/null +++ b/flatlaf-testing/src/main/java/com/formdev/flatlaf/testing/FlatSmoothScrollingTest.java @@ -0,0 +1,144 @@ +/* + * Copyright 2020 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.event.AdjustmentEvent; +import java.awt.event.AdjustmentListener; +import java.util.ArrayList; +import java.util.Arrays; +import javax.swing.*; +import net.miginfocom.swing.*; + +/** + * @author Karl Tauber + */ +public class FlatSmoothScrollingTest + extends FlatTestPanel +{ + public static void main( String[] args ) { + SwingUtilities.invokeLater( () -> { + FlatTestFrame frame = FlatTestFrame.create( args, "FlatSmoothScrollingTest" ); + UIManager.put( "ScrollBar.showButtons", true ); + frame.showFrame( FlatSmoothScrollingTest::new ); + } ); + } + + FlatSmoothScrollingTest() { + initComponents(); + + scrollPane1.getVerticalScrollBar().addAdjustmentListener( new AdjustmentHandler( "list vert" ) ); + scrollPane1.getHorizontalScrollBar().addAdjustmentListener( new AdjustmentHandler( "list horz" ) ); + + ArrayList items = new ArrayList<>(); + for( char ch = '0'; ch < 'z'; ch++ ) { + char[] chars = new char[ch - '0' + 1]; + Arrays.fill( chars, ch ); + items.add( new String( chars ) ); + } + + list1.setModel( new AbstractListModel() { + @Override + public int getSize() { + return items.size(); + } + + @Override + public String getElementAt( int index ) { + return items.get( index ); + } + } ); + } + + private void smoothScrollingChanged() { + UIManager.put( "ScrollPane.smoothScrolling", smoothScrollingCheckBox.isSelected() ); + } + + private void initComponents() { + // JFormDesigner - Component initialization - DO NOT MODIFY //GEN-BEGIN:initComponents + smoothScrollingCheckBox = new JCheckBox(); + listLabel = new JLabel(); + scrollPane1 = new JScrollPane(); + list1 = new JList<>(); + + //======== this ======== + setLayout(new MigLayout( + "ltr,insets dialog,hidemode 3", + // columns + "[]" + + "[200]", + // rows + "[]" + + "[::200,grow,fill]")); + + //---- smoothScrollingCheckBox ---- + smoothScrollingCheckBox.setText("Smooth scrolling"); + smoothScrollingCheckBox.setSelected(true); + smoothScrollingCheckBox.addActionListener(e -> smoothScrollingChanged()); + add(smoothScrollingCheckBox, "cell 0 0 2 1,alignx left,growx 0"); + + //---- listLabel ---- + listLabel.setText("JList:"); + add(listLabel, "cell 0 1,aligny top,growy 0"); + + //======== scrollPane1 ======== + { + scrollPane1.setViewportView(list1); + } + add(scrollPane1, "cell 1 1,growx"); + // JFormDesigner - End of component initialization //GEN-END:initComponents + } + + // JFormDesigner - Variables declaration - DO NOT MODIFY //GEN-BEGIN:variables + private JCheckBox smoothScrollingCheckBox; + private JLabel listLabel; + private JScrollPane scrollPane1; + private JList list1; + // JFormDesigner - End of variables declaration //GEN-END:variables + + //---- class AdjustmentHandler -------------------------------------------- + + private static class AdjustmentHandler + implements AdjustmentListener + { + private final String name; + private int count; + + AdjustmentHandler( String name ) { + this.name = name; + } + + @Override + public void adjustmentValueChanged( AdjustmentEvent e ) { + System.out.printf( "%s (%d): %s %3d %b%n", + name, ++count, + adjustmentType2Str( e.getAdjustmentType() ), + e.getValue(), + e.getValueIsAdjusting() ); + } + + private String adjustmentType2Str( int adjustmentType ) { + switch( adjustmentType ) { + case AdjustmentEvent.UNIT_INCREMENT: return "UNIT_INCREMENT"; + case AdjustmentEvent.UNIT_DECREMENT: return "UNIT_DECREMENT"; + case AdjustmentEvent.BLOCK_INCREMENT: return "BLOCK_INCREMENT"; + case AdjustmentEvent.BLOCK_DECREMENT: return "BLOCK_DECREMENT"; + case AdjustmentEvent.TRACK: return "TRACK"; + default: return "unknown type"; + } + } + } +} diff --git a/flatlaf-testing/src/main/java/com/formdev/flatlaf/testing/FlatSmoothScrollingTest.jfd b/flatlaf-testing/src/main/java/com/formdev/flatlaf/testing/FlatSmoothScrollingTest.jfd new file mode 100644 index 000000000..392312915 --- /dev/null +++ b/flatlaf-testing/src/main/java/com/formdev/flatlaf/testing/FlatSmoothScrollingTest.jfd @@ -0,0 +1,43 @@ +JFDML JFormDesigner: "7.0.2.0.298" Java: "14" 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": "ltr,insets dialog,hidemode 3" + "$columnConstraints": "[][200]" + "$rowConstraints": "[][::200,grow,fill]" + } ) { + name: "this" + add( new FormComponent( "javax.swing.JCheckBox" ) { + name: "smoothScrollingCheckBox" + "text": "Smooth scrolling" + "selected": true + addEvent( new FormEvent( "java.awt.event.ActionListener", "actionPerformed", "smoothScrollingChanged", false ) ) + }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { + "value": "cell 0 0 2 1,alignx left,growx 0" + } ) + add( new FormComponent( "javax.swing.JLabel" ) { + name: "listLabel" + "text": "JList:" + }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { + "value": "cell 0 1,aligny top,growy 0" + } ) + add( new FormContainer( "javax.swing.JScrollPane", new FormLayoutManager( class javax.swing.JScrollPane ) ) { + name: "scrollPane1" + add( new FormComponent( "javax.swing.JList" ) { + name: "list1" + auxiliary() { + "JavaCodeGenerator.typeParameters": "String" + "JavaCodeGenerator.variableLocal": false + } + } ) + }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { + "value": "cell 1 1,growx" + } ) + }, new FormLayoutConstraints( null ) { + "location": new java.awt.Point( 0, 0 ) + "size": new java.awt.Dimension( 790, 715 ) + } ) + } +} From fdabca99b26aee089d81654790920a2e004950ba Mon Sep 17 00:00:00 2001 From: Karl Tauber Date: Fri, 7 Aug 2020 23:10:22 +0200 Subject: [PATCH 06/30] ScrollBar: fixed NPE when switching LaF while smooth scrolling animation is running (issue #50) --- .../src/main/java/com/formdev/flatlaf/ui/FlatScrollBarUI.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/flatlaf-core/src/main/java/com/formdev/flatlaf/ui/FlatScrollBarUI.java b/flatlaf-core/src/main/java/com/formdev/flatlaf/ui/FlatScrollBarUI.java index 732e0fa39..676abfe1c 100644 --- a/flatlaf-core/src/main/java/com/formdev/flatlaf/ui/FlatScrollBarUI.java +++ b/flatlaf-core/src/main/java/com/formdev/flatlaf/ui/FlatScrollBarUI.java @@ -510,7 +510,7 @@ public void setValueAnimated( int value ) { Object interpolator = UIManager.get( "ScrollPane.smoothScrolling.interpolator" ); animator = new Animator( duration, fraction -> { - if( !scrollbar.isShowing() ) { + if( scrollbar == null || !scrollbar.isShowing() ) { animator.cancel(); return; } From 1ebfe00f3c0df762657e7aa1b5e2e4509afab7a8 Mon Sep 17 00:00:00 2001 From: Karl Tauber Date: Sat, 8 Aug 2020 16:19:36 +0200 Subject: [PATCH 07/30] added system properties "flatlaf.animation" and "flatlaf.smoothScrolling" to disable all animations or smooth scrolling via command line (without modifying the application) --- .../formdev/flatlaf/FlatSystemProperties.java | 8 ++++++++ .../formdev/flatlaf/ui/FlatScrollBarUI.java | 4 ++++ .../formdev/flatlaf/ui/FlatScrollPaneUI.java | 5 +++++ .../com/formdev/flatlaf/demo/DemoFrame.java | 18 ++++++++++++++++++ .../com/formdev/flatlaf/demo/DemoFrame.jfd | 12 ++++++++++++ .../demo/intellijthemes/IJThemesPanel.java | 4 ++++ .../flatlaf/extras/FlatAnimatedLafChange.java | 4 ++-- 7 files changed, 53 insertions(+), 2 deletions(-) diff --git a/flatlaf-core/src/main/java/com/formdev/flatlaf/FlatSystemProperties.java b/flatlaf-core/src/main/java/com/formdev/flatlaf/FlatSystemProperties.java index 5d4a76938..a9e4b8a6c 100644 --- a/flatlaf-core/src/main/java/com/formdev/flatlaf/FlatSystemProperties.java +++ b/flatlaf-core/src/main/java/com/formdev/flatlaf/FlatSystemProperties.java @@ -132,6 +132,14 @@ public interface FlatSystemProperties */ String ANIMATION = "flatlaf.animation"; + /** + * Specifies whether smooth scrolling is enabled. + *

+ * Allowed Values {@code false} and {@code true}
+ * Default {@code true} + */ + String SMOOTH_SCROLLING = "flatlaf.smoothScrolling"; + /** * Specifies whether vertical text position is corrected when UI is scaled on HiDPI screens. *

diff --git a/flatlaf-core/src/main/java/com/formdev/flatlaf/ui/FlatScrollBarUI.java b/flatlaf-core/src/main/java/com/formdev/flatlaf/ui/FlatScrollBarUI.java index 676abfe1c..9a5abc2ab 100644 --- a/flatlaf-core/src/main/java/com/formdev/flatlaf/ui/FlatScrollBarUI.java +++ b/flatlaf-core/src/main/java/com/formdev/flatlaf/ui/FlatScrollBarUI.java @@ -40,6 +40,7 @@ import javax.swing.plaf.basic.BasicScrollBarUI; import com.formdev.flatlaf.FlatClientProperties; import com.formdev.flatlaf.FlatLaf; +import com.formdev.flatlaf.FlatSystemProperties; import com.formdev.flatlaf.ui.FlatStylingSupport.Styleable; import com.formdev.flatlaf.ui.FlatStylingSupport.StyleableField; import com.formdev.flatlaf.ui.FlatStylingSupport.StyleableLookupProvider; @@ -540,6 +541,9 @@ public void setValueAnimated( int value ) { } protected boolean isSmoothScrollingEnabled() { + if( !Animator.useAnimation() || !FlatSystemProperties.getBoolean( FlatSystemProperties.SMOOTH_SCROLLING, true ) ) + return false; + // if scroll bar is child of scroll pane, check only client property of scroll pane Container parent = scrollbar.getParent(); JComponent c = (parent instanceof JScrollPane) ? (JScrollPane) parent : scrollbar; diff --git a/flatlaf-core/src/main/java/com/formdev/flatlaf/ui/FlatScrollPaneUI.java b/flatlaf-core/src/main/java/com/formdev/flatlaf/ui/FlatScrollPaneUI.java index 551faaad4..bcfb6efb9 100644 --- a/flatlaf-core/src/main/java/com/formdev/flatlaf/ui/FlatScrollPaneUI.java +++ b/flatlaf-core/src/main/java/com/formdev/flatlaf/ui/FlatScrollPaneUI.java @@ -48,8 +48,10 @@ import javax.swing.plaf.ComponentUI; import javax.swing.plaf.basic.BasicScrollPaneUI; import com.formdev.flatlaf.FlatClientProperties; +import com.formdev.flatlaf.FlatSystemProperties; import com.formdev.flatlaf.ui.FlatStylingSupport.Styleable; import com.formdev.flatlaf.ui.FlatStylingSupport.StyleableUI; +import com.formdev.flatlaf.util.Animator; import com.formdev.flatlaf.util.LoggingFacade; /** @@ -160,6 +162,9 @@ protected MouseWheelListener createMouseWheelListener() { } protected boolean isSmoothScrollingEnabled() { + if( !Animator.useAnimation() || !FlatSystemProperties.getBoolean( FlatSystemProperties.SMOOTH_SCROLLING, true ) ) + return false; + Object smoothScrolling = scrollpane.getClientProperty( FlatClientProperties.SCROLL_PANE_SMOOTH_SCROLLING ); if( smoothScrolling instanceof Boolean ) return (Boolean) smoothScrolling; diff --git a/flatlaf-demo/src/main/java/com/formdev/flatlaf/demo/DemoFrame.java b/flatlaf-demo/src/main/java/com/formdev/flatlaf/demo/DemoFrame.java index 5cb20c525..ca9c5795e 100644 --- a/flatlaf-demo/src/main/java/com/formdev/flatlaf/demo/DemoFrame.java +++ b/flatlaf-demo/src/main/java/com/formdev/flatlaf/demo/DemoFrame.java @@ -33,6 +33,7 @@ import com.formdev.flatlaf.FlatIntelliJLaf; import com.formdev.flatlaf.FlatLaf; import com.formdev.flatlaf.FlatLightLaf; +import com.formdev.flatlaf.FlatSystemProperties; import com.formdev.flatlaf.demo.HintManager.Hint; import com.formdev.flatlaf.demo.extras.*; import com.formdev.flatlaf.demo.intellijthemes.*; @@ -266,6 +267,14 @@ private void alwaysShowMnemonics() { repaint(); } + private void animationChanged() { + boolean enabled = animationMenuItem.isSelected(); + System.setProperty( FlatSystemProperties.ANIMATION, Boolean.toString( enabled ) ); + + smoothScrollingMenuItem.setEnabled( enabled ); + animatedLafChangeMenuItem.setEnabled( enabled ); + } + private void smoothScrollingChanged() { UIManager.put( "ScrollPane.smoothScrolling", smoothScrollingMenuItem.isSelected() ); } @@ -509,6 +518,7 @@ private void initComponents() { showTitleBarIconMenuItem = new JCheckBoxMenuItem(); underlineMenuSelectionMenuItem = new JCheckBoxMenuItem(); alwaysShowMnemonicsMenuItem = new JCheckBoxMenuItem(); + animationMenuItem = new JCheckBoxMenuItem(); smoothScrollingMenuItem = new JCheckBoxMenuItem(); animatedLafChangeMenuItem = new JCheckBoxMenuItem(); JMenuItem showHintsMenuItem = new JMenuItem(); @@ -797,6 +807,13 @@ private void initComponents() { alwaysShowMnemonicsMenuItem.setText("Always show mnemonics"); alwaysShowMnemonicsMenuItem.addActionListener(e -> alwaysShowMnemonics()); optionsMenu.add(alwaysShowMnemonicsMenuItem); + optionsMenu.addSeparator(); + + //---- animationMenuItem ---- + animationMenuItem.setText("Animation"); + animationMenuItem.setSelected(true); + animationMenuItem.addActionListener(e -> animationChanged()); + optionsMenu.add(animationMenuItem); //---- smoothScrollingMenuItem ---- smoothScrollingMenuItem.setText("Smooth Scrolling"); @@ -992,6 +1009,7 @@ private void unsupported( JCheckBoxMenuItem menuItem ) { private JCheckBoxMenuItem showTitleBarIconMenuItem; private JCheckBoxMenuItem underlineMenuSelectionMenuItem; private JCheckBoxMenuItem alwaysShowMnemonicsMenuItem; + private JCheckBoxMenuItem animationMenuItem; private JCheckBoxMenuItem smoothScrollingMenuItem; private JCheckBoxMenuItem animatedLafChangeMenuItem; private JMenuItem aboutMenuItem; diff --git a/flatlaf-demo/src/main/java/com/formdev/flatlaf/demo/DemoFrame.jfd b/flatlaf-demo/src/main/java/com/formdev/flatlaf/demo/DemoFrame.jfd index c44f5993f..1e5b104aa 100644 --- a/flatlaf-demo/src/main/java/com/formdev/flatlaf/demo/DemoFrame.jfd +++ b/flatlaf-demo/src/main/java/com/formdev/flatlaf/demo/DemoFrame.jfd @@ -418,6 +418,18 @@ new FormModel { } addEvent( new FormEvent( "java.awt.event.ActionListener", "actionPerformed", "alwaysShowMnemonics", false ) ) } ) + add( new FormComponent( "javax.swing.JPopupMenu$Separator" ) { + name: "separator9" + } ) + add( new FormComponent( "javax.swing.JCheckBoxMenuItem" ) { + name: "animationMenuItem" + "text": "Animation" + "selected": true + auxiliary() { + "JavaCodeGenerator.variableLocal": false + } + addEvent( new FormEvent( "java.awt.event.ActionListener", "actionPerformed", "animationChanged", false ) ) + } ) add( new FormComponent( "javax.swing.JCheckBoxMenuItem" ) { name: "smoothScrollingMenuItem" "text": "Smooth Scrolling" diff --git a/flatlaf-demo/src/main/java/com/formdev/flatlaf/demo/intellijthemes/IJThemesPanel.java b/flatlaf-demo/src/main/java/com/formdev/flatlaf/demo/intellijthemes/IJThemesPanel.java index 1af0f05c9..0c2561e2f 100644 --- a/flatlaf-demo/src/main/java/com/formdev/flatlaf/demo/intellijthemes/IJThemesPanel.java +++ b/flatlaf-demo/src/main/java/com/formdev/flatlaf/demo/intellijthemes/IJThemesPanel.java @@ -25,6 +25,7 @@ import java.awt.event.WindowAdapter; import java.awt.event.WindowEvent; import java.awt.event.WindowListener; +import java.beans.Beans; import java.beans.PropertyChangeEvent; import java.beans.PropertyChangeListener; import java.io.File; @@ -156,6 +157,9 @@ private String buildToolTip( IJThemeInfo ti ) { } private void updateThemesList() { + if( Beans.isDesignTime() ) + return; // disable if running in GUI builder + int filterLightDark = filterComboBox.getSelectedIndex(); boolean showLight = (filterLightDark != 2); boolean showDark = (filterLightDark != 1); diff --git a/flatlaf-extras/src/main/java/com/formdev/flatlaf/extras/FlatAnimatedLafChange.java b/flatlaf-extras/src/main/java/com/formdev/flatlaf/extras/FlatAnimatedLafChange.java index b46d26f4e..46899a19c 100644 --- a/flatlaf-extras/src/main/java/com/formdev/flatlaf/extras/FlatAnimatedLafChange.java +++ b/flatlaf-extras/src/main/java/com/formdev/flatlaf/extras/FlatAnimatedLafChange.java @@ -67,7 +67,7 @@ public class FlatAnimatedLafChange * Invoke before setting new look and feel. */ public static void showSnapshot() { - if( !FlatSystemProperties.getBoolean( "flatlaf.animatedLafChange", true ) ) + if( !Animator.useAnimation() || !FlatSystemProperties.getBoolean( "flatlaf.animatedLafChange", true ) ) return; // stop already running animation @@ -138,7 +138,7 @@ public void removeNotify() { * Invoke after updating UI. */ public static void hideSnapshotWithAnimation() { - if( !FlatSystemProperties.getBoolean( "flatlaf.animatedLafChange", true ) ) + if( !Animator.useAnimation() || !FlatSystemProperties.getBoolean( "flatlaf.animatedLafChange", true ) ) return; if( oldUIsnapshots.isEmpty() ) From 762fe89867d1eb1a04cf202dc7a29bc68c60fe18 Mon Sep 17 00:00:00 2001 From: Karl Tauber Date: Sat, 8 Aug 2020 16:26:14 +0200 Subject: [PATCH 08/30] FlatSmoothScrollingTest: added JTree, JTable, JTextArea, JTextPane and JEditorPane for testing smooth scrolling --- .../testing/FlatSmoothScrollingTest.java | 201 ++++++++++++++++-- .../testing/FlatSmoothScrollingTest.jfd | 96 ++++++++- 2 files changed, 275 insertions(+), 22 deletions(-) diff --git a/flatlaf-testing/src/main/java/com/formdev/flatlaf/testing/FlatSmoothScrollingTest.java b/flatlaf-testing/src/main/java/com/formdev/flatlaf/testing/FlatSmoothScrollingTest.java index 2c8e915c8..616f65ce8 100644 --- a/flatlaf-testing/src/main/java/com/formdev/flatlaf/testing/FlatSmoothScrollingTest.java +++ b/flatlaf-testing/src/main/java/com/formdev/flatlaf/testing/FlatSmoothScrollingTest.java @@ -16,11 +16,15 @@ package com.formdev.flatlaf.testing; +import java.awt.Point; import java.awt.event.AdjustmentEvent; import java.awt.event.AdjustmentListener; import java.util.ArrayList; import java.util.Arrays; +import java.util.stream.Collectors; import javax.swing.*; +import javax.swing.table.AbstractTableModel; +import javax.swing.tree.*; import net.miginfocom.swing.*; /** @@ -40,8 +44,23 @@ public static void main( String[] args ) { FlatSmoothScrollingTest() { initComponents(); - scrollPane1.getVerticalScrollBar().addAdjustmentListener( new AdjustmentHandler( "list vert" ) ); - scrollPane1.getHorizontalScrollBar().addAdjustmentListener( new AdjustmentHandler( "list horz" ) ); + listScrollPane.getVerticalScrollBar().addAdjustmentListener( new AdjustmentHandler( "list vert" ) ); + listScrollPane.getHorizontalScrollBar().addAdjustmentListener( new AdjustmentHandler( "list horz" ) ); + + treeScrollPane.getVerticalScrollBar().addAdjustmentListener( new AdjustmentHandler( "tree vert" ) ); + treeScrollPane.getHorizontalScrollBar().addAdjustmentListener( new AdjustmentHandler( "tree horz" ) ); + + tableScrollPane.getVerticalScrollBar().addAdjustmentListener( new AdjustmentHandler( "table vert" ) ); + tableScrollPane.getHorizontalScrollBar().addAdjustmentListener( new AdjustmentHandler( "table horz" ) ); + + textAreaScrollPane.getVerticalScrollBar().addAdjustmentListener( new AdjustmentHandler( "textArea vert" ) ); + textAreaScrollPane.getHorizontalScrollBar().addAdjustmentListener( new AdjustmentHandler( "textArea horz" ) ); + + textPaneScrollPane.getVerticalScrollBar().addAdjustmentListener( new AdjustmentHandler( "textPane vert" ) ); + textPaneScrollPane.getHorizontalScrollBar().addAdjustmentListener( new AdjustmentHandler( "textPane horz" ) ); + + editorPaneScrollPane.getVerticalScrollBar().addAdjustmentListener( new AdjustmentHandler( "editorPane vert" ) ); + editorPaneScrollPane.getHorizontalScrollBar().addAdjustmentListener( new AdjustmentHandler( "editorPane horz" ) ); ArrayList items = new ArrayList<>(); for( char ch = '0'; ch < 'z'; ch++ ) { @@ -50,17 +69,56 @@ public static void main( String[] args ) { items.add( new String( chars ) ); } - list1.setModel( new AbstractListModel() { + // list model + list.setModel( new AbstractListModel() { @Override public int getSize() { return items.size(); } - @Override public String getElementAt( int index ) { return items.get( index ); } } ); + + // tree model + DefaultMutableTreeNode root = new DefaultMutableTreeNode( items.get( 0 ) ); + DefaultMutableTreeNode last = null; + for( int i = 1; i < items.size(); i++ ) { + DefaultMutableTreeNode node = new DefaultMutableTreeNode( items.get( i ) ); + if( i % 5 == 1 ) { + root.add( node ); + last = node; + } else + last.add( node ); + } + tree.setModel( new DefaultTreeModel( root ) ); + for( int row = tree.getRowCount() - 1; row >= 0; row-- ) + tree.expandRow( row ); + + // table model + table.setModel( new AbstractTableModel() { + @Override + public int getRowCount() { + return items.size(); + } + @Override + public int getColumnCount() { + return 4; + } + @Override + public Object getValueAt( int rowIndex, int columnIndex ) { + if( columnIndex > 0 ) + rowIndex = (items.size() + rowIndex - ((items.size() / 4) * columnIndex)) % items.size(); + return items.get( rowIndex ); + } + } ); + + // text components + String text = items.stream().collect( Collectors.joining( "\n" ) ); + textArea.setText( text ); + textPane.setText( text ); + editorPane.setText( text ); } private void smoothScrollingChanged() { @@ -71,42 +129,137 @@ private void initComponents() { // JFormDesigner - Component initialization - DO NOT MODIFY //GEN-BEGIN:initComponents smoothScrollingCheckBox = new JCheckBox(); listLabel = new JLabel(); - scrollPane1 = new JScrollPane(); - list1 = new JList<>(); + label1 = new JLabel(); + label5 = new JLabel(); + listScrollPane = new FlatSmoothScrollingTest.DebugScrollPane(); + list = new JList<>(); + treeScrollPane = new FlatSmoothScrollingTest.DebugScrollPane(); + tree = new JTree(); + tableScrollPane = new JScrollPane(); + table = new JTable(); + label2 = new JLabel(); + label3 = new JLabel(); + label4 = new JLabel(); + textAreaScrollPane = new FlatSmoothScrollingTest.DebugScrollPane(); + textArea = new JTextArea(); + textPaneScrollPane = new FlatSmoothScrollingTest.DebugScrollPane(); + textPane = new JTextPane(); + editorPaneScrollPane = new FlatSmoothScrollingTest.DebugScrollPane(); + editorPane = new JEditorPane(); //======== this ======== setLayout(new MigLayout( "ltr,insets dialog,hidemode 3", // columns - "[]" + - "[200]", + "[200,fill]" + + "[200,fill]" + + "[200,fill]" + + "[200,fill]", // rows "[]" + - "[::200,grow,fill]")); + "[]" + + "[200,grow,fill]" + + "[]" + + "[200,grow,fill]")); //---- smoothScrollingCheckBox ---- smoothScrollingCheckBox.setText("Smooth scrolling"); smoothScrollingCheckBox.setSelected(true); smoothScrollingCheckBox.addActionListener(e -> smoothScrollingChanged()); - add(smoothScrollingCheckBox, "cell 0 0 2 1,alignx left,growx 0"); + add(smoothScrollingCheckBox, "cell 0 0,alignx left,growx 0"); //---- listLabel ---- listLabel.setText("JList:"); add(listLabel, "cell 0 1,aligny top,growy 0"); - //======== scrollPane1 ======== + //---- label1 ---- + label1.setText("JTree:"); + add(label1, "cell 1 1"); + + //---- label5 ---- + label5.setText("JTable:"); + add(label5, "cell 2 1"); + + //======== listScrollPane ======== + { + listScrollPane.setViewportView(list); + } + add(listScrollPane, "cell 0 2,growx"); + + //======== treeScrollPane ======== + { + + //---- tree ---- + tree.setModel(new DefaultTreeModel( + new DefaultMutableTreeNode("root") { + { + add(new DefaultMutableTreeNode("a")); + add(new DefaultMutableTreeNode("b")); + add(new DefaultMutableTreeNode("c")); + } + })); + treeScrollPane.setViewportView(tree); + } + add(treeScrollPane, "cell 1 2"); + + //======== tableScrollPane ======== + { + tableScrollPane.setViewportView(table); + } + add(tableScrollPane, "cell 2 2 2 1,width 100,height 100"); + + //---- label2 ---- + label2.setText("JTextArea:"); + add(label2, "cell 0 3"); + + //---- label3 ---- + label3.setText("JTextPane:"); + add(label3, "cell 1 3"); + + //---- label4 ---- + label4.setText("JEditorPane:"); + add(label4, "cell 2 3"); + + //======== textAreaScrollPane ======== { - scrollPane1.setViewportView(list1); + textAreaScrollPane.setViewportView(textArea); } - add(scrollPane1, "cell 1 1,growx"); + add(textAreaScrollPane, "cell 0 4"); + + //======== textPaneScrollPane ======== + { + textPaneScrollPane.setViewportView(textPane); + } + add(textPaneScrollPane, "cell 1 4"); + + //======== editorPaneScrollPane ======== + { + editorPaneScrollPane.setViewportView(editorPane); + } + add(editorPaneScrollPane, "cell 2 4"); // JFormDesigner - End of component initialization //GEN-END:initComponents } // JFormDesigner - Variables declaration - DO NOT MODIFY //GEN-BEGIN:variables private JCheckBox smoothScrollingCheckBox; private JLabel listLabel; - private JScrollPane scrollPane1; - private JList list1; + private JLabel label1; + private JLabel label5; + private FlatSmoothScrollingTest.DebugScrollPane listScrollPane; + private JList list; + private FlatSmoothScrollingTest.DebugScrollPane treeScrollPane; + private JTree tree; + private JScrollPane tableScrollPane; + private JTable table; + private JLabel label2; + private JLabel label3; + private JLabel label4; + private FlatSmoothScrollingTest.DebugScrollPane textAreaScrollPane; + private JTextArea textArea; + private FlatSmoothScrollingTest.DebugScrollPane textPaneScrollPane; + private JTextPane textPane; + private FlatSmoothScrollingTest.DebugScrollPane editorPaneScrollPane; + private JEditorPane editorPane; // JFormDesigner - End of variables declaration //GEN-END:variables //---- class AdjustmentHandler -------------------------------------------- @@ -141,4 +294,22 @@ private String adjustmentType2Str( int adjustmentType ) { } } } + + //---- class DebugViewport ------------------------------------------------ + + private static class DebugScrollPane + extends JScrollPane + { + @Override + protected JViewport createViewport() { + return new JViewport() { + @Override + public Point getViewPosition() { + Point viewPosition = super.getViewPosition(); + System.out.println( " viewPosition " + viewPosition.x + "," + viewPosition.y ); + return viewPosition; + } + }; + } + } } diff --git a/flatlaf-testing/src/main/java/com/formdev/flatlaf/testing/FlatSmoothScrollingTest.jfd b/flatlaf-testing/src/main/java/com/formdev/flatlaf/testing/FlatSmoothScrollingTest.jfd index 392312915..a2937f270 100644 --- a/flatlaf-testing/src/main/java/com/formdev/flatlaf/testing/FlatSmoothScrollingTest.jfd +++ b/flatlaf-testing/src/main/java/com/formdev/flatlaf/testing/FlatSmoothScrollingTest.jfd @@ -5,8 +5,8 @@ new FormModel { root: new FormRoot { add( new FormContainer( "com.formdev.flatlaf.testing.FlatTestPanel", new FormLayoutManager( class net.miginfocom.swing.MigLayout ) { "$layoutConstraints": "ltr,insets dialog,hidemode 3" - "$columnConstraints": "[][200]" - "$rowConstraints": "[][::200,grow,fill]" + "$columnConstraints": "[200,fill][200,fill][200,fill][200,fill]" + "$rowConstraints": "[][][200,grow,fill][][200,grow,fill]" } ) { name: "this" add( new FormComponent( "javax.swing.JCheckBox" ) { @@ -15,7 +15,7 @@ new FormModel { "selected": true addEvent( new FormEvent( "java.awt.event.ActionListener", "actionPerformed", "smoothScrollingChanged", false ) ) }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { - "value": "cell 0 0 2 1,alignx left,growx 0" + "value": "cell 0 0,alignx left,growx 0" } ) add( new FormComponent( "javax.swing.JLabel" ) { name: "listLabel" @@ -23,17 +23,99 @@ new FormModel { }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { "value": "cell 0 1,aligny top,growy 0" } ) - add( new FormContainer( "javax.swing.JScrollPane", new FormLayoutManager( class javax.swing.JScrollPane ) ) { - name: "scrollPane1" + add( new FormComponent( "javax.swing.JLabel" ) { + name: "label1" + "text": "JTree:" + }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { + "value": "cell 1 1" + } ) + add( new FormComponent( "javax.swing.JLabel" ) { + name: "label5" + "text": "JTable:" + }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { + "value": "cell 2 1" + } ) + add( new FormContainer( "com.formdev.flatlaf.testing.FlatSmoothScrollingTest$DebugScrollPane", new FormLayoutManager( class javax.swing.JScrollPane ) ) { + name: "listScrollPane" add( new FormComponent( "javax.swing.JList" ) { - name: "list1" + name: "list" auxiliary() { "JavaCodeGenerator.typeParameters": "String" "JavaCodeGenerator.variableLocal": false } } ) }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { - "value": "cell 1 1,growx" + "value": "cell 0 2,growx" + } ) + add( new FormContainer( "com.formdev.flatlaf.testing.FlatSmoothScrollingTest$DebugScrollPane", new FormLayoutManager( class javax.swing.JScrollPane ) ) { + name: "treeScrollPane" + add( new FormComponent( "javax.swing.JTree" ) { + name: "tree" + "model": new javax.swing.tree.DefaultTreeModel( new javax.swing.tree.DefaultMutableTreeNode { + userObject: "root" + add( new javax.swing.tree.DefaultMutableTreeNode { + userObject: "a" + } ) + add( new javax.swing.tree.DefaultMutableTreeNode { + userObject: "b" + } ) + add( new javax.swing.tree.DefaultMutableTreeNode { + userObject: "c" + } ) + } ) + } ) + }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { + "value": "cell 1 2" + } ) + add( new FormContainer( "javax.swing.JScrollPane", new FormLayoutManager( class javax.swing.JScrollPane ) ) { + name: "tableScrollPane" + add( new FormComponent( "javax.swing.JTable" ) { + name: "table" + } ) + }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { + "value": "cell 2 2 2 1,width 100,height 100" + } ) + add( new FormComponent( "javax.swing.JLabel" ) { + name: "label2" + "text": "JTextArea:" + }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { + "value": "cell 0 3" + } ) + add( new FormComponent( "javax.swing.JLabel" ) { + name: "label3" + "text": "JTextPane:" + }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { + "value": "cell 1 3" + } ) + add( new FormComponent( "javax.swing.JLabel" ) { + name: "label4" + "text": "JEditorPane:" + }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { + "value": "cell 2 3" + } ) + add( new FormContainer( "com.formdev.flatlaf.testing.FlatSmoothScrollingTest$DebugScrollPane", new FormLayoutManager( class javax.swing.JScrollPane ) ) { + name: "textAreaScrollPane" + add( new FormComponent( "javax.swing.JTextArea" ) { + name: "textArea" + } ) + }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { + "value": "cell 0 4" + } ) + add( new FormContainer( "com.formdev.flatlaf.testing.FlatSmoothScrollingTest$DebugScrollPane", new FormLayoutManager( class javax.swing.JScrollPane ) ) { + name: "textPaneScrollPane" + add( new FormComponent( "javax.swing.JTextPane" ) { + name: "textPane" + } ) + }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { + "value": "cell 1 4" + } ) + add( new FormContainer( "com.formdev.flatlaf.testing.FlatSmoothScrollingTest$DebugScrollPane", new FormLayoutManager( class javax.swing.JScrollPane ) ) { + name: "editorPaneScrollPane" + add( new FormComponent( "javax.swing.JEditorPane" ) { + name: "editorPane" + } ) + }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { + "value": "cell 2 4" } ) }, new FormLayoutConstraints( null ) { "location": new java.awt.Point( 0, 0 ) From 7a582c2d1f51026f7b3635dc3ebe476e6983ee89 Mon Sep 17 00:00:00 2001 From: Karl Tauber Date: Sun, 9 Aug 2020 00:14:44 +0200 Subject: [PATCH 09/30] ScrollBar: fixed issue with updating thumb location (regressing since commit 2c3ef226692fa39b7e6eca3192d197c0b0753aa1) --- .../formdev/flatlaf/ui/FlatScrollBarUI.java | 34 ++++++++++++++++--- 1 file changed, 29 insertions(+), 5 deletions(-) diff --git a/flatlaf-core/src/main/java/com/formdev/flatlaf/ui/FlatScrollBarUI.java b/flatlaf-core/src/main/java/com/formdev/flatlaf/ui/FlatScrollBarUI.java index 9a5abc2ab..d25103ed9 100644 --- a/flatlaf-core/src/main/java/com/formdev/flatlaf/ui/FlatScrollBarUI.java +++ b/flatlaf-core/src/main/java/com/formdev/flatlaf/ui/FlatScrollBarUI.java @@ -474,7 +474,8 @@ public void runAndSetValueAnimated( Runnable r ) { if( animator != null ) animator.cancel(); - scrollbar.setValueIsAdjusting( true ); + if( useValueIsAdjusting ) + scrollbar.setValueIsAdjusting( true ); // if invoked while animation is running, calculation of new value // should start at the previous target value @@ -485,12 +486,18 @@ public void runAndSetValueAnimated( Runnable r ) { r.run(); + // do not use animation if started dragging thumb + if( isDragging ) { + inRunAndSetValueAnimated = false; + return; + } + int newValue = scrollbar.getValue(); if( newValue != oldValue ) { scrollbar.setValue( oldValue ); setValueAnimated( newValue ); - } else + } else if( useValueIsAdjusting ) scrollbar.setValueIsAdjusting( false ); inRunAndSetValueAnimated = false; @@ -500,9 +507,11 @@ public void runAndSetValueAnimated( Runnable r ) { private Animator animator; private int targetValue = Integer.MIN_VALUE; private int delta; + private boolean useValueIsAdjusting = true; public void setValueAnimated( int value ) { - scrollbar.setValueIsAdjusting( true ); + if( useValueIsAdjusting ) + scrollbar.setValueIsAdjusting( true ); // create animator if( animator == null ) { @@ -518,13 +527,14 @@ public void setValueAnimated( int value ) { // re-enable valueIsAdjusting if disabled while animation is running // (e.g. in mouse released listener) - if( !scrollbar.getValueIsAdjusting() ) + if( useValueIsAdjusting && !scrollbar.getValueIsAdjusting() ) scrollbar.setValueIsAdjusting( true ); scrollbar.setValue( targetValue - delta + Math.round( delta * fraction ) ); }, () -> { targetValue = Integer.MIN_VALUE; - scrollbar.setValueIsAdjusting( false ); + if( useValueIsAdjusting ) + scrollbar.setValueIsAdjusting( false ); }); animator.setResolution( resolution ); @@ -564,10 +574,24 @@ protected class FlatTrackListener { @Override public void mousePressed( MouseEvent e ) { + // Do not use valueIsAdjusting here (in runAndSetValueAnimated()) + // for smooth scrolling because super.mousePressed() enables this itself + // and super.mouseRelease() disables it later. + // If we would disable valueIsAdjusting here (in runAndSetValueAnimated()) + // and move the thumb with the mouse, then the thumb location is not updated + // if later scrolled with a key (e.g. HOME key). + useValueIsAdjusting = false; + runAndSetValueAnimated( () -> { super.mousePressed( e ); } ); } + + @Override + public void mouseReleased( MouseEvent e ) { + super.mouseReleased( e ); + useValueIsAdjusting = true; + } } //---- class FlatScrollListener ------------------------------------------- From 522ebb6fa3d511dc05af6c2b25dbe1d62dc7d0c8 Mon Sep 17 00:00:00 2001 From: Karl Tauber Date: Sun, 9 Aug 2020 21:32:42 +0200 Subject: [PATCH 10/30] FlatSmoothScrollingTest: allow enabling/disabling smooth scrolling with Alt+S without moving focus to checkbox; removed unused tree model --- .../testing/FlatSmoothScrollingTest.java | 26 +++++++++++-------- .../testing/FlatSmoothScrollingTest.jfd | 13 +--------- 2 files changed, 16 insertions(+), 23 deletions(-) diff --git a/flatlaf-testing/src/main/java/com/formdev/flatlaf/testing/FlatSmoothScrollingTest.java b/flatlaf-testing/src/main/java/com/formdev/flatlaf/testing/FlatSmoothScrollingTest.java index 616f65ce8..cca6d7683 100644 --- a/flatlaf-testing/src/main/java/com/formdev/flatlaf/testing/FlatSmoothScrollingTest.java +++ b/flatlaf-testing/src/main/java/com/formdev/flatlaf/testing/FlatSmoothScrollingTest.java @@ -44,6 +44,15 @@ public static void main( String[] args ) { FlatSmoothScrollingTest() { initComponents(); + // allow enabling/disabling smooth scrolling with Alt+S without moving focus to checkbox + registerKeyboardAction( + e -> { + smoothScrollingCheckBox.setSelected( !smoothScrollingCheckBox.isSelected() ); + smoothScrollingChanged(); + }, + KeyStroke.getKeyStroke( "alt " + (char) smoothScrollingCheckBox.getMnemonic() ), + JComponent.WHEN_ANCESTOR_OF_FOCUSED_COMPONENT ); + listScrollPane.getVerticalScrollBar().addAdjustmentListener( new AdjustmentHandler( "list vert" ) ); listScrollPane.getHorizontalScrollBar().addAdjustmentListener( new AdjustmentHandler( "list horz" ) ); @@ -119,6 +128,10 @@ public Object getValueAt( int rowIndex, int columnIndex ) { textArea.setText( text ); textPane.setText( text ); editorPane.setText( text ); + + textArea.select( 0, 0 ); + textPane.select( 0, 0 ); + editorPane.select( 0, 0 ); } private void smoothScrollingChanged() { @@ -165,6 +178,7 @@ private void initComponents() { //---- smoothScrollingCheckBox ---- smoothScrollingCheckBox.setText("Smooth scrolling"); smoothScrollingCheckBox.setSelected(true); + smoothScrollingCheckBox.setMnemonic('S'); smoothScrollingCheckBox.addActionListener(e -> smoothScrollingChanged()); add(smoothScrollingCheckBox, "cell 0 0,alignx left,growx 0"); @@ -188,16 +202,6 @@ private void initComponents() { //======== treeScrollPane ======== { - - //---- tree ---- - tree.setModel(new DefaultTreeModel( - new DefaultMutableTreeNode("root") { - { - add(new DefaultMutableTreeNode("a")); - add(new DefaultMutableTreeNode("b")); - add(new DefaultMutableTreeNode("c")); - } - })); treeScrollPane.setViewportView(tree); } add(treeScrollPane, "cell 1 2"); @@ -306,7 +310,7 @@ protected JViewport createViewport() { @Override public Point getViewPosition() { Point viewPosition = super.getViewPosition(); - System.out.println( " viewPosition " + viewPosition.x + "," + viewPosition.y ); +// System.out.println( " viewPosition " + viewPosition.x + "," + viewPosition.y ); return viewPosition; } }; diff --git a/flatlaf-testing/src/main/java/com/formdev/flatlaf/testing/FlatSmoothScrollingTest.jfd b/flatlaf-testing/src/main/java/com/formdev/flatlaf/testing/FlatSmoothScrollingTest.jfd index a2937f270..cdeac49e3 100644 --- a/flatlaf-testing/src/main/java/com/formdev/flatlaf/testing/FlatSmoothScrollingTest.jfd +++ b/flatlaf-testing/src/main/java/com/formdev/flatlaf/testing/FlatSmoothScrollingTest.jfd @@ -13,6 +13,7 @@ new FormModel { name: "smoothScrollingCheckBox" "text": "Smooth scrolling" "selected": true + "mnemonic": 83 addEvent( new FormEvent( "java.awt.event.ActionListener", "actionPerformed", "smoothScrollingChanged", false ) ) }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { "value": "cell 0 0,alignx left,growx 0" @@ -51,18 +52,6 @@ new FormModel { name: "treeScrollPane" add( new FormComponent( "javax.swing.JTree" ) { name: "tree" - "model": new javax.swing.tree.DefaultTreeModel( new javax.swing.tree.DefaultMutableTreeNode { - userObject: "root" - add( new javax.swing.tree.DefaultMutableTreeNode { - userObject: "a" - } ) - add( new javax.swing.tree.DefaultMutableTreeNode { - userObject: "b" - } ) - add( new javax.swing.tree.DefaultMutableTreeNode { - userObject: "c" - } ) - } ) } ) }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { "value": "cell 1 2" From e603bd81a18f67ada8b33523a26718f7e864d8a3 Mon Sep 17 00:00:00 2001 From: Karl Tauber Date: Mon, 10 Aug 2020 13:53:32 +0200 Subject: [PATCH 11/30] FlatSmoothScrollingTest: added simple line chart that shows changes to scrollbar values --- .../testing/FlatSmoothScrollingTest.java | 413 ++++++++++++++++-- .../testing/FlatSmoothScrollingTest.jfd | 51 ++- 2 files changed, 419 insertions(+), 45 deletions(-) diff --git a/flatlaf-testing/src/main/java/com/formdev/flatlaf/testing/FlatSmoothScrollingTest.java b/flatlaf-testing/src/main/java/com/formdev/flatlaf/testing/FlatSmoothScrollingTest.java index cca6d7683..3c8dabb47 100644 --- a/flatlaf-testing/src/main/java/com/formdev/flatlaf/testing/FlatSmoothScrollingTest.java +++ b/flatlaf-testing/src/main/java/com/formdev/flatlaf/testing/FlatSmoothScrollingTest.java @@ -16,15 +16,27 @@ package com.formdev.flatlaf.testing; +import java.awt.Color; +import java.awt.Component; +import java.awt.Dimension; +import java.awt.Graphics; +import java.awt.Graphics2D; import java.awt.Point; +import java.awt.Rectangle; import java.awt.event.AdjustmentEvent; import java.awt.event.AdjustmentListener; import java.util.ArrayList; import java.util.Arrays; +import java.util.HashMap; +import java.util.List; +import java.util.Map; import java.util.stream.Collectors; import javax.swing.*; import javax.swing.table.AbstractTableModel; import javax.swing.tree.*; +import com.formdev.flatlaf.ui.FlatUIUtils; +import com.formdev.flatlaf.util.HiDPIUtils; +import com.formdev.flatlaf.util.UIScale; import net.miginfocom.swing.*; /** @@ -44,6 +56,8 @@ public static void main( String[] args ) { FlatSmoothScrollingTest() { initComponents(); + updateChartDelayedChanged(); + // allow enabling/disabling smooth scrolling with Alt+S without moving focus to checkbox registerKeyboardAction( e -> { @@ -53,23 +67,30 @@ public static void main( String[] args ) { KeyStroke.getKeyStroke( "alt " + (char) smoothScrollingCheckBox.getMnemonic() ), JComponent.WHEN_ANCESTOR_OF_FOCUSED_COMPONENT ); - listScrollPane.getVerticalScrollBar().addAdjustmentListener( new AdjustmentHandler( "list vert" ) ); - listScrollPane.getHorizontalScrollBar().addAdjustmentListener( new AdjustmentHandler( "list horz" ) ); + listLabel.setIcon( new ColorIcon( Color.red.darker() ) ); + treeLabel.setIcon( new ColorIcon( Color.blue.darker() ) ); + tableLabel.setIcon( new ColorIcon( Color.green.darker() ) ); + textAreaLabel.setIcon( new ColorIcon( Color.magenta.darker() ) ); + textPaneLabel.setIcon( new ColorIcon( Color.cyan.darker() ) ); + editorPaneLabel.setIcon( new ColorIcon( Color.orange.darker() ) ); + + listScrollPane.getVerticalScrollBar().addAdjustmentListener( new AdjustmentHandler( "list vert", Color.red.darker() ) ); + listScrollPane.getHorizontalScrollBar().addAdjustmentListener( new AdjustmentHandler( "list horz", Color.red ) ); - treeScrollPane.getVerticalScrollBar().addAdjustmentListener( new AdjustmentHandler( "tree vert" ) ); - treeScrollPane.getHorizontalScrollBar().addAdjustmentListener( new AdjustmentHandler( "tree horz" ) ); + treeScrollPane.getVerticalScrollBar().addAdjustmentListener( new AdjustmentHandler( "tree vert", Color.blue.darker() ) ); + treeScrollPane.getHorizontalScrollBar().addAdjustmentListener( new AdjustmentHandler( "tree horz", Color.blue ) ); - tableScrollPane.getVerticalScrollBar().addAdjustmentListener( new AdjustmentHandler( "table vert" ) ); - tableScrollPane.getHorizontalScrollBar().addAdjustmentListener( new AdjustmentHandler( "table horz" ) ); + tableScrollPane.getVerticalScrollBar().addAdjustmentListener( new AdjustmentHandler( "table vert", Color.green.darker() ) ); + tableScrollPane.getHorizontalScrollBar().addAdjustmentListener( new AdjustmentHandler( "table horz", Color.green ) ); - textAreaScrollPane.getVerticalScrollBar().addAdjustmentListener( new AdjustmentHandler( "textArea vert" ) ); - textAreaScrollPane.getHorizontalScrollBar().addAdjustmentListener( new AdjustmentHandler( "textArea horz" ) ); + textAreaScrollPane.getVerticalScrollBar().addAdjustmentListener( new AdjustmentHandler( "textArea vert", Color.magenta.darker() ) ); + textAreaScrollPane.getHorizontalScrollBar().addAdjustmentListener( new AdjustmentHandler( "textArea horz", Color.magenta ) ); - textPaneScrollPane.getVerticalScrollBar().addAdjustmentListener( new AdjustmentHandler( "textPane vert" ) ); - textPaneScrollPane.getHorizontalScrollBar().addAdjustmentListener( new AdjustmentHandler( "textPane horz" ) ); + textPaneScrollPane.getVerticalScrollBar().addAdjustmentListener( new AdjustmentHandler( "textPane vert", Color.cyan.darker() ) ); + textPaneScrollPane.getHorizontalScrollBar().addAdjustmentListener( new AdjustmentHandler( "textPane horz", Color.cyan ) ); - editorPaneScrollPane.getVerticalScrollBar().addAdjustmentListener( new AdjustmentHandler( "editorPane vert" ) ); - editorPaneScrollPane.getHorizontalScrollBar().addAdjustmentListener( new AdjustmentHandler( "editorPane horz" ) ); + editorPaneScrollPane.getVerticalScrollBar().addAdjustmentListener( new AdjustmentHandler( "editorPane vert", Color.orange.darker() ) ); + editorPaneScrollPane.getHorizontalScrollBar().addAdjustmentListener( new AdjustmentHandler( "editorPane horz", Color.orange ) ); ArrayList items = new ArrayList<>(); for( char ch = '0'; ch < 'z'; ch++ ) { @@ -138,27 +159,40 @@ private void smoothScrollingChanged() { UIManager.put( "ScrollPane.smoothScrolling", smoothScrollingCheckBox.isSelected() ); } + private void clearChart() { + lineChartPanel.clear(); + } + + private void updateChartDelayedChanged() { + lineChartPanel.setUpdateDelayed( updateChartDelayedCheckBox.isSelected() ); + } + private void initComponents() { // JFormDesigner - Component initialization - DO NOT MODIFY //GEN-BEGIN:initComponents smoothScrollingCheckBox = new JCheckBox(); listLabel = new JLabel(); - label1 = new JLabel(); - label5 = new JLabel(); + treeLabel = new JLabel(); + tableLabel = new JLabel(); listScrollPane = new FlatSmoothScrollingTest.DebugScrollPane(); list = new JList<>(); treeScrollPane = new FlatSmoothScrollingTest.DebugScrollPane(); tree = new JTree(); tableScrollPane = new JScrollPane(); table = new JTable(); - label2 = new JLabel(); - label3 = new JLabel(); - label4 = new JLabel(); + textAreaLabel = new JLabel(); + textPaneLabel = new JLabel(); + editorPaneLabel = new JLabel(); textAreaScrollPane = new FlatSmoothScrollingTest.DebugScrollPane(); textArea = new JTextArea(); textPaneScrollPane = new FlatSmoothScrollingTest.DebugScrollPane(); textPane = new JTextPane(); editorPaneScrollPane = new FlatSmoothScrollingTest.DebugScrollPane(); editorPane = new JEditorPane(); + panel1 = new JPanel(); + updateChartDelayedCheckBox = new JCheckBox(); + clearChartButton = new JButton(); + scrollPane1 = new JScrollPane(); + lineChartPanel = new FlatSmoothScrollingTest.LineChartPanel(); //======== this ======== setLayout(new MigLayout( @@ -173,7 +207,8 @@ private void initComponents() { "[]" + "[200,grow,fill]" + "[]" + - "[200,grow,fill]")); + "[200,grow,fill]" + + "[300,grow,fill]")); //---- smoothScrollingCheckBox ---- smoothScrollingCheckBox.setText("Smooth scrolling"); @@ -184,15 +219,18 @@ private void initComponents() { //---- listLabel ---- listLabel.setText("JList:"); + listLabel.setHorizontalTextPosition(SwingConstants.LEADING); add(listLabel, "cell 0 1,aligny top,growy 0"); - //---- label1 ---- - label1.setText("JTree:"); - add(label1, "cell 1 1"); + //---- treeLabel ---- + treeLabel.setText("JTree:"); + treeLabel.setHorizontalTextPosition(SwingConstants.LEADING); + add(treeLabel, "cell 1 1"); - //---- label5 ---- - label5.setText("JTable:"); - add(label5, "cell 2 1"); + //---- tableLabel ---- + tableLabel.setText("JTable:"); + tableLabel.setHorizontalTextPosition(SwingConstants.LEADING); + add(tableLabel, "cell 2 1"); //======== listScrollPane ======== { @@ -212,17 +250,20 @@ private void initComponents() { } add(tableScrollPane, "cell 2 2 2 1,width 100,height 100"); - //---- label2 ---- - label2.setText("JTextArea:"); - add(label2, "cell 0 3"); + //---- textAreaLabel ---- + textAreaLabel.setText("JTextArea:"); + textAreaLabel.setHorizontalTextPosition(SwingConstants.LEADING); + add(textAreaLabel, "cell 0 3"); - //---- label3 ---- - label3.setText("JTextPane:"); - add(label3, "cell 1 3"); + //---- textPaneLabel ---- + textPaneLabel.setText("JTextPane:"); + textPaneLabel.setHorizontalTextPosition(SwingConstants.LEADING); + add(textPaneLabel, "cell 1 3"); - //---- label4 ---- - label4.setText("JEditorPane:"); - add(label4, "cell 2 3"); + //---- editorPaneLabel ---- + editorPaneLabel.setText("JEditorPane:"); + editorPaneLabel.setHorizontalTextPosition(SwingConstants.LEADING); + add(editorPaneLabel, "cell 2 3"); //======== textAreaScrollPane ======== { @@ -241,45 +282,92 @@ private void initComponents() { editorPaneScrollPane.setViewportView(editorPane); } add(editorPaneScrollPane, "cell 2 4"); + + //======== panel1 ======== + { + panel1.setLayout(new MigLayout( + "ltr,insets 0,hidemode 3,aligny bottom", + // columns + "[200,right]", + // rows + "[]" + + "[]")); + + //---- updateChartDelayedCheckBox ---- + updateChartDelayedCheckBox.setText("Update chart delayed"); + updateChartDelayedCheckBox.setMnemonic('U'); + updateChartDelayedCheckBox.setSelected(true); + updateChartDelayedCheckBox.addActionListener(e -> updateChartDelayedChanged()); + panel1.add(updateChartDelayedCheckBox, "cell 0 0"); + + //---- clearChartButton ---- + clearChartButton.setText("Clear Chart"); + clearChartButton.addActionListener(e -> clearChart()); + panel1.add(clearChartButton, "cell 0 1"); + } + add(panel1, "cell 3 4"); + + //======== scrollPane1 ======== + { + scrollPane1.setViewportView(lineChartPanel); + } + add(scrollPane1, "cell 0 5 4 1,width 100"); // JFormDesigner - End of component initialization //GEN-END:initComponents } // JFormDesigner - Variables declaration - DO NOT MODIFY //GEN-BEGIN:variables private JCheckBox smoothScrollingCheckBox; private JLabel listLabel; - private JLabel label1; - private JLabel label5; + private JLabel treeLabel; + private JLabel tableLabel; private FlatSmoothScrollingTest.DebugScrollPane listScrollPane; private JList list; private FlatSmoothScrollingTest.DebugScrollPane treeScrollPane; private JTree tree; private JScrollPane tableScrollPane; private JTable table; - private JLabel label2; - private JLabel label3; - private JLabel label4; + private JLabel textAreaLabel; + private JLabel textPaneLabel; + private JLabel editorPaneLabel; private FlatSmoothScrollingTest.DebugScrollPane textAreaScrollPane; private JTextArea textArea; private FlatSmoothScrollingTest.DebugScrollPane textPaneScrollPane; private JTextPane textPane; private FlatSmoothScrollingTest.DebugScrollPane editorPaneScrollPane; private JEditorPane editorPane; + private JPanel panel1; + private JCheckBox updateChartDelayedCheckBox; + private JButton clearChartButton; + private JScrollPane scrollPane1; + private FlatSmoothScrollingTest.LineChartPanel lineChartPanel; // JFormDesigner - End of variables declaration //GEN-END:variables //---- class AdjustmentHandler -------------------------------------------- - private static class AdjustmentHandler + private class AdjustmentHandler implements AdjustmentListener { private final String name; + private final Color chartColor; private int count; AdjustmentHandler( String name ) { + this( name, null ); + } + + AdjustmentHandler( String name, Color chartColor ) { this.name = name; + this.chartColor = chartColor; } @Override public void adjustmentValueChanged( AdjustmentEvent e ) { + if( chartColor != null ) { + JScrollBar sb = (JScrollBar) e.getSource(); + double value = (double) (e.getValue() - sb.getMinimum()) / (double) (sb.getMaximum() - sb.getVisibleAmount()); + lineChartPanel.addValue( value, chartColor ); + } + System.out.printf( "%s (%d): %s %3d %b%n", name, ++count, adjustmentType2Str( e.getAdjustmentType() ), @@ -316,4 +404,251 @@ public Point getViewPosition() { }; } } + + //---- class LineChartPanel ----------------------------------------------- + + private static class LineChartPanel + extends JComponent + implements Scrollable + { + private static final int SECOND_WIDTH = 200; + private static final int NEW_SEQUENCE_TIME_LAG = 500; + private static final int NEW_SEQUENCE_GAP = 20; + + private static class Data { + final double value; + final long time; // in milliseconds + + Data( double value, long time ) { + this.value = value; + this.time = time; + } + } + + private final Map> color2dataMap = new HashMap<>(); + private final Timer repaintTime; + private Color lastUsedChartColor; + private boolean updateDelayed; + + LineChartPanel() { + int resolution = FlatUIUtils.getUIInt( "ScrollPane.smoothScrolling.resolution", 10 ); + + repaintTime = new Timer( resolution * 2, e -> repaintAndRevalidate() ); + repaintTime.setRepeats( false ); + } + + void addValue( double value, Color chartColor ) { + List chartData = color2dataMap.computeIfAbsent( chartColor, k -> new ArrayList<>() ); + chartData.add( new Data( value, System.currentTimeMillis()) ); + + lastUsedChartColor = chartColor; + + if( updateDelayed ) { + repaintTime.stop(); + repaintTime.start(); + } else + repaintAndRevalidate(); + } + + void clear() { + color2dataMap.clear(); + lastUsedChartColor = null; + + repaint(); + revalidate(); + } + + void setUpdateDelayed( boolean updateDelayed ) { + this.updateDelayed = updateDelayed; + } + + private void repaintAndRevalidate() { + repaint(); + revalidate(); + + // scroll horizontally + if( lastUsedChartColor != null ) { + int[] lastSeqX = new int[1]; + int cw = chartWidth( color2dataMap.get( lastUsedChartColor ), lastSeqX ); + scrollRectToVisible( new Rectangle( lastSeqX[0], 0, cw - lastSeqX[0], getHeight() ) ); + } + } + + @Override + protected void paintComponent( Graphics g ) { + Graphics g2 = g.create(); + try { + HiDPIUtils.paintAtScale1x( (Graphics2D) g2, this, this::paintImpl ); + } finally { + g2.dispose(); + } + } + + private void paintImpl( Graphics2D g, int x, int y, int width, int height, double scaleFactor ) { + FlatUIUtils.setRenderingHints( g ); + + int secondWidth = (int) (SECOND_WIDTH * scaleFactor); + int seqGapWidth = (int) (NEW_SEQUENCE_GAP * scaleFactor); + + g.translate( x, y ); + + // fill background + g.setColor( Color.white ); + g.fillRect( x, y, width, height ); + + // paint horizontal lines + g.setColor( Color.LIGHT_GRAY ); + for( int i = 1; i < 10; i++ ) { + int hy = (height * i) / 10; + g.drawLine( 0, hy, width, hy ); + } + + // paint vertical lines + g.setColor( Color.LIGHT_GRAY ); + for( int i = secondWidth; i < width; i += secondWidth ) + g.drawLine( i, 0, i, height ); + + // paint lines + for( Map.Entry> e : color2dataMap.entrySet() ) { + Color chartColor = e.getKey(); + List chartData = e.getValue(); + + long seqTime = 0; + int seqX = 0; + long ptime = 0; + int px = 0; + int py = 0; + int pcount = 0; + + g.setColor( chartColor ); + + int size = chartData.size(); + for( int i = 0; i < size; i++ ) { + Data data = chartData.get( i ); + int dy = (int) ((height - 1) * data.value); + + if( data.time > ptime + NEW_SEQUENCE_TIME_LAG ) { + if( i > 0 && pcount == 0 ) + g.drawLine( px, py, px + (int) (4 * scaleFactor), py ); + + // start new sequence + seqTime = data.time; + seqX = (i > 0) ? px + seqGapWidth : 0; + px = seqX; + pcount = 0; + } else { + // line in sequence + int dx = (int) (seqX + (((data.time - seqTime) / 1000.) * secondWidth)); + g.drawLine( px, py, dx, dy ); + px = dx; + pcount++; + } + + py = dy; + ptime = data.time; + } + } + } + + private int chartWidth() { + int width = 0; + for( List chartData : color2dataMap.values() ) + width = Math.max( width, chartWidth( chartData, null ) ); + return width; + } + + private int chartWidth( List chartData, int[] lastSeqX ) { + long seqTime = 0; + int seqX = 0; + long ptime = 0; + int px = 0; + + int size = chartData.size(); + for( int i = 0; i < size; i++ ) { + Data data = chartData.get( i ); + + if( data.time > ptime + NEW_SEQUENCE_TIME_LAG ) { + // start new sequence + seqTime = data.time; + seqX = (i > 0) ? px + NEW_SEQUENCE_GAP : 0; + px = seqX; + } else { + // line in sequence + int dx = (int) (seqX + (((data.time - seqTime) / 1000.) * SECOND_WIDTH)); + 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 SECOND_WIDTH; + } + + @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; + } + } + + //---- class ColorIcon ---------------------------------------------------- + + private static class ColorIcon + implements Icon + { + private final Color color; + + ColorIcon( Color color ) { + this.color = color; + } + + @Override + public void paintIcon( Component c, Graphics g, int x, int y ) { + int width = getIconWidth(); + int height = getIconHeight(); + + g.setColor( color ); + g.fillRect( x, y, width, height ); + } + + @Override + public int getIconWidth() { + return UIScale.scale( 24 ); + } + + @Override + public int getIconHeight() { + return UIScale.scale( 12 ); + } + + } } diff --git a/flatlaf-testing/src/main/java/com/formdev/flatlaf/testing/FlatSmoothScrollingTest.jfd b/flatlaf-testing/src/main/java/com/formdev/flatlaf/testing/FlatSmoothScrollingTest.jfd index cdeac49e3..f6eb30b1d 100644 --- a/flatlaf-testing/src/main/java/com/formdev/flatlaf/testing/FlatSmoothScrollingTest.jfd +++ b/flatlaf-testing/src/main/java/com/formdev/flatlaf/testing/FlatSmoothScrollingTest.jfd @@ -6,7 +6,7 @@ new FormModel { add( new FormContainer( "com.formdev.flatlaf.testing.FlatTestPanel", new FormLayoutManager( class net.miginfocom.swing.MigLayout ) { "$layoutConstraints": "ltr,insets dialog,hidemode 3" "$columnConstraints": "[200,fill][200,fill][200,fill][200,fill]" - "$rowConstraints": "[][][200,grow,fill][][200,grow,fill]" + "$rowConstraints": "[][][200,grow,fill][][200,grow,fill][300,grow,fill]" } ) { name: "this" add( new FormComponent( "javax.swing.JCheckBox" ) { @@ -21,18 +21,21 @@ new FormModel { add( new FormComponent( "javax.swing.JLabel" ) { name: "listLabel" "text": "JList:" + "horizontalTextPosition": 10 }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { "value": "cell 0 1,aligny top,growy 0" } ) add( new FormComponent( "javax.swing.JLabel" ) { - name: "label1" + name: "treeLabel" "text": "JTree:" + "horizontalTextPosition": 10 }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { "value": "cell 1 1" } ) add( new FormComponent( "javax.swing.JLabel" ) { - name: "label5" + name: "tableLabel" "text": "JTable:" + "horizontalTextPosition": 10 }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { "value": "cell 2 1" } ) @@ -65,20 +68,23 @@ new FormModel { "value": "cell 2 2 2 1,width 100,height 100" } ) add( new FormComponent( "javax.swing.JLabel" ) { - name: "label2" + name: "textAreaLabel" "text": "JTextArea:" + "horizontalTextPosition": 10 }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { "value": "cell 0 3" } ) add( new FormComponent( "javax.swing.JLabel" ) { - name: "label3" + name: "textPaneLabel" "text": "JTextPane:" + "horizontalTextPosition": 10 }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { "value": "cell 1 3" } ) add( new FormComponent( "javax.swing.JLabel" ) { - name: "label4" + name: "editorPaneLabel" "text": "JEditorPane:" + "horizontalTextPosition": 10 }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { "value": "cell 2 3" } ) @@ -106,6 +112,39 @@ new FormModel { }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { "value": "cell 2 4" } ) + add( new FormContainer( "javax.swing.JPanel", new FormLayoutManager( class net.miginfocom.swing.MigLayout ) { + "$columnConstraints": "[200,right]" + "$rowConstraints": "[][]" + "$layoutConstraints": "ltr,insets 0,hidemode 3,aligny bottom" + } ) { + name: "panel1" + add( new FormComponent( "javax.swing.JCheckBox" ) { + name: "updateChartDelayedCheckBox" + "text": "Update chart delayed" + "mnemonic": 85 + "selected": true + addEvent( new FormEvent( "java.awt.event.ActionListener", "actionPerformed", "updateChartDelayedChanged", false ) ) + }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { + "value": "cell 0 0" + } ) + add( new FormComponent( "javax.swing.JButton" ) { + name: "clearChartButton" + "text": "Clear Chart" + addEvent( new FormEvent( "java.awt.event.ActionListener", "actionPerformed", "clearChart", false ) ) + }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { + "value": "cell 0 1" + } ) + }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { + "value": "cell 3 4" + } ) + add( new FormContainer( "javax.swing.JScrollPane", new FormLayoutManager( class javax.swing.JScrollPane ) ) { + name: "scrollPane1" + add( new FormComponent( "com.formdev.flatlaf.testing.FlatSmoothScrollingTest$LineChartPanel" ) { + name: "lineChartPanel" + } ) + }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { + "value": "cell 0 5 4 1,width 100" + } ) }, new FormLayoutConstraints( null ) { "location": new java.awt.Point( 0, 0 ) "size": new java.awt.Dimension( 790, 715 ) From d64a8e93e1a7b4989d7216e25a55dddb6425d307 Mon Sep 17 00:00:00 2001 From: Karl Tauber Date: Mon, 10 Aug 2020 23:17:06 +0200 Subject: [PATCH 12/30] FlatSmoothScrollingTest: - use ChangeListener instead of AdjustmentListener because this is invoked before all other scrollbar listeners (which may run 20-30ms) and avoids a delay in the line chart - use System.nanoTime() instead of System.currentTimeMillis() for better precision - paint vertical lines in chart at every 200ms (was 1sec) - print elapsed time between scrollbar events --- .../testing/FlatSmoothScrollingTest.java | 80 +++++++++---------- 1 file changed, 37 insertions(+), 43 deletions(-) diff --git a/flatlaf-testing/src/main/java/com/formdev/flatlaf/testing/FlatSmoothScrollingTest.java b/flatlaf-testing/src/main/java/com/formdev/flatlaf/testing/FlatSmoothScrollingTest.java index 3c8dabb47..410ad0118 100644 --- a/flatlaf-testing/src/main/java/com/formdev/flatlaf/testing/FlatSmoothScrollingTest.java +++ b/flatlaf-testing/src/main/java/com/formdev/flatlaf/testing/FlatSmoothScrollingTest.java @@ -23,8 +23,6 @@ import java.awt.Graphics2D; import java.awt.Point; import java.awt.Rectangle; -import java.awt.event.AdjustmentEvent; -import java.awt.event.AdjustmentListener; import java.util.ArrayList; import java.util.Arrays; import java.util.HashMap; @@ -32,6 +30,8 @@ import java.util.Map; import java.util.stream.Collectors; import javax.swing.*; +import javax.swing.event.ChangeEvent; +import javax.swing.event.ChangeListener; import javax.swing.table.AbstractTableModel; import javax.swing.tree.*; import com.formdev.flatlaf.ui.FlatUIUtils; @@ -74,23 +74,23 @@ public static void main( String[] args ) { textPaneLabel.setIcon( new ColorIcon( Color.cyan.darker() ) ); editorPaneLabel.setIcon( new ColorIcon( Color.orange.darker() ) ); - listScrollPane.getVerticalScrollBar().addAdjustmentListener( new AdjustmentHandler( "list vert", Color.red.darker() ) ); - listScrollPane.getHorizontalScrollBar().addAdjustmentListener( new AdjustmentHandler( "list horz", Color.red ) ); + listScrollPane.getVerticalScrollBar().getModel().addChangeListener( new ScrollBarChangeHandler( "list vert", Color.red.darker() ) ); + listScrollPane.getHorizontalScrollBar().getModel().addChangeListener( new ScrollBarChangeHandler( "list horz", Color.red ) ); - treeScrollPane.getVerticalScrollBar().addAdjustmentListener( new AdjustmentHandler( "tree vert", Color.blue.darker() ) ); - treeScrollPane.getHorizontalScrollBar().addAdjustmentListener( new AdjustmentHandler( "tree horz", Color.blue ) ); + treeScrollPane.getVerticalScrollBar().getModel().addChangeListener( new ScrollBarChangeHandler( "tree vert", Color.blue.darker() ) ); + treeScrollPane.getHorizontalScrollBar().getModel().addChangeListener( new ScrollBarChangeHandler( "tree horz", Color.blue ) ); - tableScrollPane.getVerticalScrollBar().addAdjustmentListener( new AdjustmentHandler( "table vert", Color.green.darker() ) ); - tableScrollPane.getHorizontalScrollBar().addAdjustmentListener( new AdjustmentHandler( "table horz", Color.green ) ); + tableScrollPane.getVerticalScrollBar().getModel().addChangeListener( new ScrollBarChangeHandler( "table vert", Color.green.darker() ) ); + tableScrollPane.getHorizontalScrollBar().getModel().addChangeListener( new ScrollBarChangeHandler( "table horz", Color.green ) ); - textAreaScrollPane.getVerticalScrollBar().addAdjustmentListener( new AdjustmentHandler( "textArea vert", Color.magenta.darker() ) ); - textAreaScrollPane.getHorizontalScrollBar().addAdjustmentListener( new AdjustmentHandler( "textArea horz", Color.magenta ) ); + textAreaScrollPane.getVerticalScrollBar().getModel().addChangeListener( new ScrollBarChangeHandler( "textArea vert", Color.magenta.darker() ) ); + textAreaScrollPane.getHorizontalScrollBar().getModel().addChangeListener( new ScrollBarChangeHandler( "textArea horz", Color.magenta ) ); - textPaneScrollPane.getVerticalScrollBar().addAdjustmentListener( new AdjustmentHandler( "textPane vert", Color.cyan.darker() ) ); - textPaneScrollPane.getHorizontalScrollBar().addAdjustmentListener( new AdjustmentHandler( "textPane horz", Color.cyan ) ); + textPaneScrollPane.getVerticalScrollBar().getModel().addChangeListener( new ScrollBarChangeHandler( "textPane vert", Color.cyan.darker() ) ); + textPaneScrollPane.getHorizontalScrollBar().getModel().addChangeListener( new ScrollBarChangeHandler( "textPane horz", Color.cyan ) ); - editorPaneScrollPane.getVerticalScrollBar().addAdjustmentListener( new AdjustmentHandler( "editorPane vert", Color.orange.darker() ) ); - editorPaneScrollPane.getHorizontalScrollBar().addAdjustmentListener( new AdjustmentHandler( "editorPane horz", Color.orange ) ); + editorPaneScrollPane.getVerticalScrollBar().getModel().addChangeListener( new ScrollBarChangeHandler( "editorPane vert", Color.orange.darker() ) ); + editorPaneScrollPane.getHorizontalScrollBar().getModel().addChangeListener( new ScrollBarChangeHandler( "editorPane horz", Color.orange ) ); ArrayList items = new ArrayList<>(); for( char ch = '0'; ch < 'z'; ch++ ) { @@ -342,48 +342,41 @@ private void initComponents() { private FlatSmoothScrollingTest.LineChartPanel lineChartPanel; // JFormDesigner - End of variables declaration //GEN-END:variables - //---- class AdjustmentHandler -------------------------------------------- + //---- class ScrollBarChangeHandler --------------------------------------- - private class AdjustmentHandler - implements AdjustmentListener + private class ScrollBarChangeHandler + implements ChangeListener { private final String name; private final Color chartColor; private int count; + private long lastTime; - AdjustmentHandler( String name ) { - this( name, null ); - } - - AdjustmentHandler( String name, Color chartColor ) { + ScrollBarChangeHandler( String name, Color chartColor ) { this.name = name; this.chartColor = chartColor; } @Override - public void adjustmentValueChanged( AdjustmentEvent e ) { + public void stateChanged( ChangeEvent e ) { + DefaultBoundedRangeModel m = (DefaultBoundedRangeModel) e.getSource(); + int value = m.getValue(); + boolean valueIsAdjusting = m.getValueIsAdjusting(); + if( chartColor != null ) { - JScrollBar sb = (JScrollBar) e.getSource(); - double value = (double) (e.getValue() - sb.getMinimum()) / (double) (sb.getMaximum() - sb.getVisibleAmount()); - lineChartPanel.addValue( value, chartColor ); + double chartValue = (double) (value - m.getMinimum()) / (double) (m.getMaximum() - m.getExtent()); + lineChartPanel.addValue( chartValue, chartColor ); } - System.out.printf( "%s (%d): %s %3d %b%n", + long t = System.nanoTime() / 1000000; + + System.out.printf( "%s (%d): %4d %3d ms %b%n", name, ++count, - adjustmentType2Str( e.getAdjustmentType() ), - e.getValue(), - e.getValueIsAdjusting() ); - } - - private String adjustmentType2Str( int adjustmentType ) { - switch( adjustmentType ) { - case AdjustmentEvent.UNIT_INCREMENT: return "UNIT_INCREMENT"; - case AdjustmentEvent.UNIT_DECREMENT: return "UNIT_DECREMENT"; - case AdjustmentEvent.BLOCK_INCREMENT: return "BLOCK_INCREMENT"; - case AdjustmentEvent.BLOCK_DECREMENT: return "BLOCK_DECREMENT"; - case AdjustmentEvent.TRACK: return "TRACK"; - default: return "unknown type"; - } + value, + t - lastTime, + valueIsAdjusting ); + + lastTime = t; } } @@ -439,7 +432,7 @@ private static class Data { void addValue( double value, Color chartColor ) { List chartData = color2dataMap.computeIfAbsent( chartColor, k -> new ArrayList<>() ); - chartData.add( new Data( value, System.currentTimeMillis()) ); + chartData.add( new Data( value, System.nanoTime() / 1000000) ); lastUsedChartColor = chartColor; @@ -505,7 +498,8 @@ private void paintImpl( Graphics2D g, int x, int y, int width, int height, doubl // paint vertical lines g.setColor( Color.LIGHT_GRAY ); - for( int i = secondWidth; i < width; i += secondWidth ) + int twoHundredMillisWidth = secondWidth / 5; + for( int i = twoHundredMillisWidth; i < width; i += twoHundredMillisWidth ) g.drawLine( i, 0, i, height ); // paint lines From 1ae31588c4560325c8e4fc09729d36ce9ef5d073 Mon Sep 17 00:00:00 2001 From: Karl Tauber Date: Tue, 11 Aug 2020 00:25:55 +0200 Subject: [PATCH 13/30] FlatSmoothScrollingTest: paint "temporary" scrollbar values in line chart using a lighter color --- .../testing/FlatSmoothScrollingTest.java | 23 ++++++++++++++++++- 1 file changed, 22 insertions(+), 1 deletion(-) diff --git a/flatlaf-testing/src/main/java/com/formdev/flatlaf/testing/FlatSmoothScrollingTest.java b/flatlaf-testing/src/main/java/com/formdev/flatlaf/testing/FlatSmoothScrollingTest.java index 410ad0118..6967b3380 100644 --- a/flatlaf-testing/src/main/java/com/formdev/flatlaf/testing/FlatSmoothScrollingTest.java +++ b/flatlaf-testing/src/main/java/com/formdev/flatlaf/testing/FlatSmoothScrollingTest.java @@ -350,7 +350,7 @@ private class ScrollBarChangeHandler private final String name; private final Color chartColor; private int count; - private long lastTime; + private long lastTime = System.nanoTime() / 1000000; ScrollBarChangeHandler( String name, Color chartColor ) { this.name = name; @@ -416,6 +416,12 @@ private static class Data { this.value = value; this.time = time; } + + @Override + public String toString() { + // for debugging + return String.valueOf( value ); + } } private final Map> color2dataMap = new HashMap<>(); @@ -506,6 +512,7 @@ private void paintImpl( Graphics2D g, int x, int y, int width, int height, doubl for( Map.Entry> e : color2dataMap.entrySet() ) { Color chartColor = e.getKey(); List chartData = e.getValue(); + Color temporaryValueColor = new Color( (chartColor.getRGB() & 0xffffff) | 0x40000000, true ); long seqTime = 0; int seqX = 0; @@ -531,11 +538,18 @@ private void paintImpl( Graphics2D g, int x, int y, int width, int height, doubl px = seqX; pcount = 0; } else { + boolean isTemporaryValue = isTemporaryValue( chartData, i ) || isTemporaryValue( chartData, i - 1 ); + if( isTemporaryValue ) + g.setColor( temporaryValueColor ); + // line in sequence int dx = (int) (seqX + (((data.time - seqTime) / 1000.) * secondWidth)); g.drawLine( px, py, dx, dy ); px = dx; pcount++; + + if( isTemporaryValue ) + g.setColor( chartColor ); } py = dy; @@ -544,6 +558,13 @@ private void paintImpl( Graphics2D g, int x, int y, int width, int height, doubl } } + private boolean isTemporaryValue( List chartData, int i ) { + if( i == 0 || i == chartData.size() - 1 ) + return false; + + return chartData.get( i - 1 ).value == chartData.get( i + 1 ).value; + } + private int chartWidth() { int width = 0; for( List chartData : color2dataMap.values() ) From 305e9e602e82c8eae98237e702b463a6c666f024 Mon Sep 17 00:00:00 2001 From: Karl Tauber Date: Tue, 11 Aug 2020 10:13:55 +0200 Subject: [PATCH 14/30] ScrollBar: fixed jittery scrolling when in repeating mode (hold down mouse button) and smooth scrolling enabled --- .../java/com/formdev/flatlaf/ui/FlatScrollBarUI.java | 4 ++-- .../flatlaf/testing/FlatSmoothScrollingTest.java | 11 ++++++++++- 2 files changed, 12 insertions(+), 3 deletions(-) diff --git a/flatlaf-core/src/main/java/com/formdev/flatlaf/ui/FlatScrollBarUI.java b/flatlaf-core/src/main/java/com/formdev/flatlaf/ui/FlatScrollBarUI.java index d25103ed9..87d43bc27 100644 --- a/flatlaf-core/src/main/java/com/formdev/flatlaf/ui/FlatScrollBarUI.java +++ b/flatlaf-core/src/main/java/com/formdev/flatlaf/ui/FlatScrollBarUI.java @@ -477,13 +477,13 @@ public void runAndSetValueAnimated( Runnable r ) { if( useValueIsAdjusting ) scrollbar.setValueIsAdjusting( true ); + int oldValue = scrollbar.getValue(); + // if invoked while animation is running, calculation of new value // should start at the previous target value if( targetValue != Integer.MIN_VALUE ) scrollbar.setValue( targetValue ); - int oldValue = scrollbar.getValue(); - r.run(); // do not use animation if started dragging thumb diff --git a/flatlaf-testing/src/main/java/com/formdev/flatlaf/testing/FlatSmoothScrollingTest.java b/flatlaf-testing/src/main/java/com/formdev/flatlaf/testing/FlatSmoothScrollingTest.java index 6967b3380..7080d86e4 100644 --- a/flatlaf-testing/src/main/java/com/formdev/flatlaf/testing/FlatSmoothScrollingTest.java +++ b/flatlaf-testing/src/main/java/com/formdev/flatlaf/testing/FlatSmoothScrollingTest.java @@ -558,11 +558,20 @@ private void paintImpl( Graphics2D g, int x, int y, int width, int height, doubl } } + /** + * One or two values between two equal values are considered "temporary", + * which means that they are the target value for the following scroll animation. + */ private boolean isTemporaryValue( List chartData, int i ) { if( i == 0 || i == chartData.size() - 1 ) return false; - return chartData.get( i - 1 ).value == chartData.get( i + 1 ).value; + double valueBefore = chartData.get( i - 1 ).value; + double valueAfter = chartData.get( i + 1 ).value; + + return valueBefore == valueAfter || + (i < chartData.size() - 2 && valueBefore == chartData.get( i + 2 ).value) || + (i > 1 && chartData.get( i - 2 ).value == valueAfter); } private int chartWidth() { From 1f2622819a2b4516e444a29b7494e51a0d546ee7 Mon Sep 17 00:00:00 2001 From: Karl Tauber Date: Tue, 11 Aug 2020 16:31:26 +0200 Subject: [PATCH 15/30] FlatSmoothScrollingTest: support dark themes and added "Show table grid" and "Auto-resize mode" check boxes --- .../testing/FlatSmoothScrollingTest.java | 63 ++++++++++++++++--- .../testing/FlatSmoothScrollingTest.jfd | 27 ++++++-- 2 files changed, 76 insertions(+), 14 deletions(-) diff --git a/flatlaf-testing/src/main/java/com/formdev/flatlaf/testing/FlatSmoothScrollingTest.java b/flatlaf-testing/src/main/java/com/formdev/flatlaf/testing/FlatSmoothScrollingTest.java index 7080d86e4..23b29bd4a 100644 --- a/flatlaf-testing/src/main/java/com/formdev/flatlaf/testing/FlatSmoothScrollingTest.java +++ b/flatlaf-testing/src/main/java/com/formdev/flatlaf/testing/FlatSmoothScrollingTest.java @@ -19,6 +19,7 @@ import java.awt.Color; import java.awt.Component; import java.awt.Dimension; +import java.awt.EventQueue; import java.awt.Graphics; import java.awt.Graphics2D; import java.awt.Point; @@ -34,7 +35,9 @@ import javax.swing.event.ChangeListener; import javax.swing.table.AbstractTableModel; import javax.swing.tree.*; +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.*; @@ -167,6 +170,27 @@ private void updateChartDelayedChanged() { lineChartPanel.setUpdateDelayed( updateChartDelayedCheckBox.isSelected() ); } + private void showTableGridChanged() { + boolean showGrid = showTableGridCheckBox.isSelected(); + table.setShowHorizontalLines( showGrid ); + table.setShowVerticalLines( showGrid ); + table.setIntercellSpacing( showGrid ? new Dimension( 1, 1 ) : new Dimension() ); + table.setGridColor( Color.gray ); + } + + private void autoResizeModeChanged() { + table.setAutoResizeMode( autoResizeModeCheckBox.isSelected() ? JTable.AUTO_RESIZE_SUBSEQUENT_COLUMNS : JTable.AUTO_RESIZE_OFF ); + } + + @Override + public void updateUI() { + super.updateUI(); + + EventQueue.invokeLater( () -> { + showTableGridChanged(); + } ); + } + private void initComponents() { // JFormDesigner - Component initialization - DO NOT MODIFY //GEN-BEGIN:initComponents smoothScrollingCheckBox = new JCheckBox(); @@ -189,6 +213,8 @@ private void initComponents() { editorPaneScrollPane = new FlatSmoothScrollingTest.DebugScrollPane(); editorPane = new JEditorPane(); panel1 = new JPanel(); + showTableGridCheckBox = new JCheckBox(); + autoResizeModeCheckBox = new JCheckBox(); updateChartDelayedCheckBox = new JCheckBox(); clearChartButton = new JButton(); scrollPane1 = new JScrollPane(); @@ -286,26 +312,42 @@ private void initComponents() { //======== panel1 ======== { panel1.setLayout(new MigLayout( - "ltr,insets 0,hidemode 3,aligny bottom", + "ltr,insets 0,hidemode 3", // columns "[200,right]", // rows + "[]0" + + "[]" + + "[grow]" + "[]" + "[]")); + //---- showTableGridCheckBox ---- + showTableGridCheckBox.setText("Show table grid"); + showTableGridCheckBox.setMnemonic('G'); + showTableGridCheckBox.addActionListener(e -> showTableGridChanged()); + panel1.add(showTableGridCheckBox, "cell 0 0"); + + //---- autoResizeModeCheckBox ---- + autoResizeModeCheckBox.setText("Auto-resize mode"); + autoResizeModeCheckBox.setSelected(true); + autoResizeModeCheckBox.addActionListener(e -> autoResizeModeChanged()); + panel1.add(autoResizeModeCheckBox, "cell 0 1"); + //---- updateChartDelayedCheckBox ---- updateChartDelayedCheckBox.setText("Update chart delayed"); updateChartDelayedCheckBox.setMnemonic('U'); updateChartDelayedCheckBox.setSelected(true); updateChartDelayedCheckBox.addActionListener(e -> updateChartDelayedChanged()); - panel1.add(updateChartDelayedCheckBox, "cell 0 0"); + panel1.add(updateChartDelayedCheckBox, "cell 0 3"); //---- clearChartButton ---- clearChartButton.setText("Clear Chart"); + clearChartButton.setMnemonic('C'); clearChartButton.addActionListener(e -> clearChart()); - panel1.add(clearChartButton, "cell 0 1"); + panel1.add(clearChartButton, "cell 0 4"); } - add(panel1, "cell 3 4"); + add(panel1, "cell 3 3 1 2,growy"); //======== scrollPane1 ======== { @@ -336,6 +378,8 @@ private void initComponents() { private FlatSmoothScrollingTest.DebugScrollPane editorPaneScrollPane; private JEditorPane editorPane; private JPanel panel1; + private JCheckBox showTableGridCheckBox; + private JCheckBox autoResizeModeCheckBox; private JCheckBox updateChartDelayedCheckBox; private JButton clearChartButton; private JScrollPane scrollPane1; @@ -350,7 +394,7 @@ private class ScrollBarChangeHandler private final String name; private final Color chartColor; private int count; - private long lastTime = System.nanoTime() / 1000000; + private long lastTime; ScrollBarChangeHandler( String name, Color chartColor ) { this.name = name; @@ -492,26 +536,27 @@ private void paintImpl( Graphics2D g, int x, int y, int width, int height, doubl g.translate( x, y ); // fill background - g.setColor( Color.white ); + g.setColor( UIManager.getColor( "Table.background" ) ); g.fillRect( x, y, width, height ); // paint horizontal lines - g.setColor( Color.LIGHT_GRAY ); + g.setColor( UIManager.getColor( "Component.borderColor" ) ); for( int i = 1; i < 10; i++ ) { int hy = (height * i) / 10; g.drawLine( 0, hy, width, hy ); } // paint vertical lines - g.setColor( Color.LIGHT_GRAY ); int twoHundredMillisWidth = secondWidth / 5; for( int i = twoHundredMillisWidth; i < width; i += twoHundredMillisWidth ) g.drawLine( i, 0, i, height ); // paint lines for( Map.Entry> e : color2dataMap.entrySet() ) { - Color chartColor = e.getKey(); List chartData = e.getValue(); + Color chartColor = e.getKey(); + if( FlatLaf.isLafDark() ) + chartColor = new HSLColor( chartColor ).adjustTone( 50 ); Color temporaryValueColor = new Color( (chartColor.getRGB() & 0xffffff) | 0x40000000, true ); long seqTime = 0; diff --git a/flatlaf-testing/src/main/java/com/formdev/flatlaf/testing/FlatSmoothScrollingTest.jfd b/flatlaf-testing/src/main/java/com/formdev/flatlaf/testing/FlatSmoothScrollingTest.jfd index f6eb30b1d..017fd97e4 100644 --- a/flatlaf-testing/src/main/java/com/formdev/flatlaf/testing/FlatSmoothScrollingTest.jfd +++ b/flatlaf-testing/src/main/java/com/formdev/flatlaf/testing/FlatSmoothScrollingTest.jfd @@ -114,10 +114,26 @@ new FormModel { } ) add( new FormContainer( "javax.swing.JPanel", new FormLayoutManager( class net.miginfocom.swing.MigLayout ) { "$columnConstraints": "[200,right]" - "$rowConstraints": "[][]" - "$layoutConstraints": "ltr,insets 0,hidemode 3,aligny bottom" + "$rowConstraints": "[]0[][grow][][]" + "$layoutConstraints": "ltr,insets 0,hidemode 3" } ) { name: "panel1" + add( new FormComponent( "javax.swing.JCheckBox" ) { + name: "showTableGridCheckBox" + "text": "Show table grid" + "mnemonic": 71 + addEvent( new FormEvent( "java.awt.event.ActionListener", "actionPerformed", "showTableGridChanged", false ) ) + }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { + "value": "cell 0 0" + } ) + add( new FormComponent( "javax.swing.JCheckBox" ) { + name: "autoResizeModeCheckBox" + "text": "Auto-resize mode" + "selected": true + addEvent( new FormEvent( "java.awt.event.ActionListener", "actionPerformed", "autoResizeModeChanged", false ) ) + }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { + "value": "cell 0 1" + } ) add( new FormComponent( "javax.swing.JCheckBox" ) { name: "updateChartDelayedCheckBox" "text": "Update chart delayed" @@ -125,17 +141,18 @@ new FormModel { "selected": true addEvent( new FormEvent( "java.awt.event.ActionListener", "actionPerformed", "updateChartDelayedChanged", false ) ) }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { - "value": "cell 0 0" + "value": "cell 0 3" } ) 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" + "value": "cell 0 4" } ) }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { - "value": "cell 3 4" + "value": "cell 3 3 1 2,growy" } ) add( new FormContainer( "javax.swing.JScrollPane", new FormLayoutManager( class javax.swing.JScrollPane ) ) { name: "scrollPane1" From 357318802528a4d3a4b508eeec275ef2bd776a41 Mon Sep 17 00:00:00 2001 From: Karl Tauber Date: Wed, 12 Aug 2020 16:13:09 +0200 Subject: [PATCH 16/30] ScrollBar: support smooth scrolling via keyboard --- .../formdev/flatlaf/ui/FlatScrollBarUI.java | 48 +++++++++++++------ .../formdev/flatlaf/ui/FlatScrollPaneUI.java | 43 +++++++++++++++++ 2 files changed, 77 insertions(+), 14 deletions(-) diff --git a/flatlaf-core/src/main/java/com/formdev/flatlaf/ui/FlatScrollBarUI.java b/flatlaf-core/src/main/java/com/formdev/flatlaf/ui/FlatScrollBarUI.java index 87d43bc27..f9aef8e62 100644 --- a/flatlaf-core/src/main/java/com/formdev/flatlaf/ui/FlatScrollBarUI.java +++ b/flatlaf-core/src/main/java/com/formdev/flatlaf/ui/FlatScrollBarUI.java @@ -477,6 +477,7 @@ public void runAndSetValueAnimated( Runnable r ) { if( useValueIsAdjusting ) scrollbar.setValueIsAdjusting( true ); + // remember current scrollbar value so that we can start scroll animation from there int oldValue = scrollbar.getValue(); // if invoked while animation is running, calculation of new value @@ -488,31 +489,51 @@ public void runAndSetValueAnimated( Runnable r ) { // do not use animation if started dragging thumb if( isDragging ) { + // do not clear valueIsAdjusting here inRunAndSetValueAnimated = false; return; } int newValue = scrollbar.getValue(); if( newValue != oldValue ) { - scrollbar.setValue( oldValue ); - - setValueAnimated( newValue ); - } else if( useValueIsAdjusting ) - scrollbar.setValueIsAdjusting( false ); + // start scroll animation if value has changed + setValueAnimated( oldValue, newValue ); + } else { + // clear valueIsAdjusting if value has not changed + if( useValueIsAdjusting ) + scrollbar.setValueIsAdjusting( false ); + } inRunAndSetValueAnimated = false; } private boolean inRunAndSetValueAnimated; private Animator animator; + private int startValue = Integer.MIN_VALUE; private int targetValue = Integer.MIN_VALUE; - private int delta; private boolean useValueIsAdjusting = true; - public void setValueAnimated( int value ) { + public void setValueAnimated( int initialValue, int value ) { + // do some check if animation already running + if( animator != null && animator.isRunning() && targetValue != Integer.MIN_VALUE ) { + // ignore requests if animation still running and scroll direction is the same + // and new value is within currently running animation + // (this may occur when repeat-scrolling via keyboard) + if( value == targetValue || + (value > startValue && value < targetValue) || // scroll down/right + (value < startValue && value > targetValue) ) // scroll up/left + return; + } + if( useValueIsAdjusting ) scrollbar.setValueIsAdjusting( true ); + // set scrollbar value to initial value + scrollbar.setValue( initialValue ); + + startValue = initialValue; + targetValue = value; + // create animator if( animator == null ) { int duration = FlatUIUtils.getUIInt( "ScrollPane.smoothScrolling.duration", 200 ); @@ -521,7 +542,7 @@ public void setValueAnimated( int value ) { animator = new Animator( duration, fraction -> { if( scrollbar == null || !scrollbar.isShowing() ) { - animator.cancel(); + animator.stop(); return; } @@ -530,10 +551,11 @@ public void setValueAnimated( int value ) { if( useValueIsAdjusting && !scrollbar.getValueIsAdjusting() ) scrollbar.setValueIsAdjusting( true ); - scrollbar.setValue( targetValue - delta + Math.round( delta * fraction ) ); + scrollbar.setValue( startValue + Math.round( (targetValue - startValue) * fraction ) ); }, () -> { - targetValue = Integer.MIN_VALUE; - if( useValueIsAdjusting ) + startValue = targetValue = Integer.MIN_VALUE; + + if( useValueIsAdjusting && scrollbar != null ) scrollbar.setValueIsAdjusting( false ); }); @@ -543,9 +565,7 @@ public void setValueAnimated( int value ) { : new CubicBezierEasing( 0.5f, 0.5f, 0.5f, 1 ) ); } - targetValue = value; - delta = targetValue - scrollbar.getValue(); - + // restart animator animator.cancel(); animator.start(); } diff --git a/flatlaf-core/src/main/java/com/formdev/flatlaf/ui/FlatScrollPaneUI.java b/flatlaf-core/src/main/java/com/formdev/flatlaf/ui/FlatScrollPaneUI.java index bcfb6efb9..c13781620 100644 --- a/flatlaf-core/src/main/java/com/formdev/flatlaf/ui/FlatScrollPaneUI.java +++ b/flatlaf-core/src/main/java/com/formdev/flatlaf/ui/FlatScrollPaneUI.java @@ -453,6 +453,49 @@ public static boolean isPermanentFocusOwner( JScrollPane scrollPane ) { return false; } + @Override + protected void syncScrollPaneWithViewport() { + if( isSmoothScrollingEnabled() ) { + runAndSyncScrollBarValueAnimated( scrollpane.getVerticalScrollBar(), 0, () -> { + runAndSyncScrollBarValueAnimated( scrollpane.getHorizontalScrollBar(), 1, () -> { + super.syncScrollPaneWithViewport(); + } ); + } ); + } else + super.syncScrollPaneWithViewport(); + } + + private void runAndSyncScrollBarValueAnimated( JScrollBar sb, int i, Runnable r ) { + if( inRunAndSyncValueAnimated[i] || sb == null ) { + r.run(); + return; + } + + inRunAndSyncValueAnimated[i] = true; + + int oldValue = sb.getValue(); + int oldVisibleAmount = sb.getVisibleAmount(); + int oldMinimum = sb.getMinimum(); + int oldMaximum = sb.getMaximum(); + + r.run(); + + int newValue = sb.getValue(); + + if( newValue != oldValue && + sb.getVisibleAmount() == oldVisibleAmount && + sb.getMinimum() == oldMinimum && + sb.getMaximum() == oldMaximum && + sb.getUI() instanceof FlatScrollBarUI ) + { + ((FlatScrollBarUI)sb.getUI()).setValueAnimated( oldValue, newValue ); + } + + inRunAndSyncValueAnimated[i] = false; + } + + private final boolean[] inRunAndSyncValueAnimated = new boolean[2]; + //---- class Handler ------------------------------------------------------ /** From 865a56875f52179b08d560010934ca1430c89386 Mon Sep 17 00:00:00 2001 From: Karl Tauber Date: Wed, 12 Aug 2020 17:31:55 +0200 Subject: [PATCH 17/30] FlatSmoothScrollingTest: added "custom" scroll pane for testing smooth scrolling in case that scroll view does not implement Scrollable interface --- .../testing/FlatSmoothScrollingTest.java | 127 +++++++++++------- .../testing/FlatSmoothScrollingTest.jfd | 95 +++++++------ 2 files changed, 130 insertions(+), 92 deletions(-) diff --git a/flatlaf-testing/src/main/java/com/formdev/flatlaf/testing/FlatSmoothScrollingTest.java b/flatlaf-testing/src/main/java/com/formdev/flatlaf/testing/FlatSmoothScrollingTest.java index 23b29bd4a..1aff8f4cc 100644 --- a/flatlaf-testing/src/main/java/com/formdev/flatlaf/testing/FlatSmoothScrollingTest.java +++ b/flatlaf-testing/src/main/java/com/formdev/flatlaf/testing/FlatSmoothScrollingTest.java @@ -76,6 +76,7 @@ public static void main( String[] args ) { textAreaLabel.setIcon( new ColorIcon( Color.magenta.darker() ) ); textPaneLabel.setIcon( new ColorIcon( Color.cyan.darker() ) ); editorPaneLabel.setIcon( new ColorIcon( Color.orange.darker() ) ); + customLabel.setIcon( new ColorIcon( Color.pink ) ); listScrollPane.getVerticalScrollBar().getModel().addChangeListener( new ScrollBarChangeHandler( "list vert", Color.red.darker() ) ); listScrollPane.getHorizontalScrollBar().getModel().addChangeListener( new ScrollBarChangeHandler( "list horz", Color.red ) ); @@ -95,6 +96,9 @@ public static void main( String[] args ) { editorPaneScrollPane.getVerticalScrollBar().getModel().addChangeListener( new ScrollBarChangeHandler( "editorPane vert", Color.orange.darker() ) ); editorPaneScrollPane.getHorizontalScrollBar().getModel().addChangeListener( new ScrollBarChangeHandler( "editorPane horz", Color.orange ) ); + customScrollPane.getVerticalScrollBar().getModel().addChangeListener( new ScrollBarChangeHandler( "custom vert", Color.pink ) ); + customScrollPane.getHorizontalScrollBar().getModel().addChangeListener( new ScrollBarChangeHandler( "custom horz", Color.pink.darker() ) ); + ArrayList items = new ArrayList<>(); for( char ch = '0'; ch < 'z'; ch++ ) { char[] chars = new char[ch - '0' + 1]; @@ -197,6 +201,8 @@ private void initComponents() { listLabel = new JLabel(); treeLabel = new JLabel(); tableLabel = new JLabel(); + showTableGridCheckBox = new JCheckBox(); + autoResizeModeCheckBox = new JCheckBox(); listScrollPane = new FlatSmoothScrollingTest.DebugScrollPane(); list = new JList<>(); treeScrollPane = new FlatSmoothScrollingTest.DebugScrollPane(); @@ -206,19 +212,20 @@ private void initComponents() { textAreaLabel = new JLabel(); textPaneLabel = new JLabel(); editorPaneLabel = new JLabel(); + customLabel = new JLabel(); textAreaScrollPane = new FlatSmoothScrollingTest.DebugScrollPane(); textArea = new JTextArea(); textPaneScrollPane = new FlatSmoothScrollingTest.DebugScrollPane(); textPane = new JTextPane(); editorPaneScrollPane = new FlatSmoothScrollingTest.DebugScrollPane(); editorPane = new JEditorPane(); - panel1 = new JPanel(); - showTableGridCheckBox = new JCheckBox(); - autoResizeModeCheckBox = new JCheckBox(); - updateChartDelayedCheckBox = new JCheckBox(); - clearChartButton = new JButton(); + customScrollPane = new JScrollPane(); + button1 = new JButton(); scrollPane1 = new JScrollPane(); lineChartPanel = new FlatSmoothScrollingTest.LineChartPanel(); + label1 = new JLabel(); + updateChartDelayedCheckBox = new JCheckBox(); + clearChartButton = new JButton(); //======== this ======== setLayout(new MigLayout( @@ -234,7 +241,8 @@ private void initComponents() { "[200,grow,fill]" + "[]" + "[200,grow,fill]" + - "[300,grow,fill]")); + "[300,grow,fill]" + + "[]")); //---- smoothScrollingCheckBox ---- smoothScrollingCheckBox.setText("Smooth scrolling"); @@ -256,7 +264,19 @@ private void initComponents() { //---- tableLabel ---- tableLabel.setText("JTable:"); tableLabel.setHorizontalTextPosition(SwingConstants.LEADING); - add(tableLabel, "cell 2 1"); + add(tableLabel, "cell 2 1 2 1"); + + //---- showTableGridCheckBox ---- + showTableGridCheckBox.setText("Show table grid"); + showTableGridCheckBox.setMnemonic('G'); + showTableGridCheckBox.addActionListener(e -> showTableGridChanged()); + add(showTableGridCheckBox, "cell 2 1 2 1,alignx right,growx 0"); + + //---- autoResizeModeCheckBox ---- + autoResizeModeCheckBox.setText("Auto-resize mode"); + autoResizeModeCheckBox.setSelected(true); + autoResizeModeCheckBox.addActionListener(e -> autoResizeModeChanged()); + add(autoResizeModeCheckBox, "cell 2 1 2 1,alignx right,growx 0"); //======== listScrollPane ======== { @@ -291,6 +311,10 @@ private void initComponents() { editorPaneLabel.setHorizontalTextPosition(SwingConstants.LEADING); add(editorPaneLabel, "cell 2 3"); + //---- customLabel ---- + customLabel.setText("Custom:"); + add(customLabel, "cell 3 3"); + //======== textAreaScrollPane ======== { textAreaScrollPane.setViewportView(textArea); @@ -309,51 +333,40 @@ private void initComponents() { } add(editorPaneScrollPane, "cell 2 4"); - //======== panel1 ======== + //======== customScrollPane ======== { - panel1.setLayout(new MigLayout( - "ltr,insets 0,hidemode 3", - // columns - "[200,right]", - // rows - "[]0" + - "[]" + - "[grow]" + - "[]" + - "[]")); - - //---- showTableGridCheckBox ---- - showTableGridCheckBox.setText("Show table grid"); - showTableGridCheckBox.setMnemonic('G'); - showTableGridCheckBox.addActionListener(e -> showTableGridChanged()); - panel1.add(showTableGridCheckBox, "cell 0 0"); - - //---- autoResizeModeCheckBox ---- - autoResizeModeCheckBox.setText("Auto-resize mode"); - autoResizeModeCheckBox.setSelected(true); - autoResizeModeCheckBox.addActionListener(e -> autoResizeModeChanged()); - panel1.add(autoResizeModeCheckBox, "cell 0 1"); - - //---- updateChartDelayedCheckBox ---- - updateChartDelayedCheckBox.setText("Update chart delayed"); - updateChartDelayedCheckBox.setMnemonic('U'); - updateChartDelayedCheckBox.setSelected(true); - updateChartDelayedCheckBox.addActionListener(e -> updateChartDelayedChanged()); - panel1.add(updateChartDelayedCheckBox, "cell 0 3"); - - //---- clearChartButton ---- - clearChartButton.setText("Clear Chart"); - clearChartButton.setMnemonic('C'); - clearChartButton.addActionListener(e -> clearChart()); - panel1.add(clearChartButton, "cell 0 4"); - } - add(panel1, "cell 3 3 1 2,growy"); + + //---- button1 ---- + button1.setText("I'm a large button, but do not implement Scrollable interface"); + button1.setPreferredSize(new Dimension(800, 800)); + button1.setHorizontalAlignment(SwingConstants.LEADING); + button1.setVerticalAlignment(SwingConstants.TOP); + customScrollPane.setViewportView(button1); + } + add(customScrollPane, "cell 3 4"); //======== scrollPane1 ======== { scrollPane1.setViewportView(lineChartPanel); } add(scrollPane1, "cell 0 5 4 1,width 100"); + + //---- label1 ---- + label1.setText("X: time (200ms per line) / Y: scroll bar value (10% per line)"); + add(label1, "cell 0 6 4 1"); + + //---- updateChartDelayedCheckBox ---- + updateChartDelayedCheckBox.setText("Update chart delayed"); + updateChartDelayedCheckBox.setMnemonic('U'); + updateChartDelayedCheckBox.setSelected(true); + updateChartDelayedCheckBox.addActionListener(e -> updateChartDelayedChanged()); + add(updateChartDelayedCheckBox, "cell 0 6 4 1,alignx right,growx 0"); + + //---- clearChartButton ---- + clearChartButton.setText("Clear Chart"); + clearChartButton.setMnemonic('C'); + clearChartButton.addActionListener(e -> clearChart()); + add(clearChartButton, "cell 0 6 4 1,alignx right,growx 0"); // JFormDesigner - End of component initialization //GEN-END:initComponents } @@ -362,6 +375,8 @@ private void initComponents() { private JLabel listLabel; private JLabel treeLabel; private JLabel tableLabel; + private JCheckBox showTableGridCheckBox; + private JCheckBox autoResizeModeCheckBox; private FlatSmoothScrollingTest.DebugScrollPane listScrollPane; private JList list; private FlatSmoothScrollingTest.DebugScrollPane treeScrollPane; @@ -371,19 +386,20 @@ private void initComponents() { private JLabel textAreaLabel; private JLabel textPaneLabel; private JLabel editorPaneLabel; + private JLabel customLabel; private FlatSmoothScrollingTest.DebugScrollPane textAreaScrollPane; private JTextArea textArea; private FlatSmoothScrollingTest.DebugScrollPane textPaneScrollPane; private JTextPane textPane; private FlatSmoothScrollingTest.DebugScrollPane editorPaneScrollPane; private JEditorPane editorPane; - private JPanel panel1; - private JCheckBox showTableGridCheckBox; - private JCheckBox autoResizeModeCheckBox; - private JCheckBox updateChartDelayedCheckBox; - private JButton clearChartButton; + private JScrollPane customScrollPane; + private JButton button1; private JScrollPane scrollPane1; private FlatSmoothScrollingTest.LineChartPanel lineChartPanel; + private JLabel label1; + private JCheckBox updateChartDelayedCheckBox; + private JButton clearChartButton; // JFormDesigner - End of variables declaration //GEN-END:variables //---- class ScrollBarChangeHandler --------------------------------------- @@ -533,6 +549,11 @@ private void paintImpl( Graphics2D g, int x, int y, int width, int height, doubl int secondWidth = (int) (SECOND_WIDTH * scaleFactor); int seqGapWidth = (int) (NEW_SEQUENCE_GAP * 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 @@ -540,16 +561,18 @@ private void paintImpl( Graphics2D g, int x, int y, int width, int height, doubl g.fillRect( x, y, width, height ); // paint horizontal lines - g.setColor( UIManager.getColor( "Component.borderColor" ) ); 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 twoHundredMillisWidth = secondWidth / 5; - for( int i = twoHundredMillisWidth; i < width; i += twoHundredMillisWidth ) + for( int i = twoHundredMillisWidth; i < width; i += twoHundredMillisWidth ) { + g.setColor( (i % secondWidth != 0) ? lineColor : lineColor2 ); g.drawLine( i, 0, i, height ); + } // paint lines for( Map.Entry> e : color2dataMap.entrySet() ) { diff --git a/flatlaf-testing/src/main/java/com/formdev/flatlaf/testing/FlatSmoothScrollingTest.jfd b/flatlaf-testing/src/main/java/com/formdev/flatlaf/testing/FlatSmoothScrollingTest.jfd index 017fd97e4..d70167661 100644 --- a/flatlaf-testing/src/main/java/com/formdev/flatlaf/testing/FlatSmoothScrollingTest.jfd +++ b/flatlaf-testing/src/main/java/com/formdev/flatlaf/testing/FlatSmoothScrollingTest.jfd @@ -6,7 +6,7 @@ new FormModel { add( new FormContainer( "com.formdev.flatlaf.testing.FlatTestPanel", new FormLayoutManager( class net.miginfocom.swing.MigLayout ) { "$layoutConstraints": "ltr,insets dialog,hidemode 3" "$columnConstraints": "[200,fill][200,fill][200,fill][200,fill]" - "$rowConstraints": "[][][200,grow,fill][][200,grow,fill][300,grow,fill]" + "$rowConstraints": "[][][200,grow,fill][][200,grow,fill][300,grow,fill][]" } ) { name: "this" add( new FormComponent( "javax.swing.JCheckBox" ) { @@ -37,7 +37,23 @@ new FormModel { "text": "JTable:" "horizontalTextPosition": 10 }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { - "value": "cell 2 1" + "value": "cell 2 1 2 1" + } ) + add( new FormComponent( "javax.swing.JCheckBox" ) { + name: "showTableGridCheckBox" + "text": "Show table grid" + "mnemonic": 71 + addEvent( new FormEvent( "java.awt.event.ActionListener", "actionPerformed", "showTableGridChanged", false ) ) + }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { + "value": "cell 2 1 2 1,alignx right,growx 0" + } ) + add( new FormComponent( "javax.swing.JCheckBox" ) { + name: "autoResizeModeCheckBox" + "text": "Auto-resize mode" + "selected": true + addEvent( new FormEvent( "java.awt.event.ActionListener", "actionPerformed", "autoResizeModeChanged", false ) ) + }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { + "value": "cell 2 1 2 1,alignx right,growx 0" } ) add( new FormContainer( "com.formdev.flatlaf.testing.FlatSmoothScrollingTest$DebugScrollPane", new FormLayoutManager( class javax.swing.JScrollPane ) ) { name: "listScrollPane" @@ -88,6 +104,12 @@ new FormModel { }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { "value": "cell 2 3" } ) + add( new FormComponent( "javax.swing.JLabel" ) { + name: "customLabel" + "text": "Custom:" + }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { + "value": "cell 3 3" + } ) add( new FormContainer( "com.formdev.flatlaf.testing.FlatSmoothScrollingTest$DebugScrollPane", new FormLayoutManager( class javax.swing.JScrollPane ) ) { name: "textAreaScrollPane" add( new FormComponent( "javax.swing.JTextArea" ) { @@ -112,47 +134,17 @@ new FormModel { }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { "value": "cell 2 4" } ) - add( new FormContainer( "javax.swing.JPanel", new FormLayoutManager( class net.miginfocom.swing.MigLayout ) { - "$columnConstraints": "[200,right]" - "$rowConstraints": "[]0[][grow][][]" - "$layoutConstraints": "ltr,insets 0,hidemode 3" - } ) { - name: "panel1" - add( new FormComponent( "javax.swing.JCheckBox" ) { - name: "showTableGridCheckBox" - "text": "Show table grid" - "mnemonic": 71 - addEvent( new FormEvent( "java.awt.event.ActionListener", "actionPerformed", "showTableGridChanged", false ) ) - }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { - "value": "cell 0 0" - } ) - add( new FormComponent( "javax.swing.JCheckBox" ) { - name: "autoResizeModeCheckBox" - "text": "Auto-resize mode" - "selected": true - addEvent( new FormEvent( "java.awt.event.ActionListener", "actionPerformed", "autoResizeModeChanged", false ) ) - }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { - "value": "cell 0 1" - } ) - add( new FormComponent( "javax.swing.JCheckBox" ) { - name: "updateChartDelayedCheckBox" - "text": "Update chart delayed" - "mnemonic": 85 - "selected": true - addEvent( new FormEvent( "java.awt.event.ActionListener", "actionPerformed", "updateChartDelayedChanged", false ) ) - }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { - "value": "cell 0 3" - } ) + add( new FormContainer( "javax.swing.JScrollPane", new FormLayoutManager( class javax.swing.JScrollPane ) ) { + name: "customScrollPane" 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 4" + name: "button1" + "text": "I'm a large button, but do not implement Scrollable interface" + "preferredSize": new java.awt.Dimension( 800, 800 ) + "horizontalAlignment": 10 + "verticalAlignment": 1 } ) }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { - "value": "cell 3 3 1 2,growy" + "value": "cell 3 4" } ) add( new FormContainer( "javax.swing.JScrollPane", new FormLayoutManager( class javax.swing.JScrollPane ) ) { name: "scrollPane1" @@ -162,6 +154,29 @@ new FormModel { }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { "value": "cell 0 5 4 1,width 100" } ) + add( new FormComponent( "javax.swing.JLabel" ) { + name: "label1" + "text": "X: time (200ms per line) / Y: scroll bar value (10% per line)" + }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { + "value": "cell 0 6 4 1" + } ) + add( new FormComponent( "javax.swing.JCheckBox" ) { + name: "updateChartDelayedCheckBox" + "text": "Update chart delayed" + "mnemonic": 85 + "selected": true + addEvent( new FormEvent( "java.awt.event.ActionListener", "actionPerformed", "updateChartDelayedChanged", false ) ) + }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { + "value": "cell 0 6 4 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 6 4 1,alignx right,growx 0" + } ) }, new FormLayoutConstraints( null ) { "location": new java.awt.Point( 0, 0 ) "size": new java.awt.Dimension( 790, 715 ) From 419a689ca47bacf1e002c70985dae821f3e21afa Mon Sep 17 00:00:00 2001 From: Karl Tauber Date: Fri, 9 Oct 2020 16:01:45 +0200 Subject: [PATCH 18/30] FlatAnimatorTest: added test for wheel scrolling (including chart) --- .../flatlaf/testing/FlatAnimatorTest.java | 121 ++++++++++++++++++ .../flatlaf/testing/FlatAnimatorTest.jfd | 58 ++++++++- .../testing/FlatSmoothScrollingTest.java | 51 ++++++-- 3 files changed, 216 insertions(+), 14 deletions(-) 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..a8456db4c 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 @@ -17,7 +17,12 @@ package com.formdev.flatlaf.testing; import java.awt.*; +import java.awt.event.MouseWheelEvent; +import java.awt.event.MouseWheelListener; import javax.swing.*; +import javax.swing.border.*; +import com.formdev.flatlaf.testing.FlatSmoothScrollingTest.LineChartPanel; +import com.formdev.flatlaf.ui.FlatUIUtils; import com.formdev.flatlaf.util.Animator; import com.formdev.flatlaf.util.CubicBezierEasing; import net.miginfocom.swing.*; @@ -40,6 +45,9 @@ public static void main( String[] args ) { FlatAnimatorTest() { initComponents(); + + lineChartPanel.setSecondWidth( 500 ); + mouseWheelTestPanel.lineChartPanel = lineChartPanel; } private void start() { @@ -72,6 +80,14 @@ private void startEaseInOut() { } } + private void updateChartDelayedChanged() { + lineChartPanel.setUpdateDelayed( updateChartDelayedCheckBox.isSelected() ); + } + + private void clearChart() { + lineChartPanel.clear(); + } + private void initComponents() { // JFormDesigner - Component initialization - DO NOT MODIFY //GEN-BEGIN:initComponents JLabel label1 = new JLabel(); @@ -79,6 +95,13 @@ private void initComponents() { JLabel label2 = new JLabel(); easeInOutScrollBar = new JScrollBar(); startButton = new JButton(); + JLabel label3 = new JLabel(); + mouseWheelTestPanel = new FlatAnimatorTest.MouseWheelTestPanel(); + JScrollPane scrollPane1 = new JScrollPane(); + lineChartPanel = new FlatSmoothScrollingTest.LineChartPanel(); + JLabel label4 = new JLabel(); + updateChartDelayedCheckBox = new JCheckBox(); + JButton clearChartButton = new JButton(); //======== this ======== setLayout(new MigLayout( @@ -89,6 +112,10 @@ private void initComponents() { // rows "[]" + "[]" + + "[]" + + "[]" + + "[top]" + + "[400,grow,fill]" + "[]")); //---- label1 ---- @@ -113,6 +140,37 @@ private void initComponents() { startButton.setText("Start"); startButton.addActionListener(e -> start()); add(startButton, "cell 0 2"); + + //---- label3 ---- + label3.setText("Mouse wheel test:"); + add(label3, "cell 0 4"); + + //---- mouseWheelTestPanel ---- + mouseWheelTestPanel.setBorder(new LineBorder(Color.red)); + add(mouseWheelTestPanel, "cell 1 4,height 100"); + + //======== scrollPane1 ======== + { + scrollPane1.setViewportView(lineChartPanel); + } + add(scrollPane1, "cell 0 5 2 1"); + + //---- label4 ---- + label4.setText("X: time (500ms per line) / Y: value (10% per line)"); + add(label4, "cell 0 6 2 1"); + + //---- updateChartDelayedCheckBox ---- + updateChartDelayedCheckBox.setText("Update chart delayed"); + updateChartDelayedCheckBox.setMnemonic('U'); + updateChartDelayedCheckBox.setSelected(true); + updateChartDelayedCheckBox.addActionListener(e -> updateChartDelayedChanged()); + add(updateChartDelayedCheckBox, "cell 0 6 2 1,alignx right,growx 0"); + + //---- clearChartButton ---- + clearChartButton.setText("Clear Chart"); + clearChartButton.setMnemonic('C'); + clearChartButton.addActionListener(e -> clearChart()); + add(clearChartButton, "cell 0 6 2 1,alignx right,growx 0"); // JFormDesigner - End of component initialization //GEN-END:initComponents } @@ -120,5 +178,68 @@ private void initComponents() { private JScrollBar linearScrollBar; private JScrollBar easeInOutScrollBar; private JButton startButton; + private FlatAnimatorTest.MouseWheelTestPanel mouseWheelTestPanel; + private FlatSmoothScrollingTest.LineChartPanel lineChartPanel; + private JCheckBox updateChartDelayedCheckBox; // JFormDesigner - End of variables declaration //GEN-END:variables + + //---- class MouseWheelTestPanel ------------------------------------------ + + static class MouseWheelTestPanel + extends JPanel + implements MouseWheelListener + { + private static final int MAX_VALUE = 1000; + private static final int STEP = 100; + + private final JLabel valueLabel; + private final Animator animator; + + LineChartPanel lineChartPanel; + + private int value; + private int startValue; + private int targetValue = -1; + + MouseWheelTestPanel() { + super( new BorderLayout() ); + valueLabel = new JLabel( String.valueOf( value ), SwingConstants.CENTER ); + valueLabel.setFont( valueLabel.getFont().deriveFont( (float) valueLabel.getFont().getSize() * 2 ) ); + add( valueLabel, BorderLayout.CENTER ); + add( new JLabel( " " ), BorderLayout.NORTH ); + add( new JLabel( "(move mouse into rectangle and rotate mouse wheel)", SwingConstants.CENTER ), BorderLayout.SOUTH ); + + int duration = FlatUIUtils.getUIInt( "ScrollPane.smoothScrolling.duration", 200 ); + int resolution = FlatUIUtils.getUIInt( "ScrollPane.smoothScrolling.resolution", 10 ); + + animator = new Animator( duration, fraction -> { + value = startValue + Math.round( (targetValue - startValue) * fraction ); + valueLabel.setText( String.valueOf( value ) ); + + lineChartPanel.addValue( value / (double) MAX_VALUE, Color.red ); + }, () -> { + targetValue = -1; + } ); + animator.setResolution( resolution ); + animator.setInterpolator( new CubicBezierEasing( 0.5f, 0.5f, 0.5f, 1 ) ); + + addMouseWheelListener( this ); + } + + @Override + public void mouseWheelMoved( MouseWheelEvent e ) { + lineChartPanel.addValue( 0.5 + (e.getWheelRotation() / 10.), true, Color.red ); + + // start next animation at the current value + startValue = value; + + // increase/decrease target value if animation is in progress + targetValue = (targetValue < 0 ? value : targetValue) + (STEP * e.getWheelRotation()); + targetValue = Math.min( Math.max( targetValue, 0 ), MAX_VALUE ); + + // restart animator + animator.cancel(); + animator.start(); + } + } } 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..fdb1849f3 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: "7.0.2.0.298" Java: "15" encoding: "UTF-8" new FormModel { contentType: "form/swing" @@ -9,7 +9,7 @@ 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": "[][][]" + "$rowConstraints": "[][][][][top][400,grow,fill][]" } ) { name: "this" add( new FormComponent( "javax.swing.JLabel" ) { @@ -54,9 +54,61 @@ new FormModel { }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { "value": "cell 0 2" } ) + add( new FormComponent( "javax.swing.JLabel" ) { + name: "label3" + "text": "Mouse wheel test:" + }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { + "value": "cell 0 4" + } ) + add( new FormComponent( "com.formdev.flatlaf.testing.FlatAnimatorTest$MouseWheelTestPanel" ) { + name: "mouseWheelTestPanel" + "border": new javax.swing.border.LineBorder( sfield java.awt.Color red, 1, false ) + auxiliary() { + "JavaCodeGenerator.variableLocal": false + } + }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { + "value": "cell 1 4,height 100" + } ) + add( new FormContainer( "javax.swing.JScrollPane", new FormLayoutManager( class javax.swing.JScrollPane ) ) { + name: "scrollPane1" + add( new FormComponent( "com.formdev.flatlaf.testing.FlatSmoothScrollingTest$LineChartPanel" ) { + name: "lineChartPanel" + auxiliary() { + "JavaCodeGenerator.variableLocal": false + } + } ) + }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { + "value": "cell 0 5 2 1" + } ) + add( new FormComponent( "javax.swing.JLabel" ) { + name: "label4" + "text": "X: time (500ms per line) / Y: value (10% per line)" + }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { + "value": "cell 0 6 2 1" + } ) + add( new FormComponent( "javax.swing.JCheckBox" ) { + name: "updateChartDelayedCheckBox" + "text": "Update chart delayed" + "mnemonic": 85 + "selected": true + auxiliary() { + "JavaCodeGenerator.variableLocal": false + } + addEvent( new FormEvent( "java.awt.event.ActionListener", "actionPerformed", "updateChartDelayedChanged", false ) ) + }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { + "value": "cell 0 6 2 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 6 2 1,alignx right,growx 0" + } ) }, 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/FlatSmoothScrollingTest.java b/flatlaf-testing/src/main/java/com/formdev/flatlaf/testing/FlatSmoothScrollingTest.java index 1aff8f4cc..632f8d27d 100644 --- a/flatlaf-testing/src/main/java/com/formdev/flatlaf/testing/FlatSmoothScrollingTest.java +++ b/flatlaf-testing/src/main/java/com/formdev/flatlaf/testing/FlatSmoothScrollingTest.java @@ -460,20 +460,23 @@ public Point getViewPosition() { //---- class LineChartPanel ----------------------------------------------- - private static class LineChartPanel + static class LineChartPanel extends JComponent implements Scrollable { - private static final int SECOND_WIDTH = 200; private static final int NEW_SEQUENCE_TIME_LAG = 500; private static final int NEW_SEQUENCE_GAP = 20; + private int secondWidth = 1000; + private static class Data { final double value; + final boolean dot; final long time; // in milliseconds - Data( double value, long time ) { + Data( double value, boolean dot, long time ) { this.value = value; + this.dot = dot; this.time = time; } @@ -497,8 +500,12 @@ public String toString() { } void addValue( double value, Color chartColor ) { + addValue( value, false, chartColor ); + } + + void addValue( double value, boolean dot, Color chartColor ) { List chartData = color2dataMap.computeIfAbsent( chartColor, k -> new ArrayList<>() ); - chartData.add( new Data( value, System.nanoTime() / 1000000) ); + chartData.add( new Data( value, dot, System.nanoTime() / 1000000) ); lastUsedChartColor = chartColor; @@ -521,6 +528,10 @@ void setUpdateDelayed( boolean updateDelayed ) { this.updateDelayed = updateDelayed; } + void setSecondWidth( int secondWidth ) { + this.secondWidth = secondWidth; + } + private void repaintAndRevalidate() { repaint(); revalidate(); @@ -546,7 +557,7 @@ protected void paintComponent( Graphics g ) { private void paintImpl( Graphics2D g, int x, int y, int width, int height, double scaleFactor ) { FlatUIUtils.setRenderingHints( g ); - int secondWidth = (int) (SECOND_WIDTH * scaleFactor); + int secondWidth = (int) (this.secondWidth * scaleFactor); int seqGapWidth = (int) (NEW_SEQUENCE_GAP * scaleFactor); Color lineColor = FlatUIUtils.getUIColor( "Component.borderColor", Color.lightGray ); @@ -591,20 +602,32 @@ private void paintImpl( Graphics2D g, int x, int y, int width, int height, doubl g.setColor( chartColor ); + boolean first = true; int size = chartData.size(); for( int i = 0; i < size; i++ ) { Data data = chartData.get( i ); int dy = (int) ((height - 1) * data.value); + if( data.dot ) { + int dotx = px; + if( i > 0 && data.time > ptime + NEW_SEQUENCE_TIME_LAG ) + dotx += seqGapWidth; + int o = UIScale.scale( 1 ); + int s = UIScale.scale( 3 ); + g.fillRect( dotx - o, dy - o, s, s ); + continue; + } + if( data.time > ptime + NEW_SEQUENCE_TIME_LAG ) { - if( i > 0 && pcount == 0 ) + if( !first && pcount == 0 ) g.drawLine( px, py, px + (int) (4 * scaleFactor), py ); // start new sequence seqTime = data.time; - seqX = (i > 0) ? px + seqGapWidth : 0; + seqX = !first ? px + seqGapWidth : 0; px = seqX; pcount = 0; + first = false; } else { boolean isTemporaryValue = isTemporaryValue( chartData, i ) || isTemporaryValue( chartData, i - 1 ); if( isTemporaryValue ) @@ -634,8 +657,14 @@ private boolean isTemporaryValue( List chartData, int i ) { if( i == 0 || i == chartData.size() - 1 ) return false; - double valueBefore = chartData.get( i - 1 ).value; - double valueAfter = chartData.get( i + 1 ).value; + Data dataBefore = chartData.get( i - 1 ); + Data dataAfter = chartData.get( i + 1 ); + + if( dataBefore.dot || dataAfter.dot ) + return false; + + double valueBefore = dataBefore.value; + double valueAfter = dataAfter.value; return valueBefore == valueAfter || (i < chartData.size() - 2 && valueBefore == chartData.get( i + 2 ).value) || @@ -666,7 +695,7 @@ private int chartWidth( List chartData, int[] lastSeqX ) { px = seqX; } else { // line in sequence - int dx = (int) (seqX + (((data.time - seqTime) / 1000.) * SECOND_WIDTH)); + int dx = (int) (seqX + (((data.time - seqTime) / 1000.) * secondWidth)); px = dx; } @@ -691,7 +720,7 @@ public Dimension getPreferredScrollableViewportSize() { @Override public int getScrollableUnitIncrement( Rectangle visibleRect, int orientation, int direction ) { - return SECOND_WIDTH; + return secondWidth; } @Override From 29f6c5fae97a11eb13f69ccca75fbd7079f34016 Mon Sep 17 00:00:00 2001 From: Karl Tauber Date: Mon, 12 Oct 2020 00:36:35 +0200 Subject: [PATCH 19/30] FlatAnimatorTest: added test for precise scrolling with trackpad --- .../flatlaf/testing/FlatAnimatorTest.java | 85 ++++++++++++------- .../flatlaf/testing/FlatAnimatorTest.jfd | 26 +++--- .../testing/FlatSmoothScrollingTest.java | 11 ++- .../testing/FlatSmoothScrollingTest.jfd | 1 + 4 files changed, 77 insertions(+), 46 deletions(-) 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 a8456db4c..b46195616 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 @@ -46,6 +46,8 @@ public static void main( String[] args ) { FlatAnimatorTest() { initComponents(); + updateChartDelayedChanged(); + lineChartPanel.setSecondWidth( 500 ); mouseWheelTestPanel.lineChartPanel = lineChartPanel; } @@ -90,16 +92,16 @@ private void clearChart() { private void initComponents() { // JFormDesigner - Component initialization - DO NOT MODIFY //GEN-BEGIN:initComponents - JLabel label1 = new JLabel(); + JLabel linearLabel = new JLabel(); linearScrollBar = new JScrollBar(); - JLabel label2 = new JLabel(); + JLabel easeInOutLabel = new JLabel(); easeInOutScrollBar = new JScrollBar(); startButton = new JButton(); - JLabel label3 = new JLabel(); + JLabel mouseWheelTestLabel = new JLabel(); mouseWheelTestPanel = new FlatAnimatorTest.MouseWheelTestPanel(); - JScrollPane scrollPane1 = new JScrollPane(); + JScrollPane lineChartScrollPane = new JScrollPane(); lineChartPanel = new FlatSmoothScrollingTest.LineChartPanel(); - JLabel label4 = new JLabel(); + JLabel lineChartInfoLabel = new JLabel(); updateChartDelayedCheckBox = new JCheckBox(); JButton clearChartButton = new JButton(); @@ -112,24 +114,23 @@ private void initComponents() { // rows "[]" + "[]" + - "[]" + - "[]" + + "[]para" + "[top]" + "[400,grow,fill]" + "[]")); - //---- label1 ---- - label1.setText("Linear:"); - add(label1, "cell 0 0"); + //---- linearLabel ---- + linearLabel.setText("Linear:"); + add(linearLabel, "cell 0 0"); //---- linearScrollBar ---- linearScrollBar.setOrientation(Adjustable.HORIZONTAL); linearScrollBar.setBlockIncrement(1); add(linearScrollBar, "cell 1 0"); - //---- label2 ---- - label2.setText("Ease in out:"); - add(label2, "cell 0 1"); + //---- easeInOutLabel ---- + easeInOutLabel.setText("Ease in out:"); + add(easeInOutLabel, "cell 0 1"); //---- easeInOutScrollBar ---- easeInOutScrollBar.setOrientation(Adjustable.HORIZONTAL); @@ -141,36 +142,36 @@ private void initComponents() { startButton.addActionListener(e -> start()); add(startButton, "cell 0 2"); - //---- label3 ---- - label3.setText("Mouse wheel test:"); - add(label3, "cell 0 4"); + //---- mouseWheelTestLabel ---- + mouseWheelTestLabel.setText("Mouse wheel test:"); + add(mouseWheelTestLabel, "cell 0 3"); //---- mouseWheelTestPanel ---- mouseWheelTestPanel.setBorder(new LineBorder(Color.red)); - add(mouseWheelTestPanel, "cell 1 4,height 100"); + add(mouseWheelTestPanel, "cell 1 3,height 100"); - //======== scrollPane1 ======== + //======== lineChartScrollPane ======== { - scrollPane1.setViewportView(lineChartPanel); + lineChartScrollPane.putClientProperty("JScrollPane.smoothScrolling", false); + lineChartScrollPane.setViewportView(lineChartPanel); } - add(scrollPane1, "cell 0 5 2 1"); + add(lineChartScrollPane, "cell 0 4 2 1"); - //---- label4 ---- - label4.setText("X: time (500ms per line) / Y: value (10% per line)"); - add(label4, "cell 0 6 2 1"); + //---- lineChartInfoLabel ---- + lineChartInfoLabel.setText("X: time (500ms per line) / Y: value (10% per line)"); + add(lineChartInfoLabel, "cell 0 5 2 1"); //---- updateChartDelayedCheckBox ---- updateChartDelayedCheckBox.setText("Update chart delayed"); updateChartDelayedCheckBox.setMnemonic('U'); - updateChartDelayedCheckBox.setSelected(true); updateChartDelayedCheckBox.addActionListener(e -> updateChartDelayedChanged()); - add(updateChartDelayedCheckBox, "cell 0 6 2 1,alignx right,growx 0"); + add(updateChartDelayedCheckBox, "cell 0 5 2 1,alignx right,growx 0"); //---- clearChartButton ---- clearChartButton.setText("Clear Chart"); clearChartButton.setMnemonic('C'); clearChartButton.addActionListener(e -> clearChart()); - add(clearChartButton, "cell 0 6 2 1,alignx right,growx 0"); + add(clearChartButton, "cell 0 5 2 1,alignx right,growx 0"); // JFormDesigner - End of component initialization //GEN-END:initComponents } @@ -228,14 +229,36 @@ static class MouseWheelTestPanel @Override public void mouseWheelMoved( MouseWheelEvent e ) { - lineChartPanel.addValue( 0.5 + (e.getWheelRotation() / 10.), true, Color.red ); + double preciseWheelRotation = e.getPreciseWheelRotation(); - // start next animation at the current value - startValue = value; + // add a dot in the middle of the chart for the wheel rotation + // for unprecise wheels the rotation value is usually -1 or +1 + // for precise wheels the rotation value is in range ca. -10 to +10, + // depending how fast the wheel is rotated + lineChartPanel.addValue( 0.5 + (preciseWheelRotation / 20.), true, Color.red ); // increase/decrease target value if animation is in progress - targetValue = (targetValue < 0 ? value : targetValue) + (STEP * e.getWheelRotation()); - targetValue = Math.min( Math.max( targetValue, 0 ), MAX_VALUE ); + int newValue = (int) ((targetValue < 0 ? value : targetValue) + (STEP * preciseWheelRotation)); + newValue = Math.min( Math.max( newValue, 0 ), MAX_VALUE ); + + if( preciseWheelRotation != 0 && + preciseWheelRotation != e.getWheelRotation() ) + { + // do not use animation for precise scrolling (e.g. with trackpad) + + // stop running animation (if any) + animator.stop(); + + value = newValue; + valueLabel.setText( String.valueOf( value ) ); + + lineChartPanel.addValue( value / (double) MAX_VALUE, Color.red ); + return; + } + + // start next animation at the current value + startValue = value; + targetValue = newValue; // restart animator animator.cancel(); 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 fdb1849f3..970818f39 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 @@ -9,11 +9,11 @@ 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": "[][][][][top][400,grow,fill][]" + "$rowConstraints": "[][][]para[top][400,grow,fill][]" } ) { name: "this" add( new FormComponent( "javax.swing.JLabel" ) { - name: "label1" + name: "linearLabel" "text": "Linear:" }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { "value": "cell 0 0" @@ -29,7 +29,7 @@ new FormModel { "value": "cell 1 0" } ) add( new FormComponent( "javax.swing.JLabel" ) { - name: "label2" + name: "easeInOutLabel" "text": "Ease in out:" }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { "value": "cell 0 1" @@ -55,10 +55,10 @@ new FormModel { "value": "cell 0 2" } ) add( new FormComponent( "javax.swing.JLabel" ) { - name: "label3" + name: "mouseWheelTestLabel" "text": "Mouse wheel test:" }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { - "value": "cell 0 4" + "value": "cell 0 3" } ) add( new FormComponent( "com.formdev.flatlaf.testing.FlatAnimatorTest$MouseWheelTestPanel" ) { name: "mouseWheelTestPanel" @@ -67,10 +67,11 @@ new FormModel { "JavaCodeGenerator.variableLocal": false } }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { - "value": "cell 1 4,height 100" + "value": "cell 1 3,height 100" } ) add( new FormContainer( "javax.swing.JScrollPane", new FormLayoutManager( class javax.swing.JScrollPane ) ) { - name: "scrollPane1" + name: "lineChartScrollPane" + "$client.JScrollPane.smoothScrolling": false add( new FormComponent( "com.formdev.flatlaf.testing.FlatSmoothScrollingTest$LineChartPanel" ) { name: "lineChartPanel" auxiliary() { @@ -78,25 +79,24 @@ new FormModel { } } ) }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { - "value": "cell 0 5 2 1" + "value": "cell 0 4 2 1" } ) add( new FormComponent( "javax.swing.JLabel" ) { - name: "label4" + name: "lineChartInfoLabel" "text": "X: time (500ms per line) / Y: value (10% per line)" }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { - "value": "cell 0 6 2 1" + "value": "cell 0 5 2 1" } ) add( new FormComponent( "javax.swing.JCheckBox" ) { name: "updateChartDelayedCheckBox" "text": "Update chart delayed" "mnemonic": 85 - "selected": true auxiliary() { "JavaCodeGenerator.variableLocal": false } addEvent( new FormEvent( "java.awt.event.ActionListener", "actionPerformed", "updateChartDelayedChanged", false ) ) }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { - "value": "cell 0 6 2 1,alignx right,growx 0" + "value": "cell 0 5 2 1,alignx right,growx 0" } ) add( new FormComponent( "javax.swing.JButton" ) { name: "clearChartButton" @@ -104,7 +104,7 @@ new FormModel { "mnemonic": 67 addEvent( new FormEvent( "java.awt.event.ActionListener", "actionPerformed", "clearChart", false ) ) }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { - "value": "cell 0 6 2 1,alignx right,growx 0" + "value": "cell 0 5 2 1,alignx right,growx 0" } ) }, new FormLayoutConstraints( null ) { "location": new java.awt.Point( 0, 0 ) diff --git a/flatlaf-testing/src/main/java/com/formdev/flatlaf/testing/FlatSmoothScrollingTest.java b/flatlaf-testing/src/main/java/com/formdev/flatlaf/testing/FlatSmoothScrollingTest.java index 632f8d27d..95e27d672 100644 --- a/flatlaf-testing/src/main/java/com/formdev/flatlaf/testing/FlatSmoothScrollingTest.java +++ b/flatlaf-testing/src/main/java/com/formdev/flatlaf/testing/FlatSmoothScrollingTest.java @@ -347,6 +347,7 @@ private void initComponents() { //======== scrollPane1 ======== { + scrollPane1.putClientProperty("JScrollPane.smoothScrolling", false); scrollPane1.setViewportView(lineChartPanel); } add(scrollPane1, "cell 0 5 4 1,width 100"); @@ -465,7 +466,7 @@ static class LineChartPanel implements Scrollable { private static final int NEW_SEQUENCE_TIME_LAG = 500; - private static final int NEW_SEQUENCE_GAP = 20; + private static final int NEW_SEQUENCE_GAP = 50; private int secondWidth = 1000; @@ -538,9 +539,15 @@ private void repaintAndRevalidate() { // 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( color2dataMap.get( lastUsedChartColor ), lastSeqX ); - scrollRectToVisible( new Rectangle( lastSeqX[0], 0, cw - lastSeqX[0], getHeight() ) ); + + // 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() ) ); } } diff --git a/flatlaf-testing/src/main/java/com/formdev/flatlaf/testing/FlatSmoothScrollingTest.jfd b/flatlaf-testing/src/main/java/com/formdev/flatlaf/testing/FlatSmoothScrollingTest.jfd index d70167661..1a2fbc3f3 100644 --- a/flatlaf-testing/src/main/java/com/formdev/flatlaf/testing/FlatSmoothScrollingTest.jfd +++ b/flatlaf-testing/src/main/java/com/formdev/flatlaf/testing/FlatSmoothScrollingTest.jfd @@ -148,6 +148,7 @@ new FormModel { } ) add( new FormContainer( "javax.swing.JScrollPane", new FormLayoutManager( class javax.swing.JScrollPane ) ) { name: "scrollPane1" + "$client.JScrollPane.smoothScrolling": false add( new FormComponent( "com.formdev.flatlaf.testing.FlatSmoothScrollingTest$LineChartPanel" ) { name: "lineChartPanel" } ) From cf70cfb50c1481cefb0084b2ce6c28bdaef13f4e Mon Sep 17 00:00:00 2001 From: Karl Tauber Date: Sun, 4 Jun 2023 19:40:18 +0200 Subject: [PATCH 20/30] ScrollBar: fixed temporary painting at wrong location during smooth scrolling when using mouse-wheel or scroll bar (still occurs when scrolling by moving selection via keyboard) many thanks to @Chrriis for the idea to temporary disable blitting mode on viewport --- .../formdev/flatlaf/ui/FlatScrollBarUI.java | 33 ++++++- .../formdev/flatlaf/ui/FlatScrollPaneUI.java | 2 + .../testing/FlatSmoothScrollingTest.java | 91 +++++++++++++------ .../testing/FlatSmoothScrollingTest.jfd | 6 +- 4 files changed, 97 insertions(+), 35 deletions(-) diff --git a/flatlaf-core/src/main/java/com/formdev/flatlaf/ui/FlatScrollBarUI.java b/flatlaf-core/src/main/java/com/formdev/flatlaf/ui/FlatScrollBarUI.java index f9aef8e62..f999e3991 100644 --- a/flatlaf-core/src/main/java/com/formdev/flatlaf/ui/FlatScrollBarUI.java +++ b/flatlaf-core/src/main/java/com/formdev/flatlaf/ui/FlatScrollBarUI.java @@ -34,6 +34,7 @@ import javax.swing.JComponent; import javax.swing.JScrollBar; import javax.swing.JScrollPane; +import javax.swing.JViewport; import javax.swing.SwingUtilities; import javax.swing.UIManager; import javax.swing.plaf.ComponentUI; @@ -480,12 +481,14 @@ public void runAndSetValueAnimated( Runnable r ) { // remember current scrollbar value so that we can start scroll animation from there int oldValue = scrollbar.getValue(); - // if invoked while animation is running, calculation of new value - // should start at the previous target value - if( targetValue != Integer.MIN_VALUE ) - scrollbar.setValue( targetValue ); + runWithoutBlitting( scrollbar.getParent(), () ->{ + // if invoked while animation is running, calculation of new value + // should start at the previous target value + if( targetValue != Integer.MIN_VALUE ) + scrollbar.setValue( targetValue ); - r.run(); + r.run(); + } ); // do not use animation if started dragging thumb if( isDragging ) { @@ -507,6 +510,26 @@ public void runAndSetValueAnimated( Runnable r ) { inRunAndSetValueAnimated = false; } + private void runWithoutBlitting( Container scrollPane, Runnable r ) { + // prevent the viewport to immediately repaint using blitting + JViewport viewport = null; + int oldScrollMode = 0; + if( scrollPane instanceof JScrollPane ) { + viewport = ((JScrollPane) scrollPane).getViewport(); + if( viewport != null ) { + oldScrollMode = viewport.getScrollMode(); + viewport.setScrollMode( JViewport.BACKINGSTORE_SCROLL_MODE ); + } + } + + try { + r.run(); + } finally { + if( viewport != null ) + viewport.setScrollMode( oldScrollMode ); + } + } + private boolean inRunAndSetValueAnimated; private Animator animator; private int startValue = Integer.MIN_VALUE; diff --git a/flatlaf-core/src/main/java/com/formdev/flatlaf/ui/FlatScrollPaneUI.java b/flatlaf-core/src/main/java/com/formdev/flatlaf/ui/FlatScrollPaneUI.java index c13781620..c59e1ed26 100644 --- a/flatlaf-core/src/main/java/com/formdev/flatlaf/ui/FlatScrollPaneUI.java +++ b/flatlaf-core/src/main/java/com/formdev/flatlaf/ui/FlatScrollPaneUI.java @@ -455,6 +455,8 @@ public static boolean isPermanentFocusOwner( JScrollPane scrollPane ) { @Override protected void syncScrollPaneWithViewport() { + // if the viewport has been scrolled by using JComponent.scrollRectToVisible() + // (e.g. by moving selection), then it is necessary to update the scroll bar values if( isSmoothScrollingEnabled() ) { runAndSyncScrollBarValueAnimated( scrollpane.getVerticalScrollBar(), 0, () -> { runAndSyncScrollBarValueAnimated( scrollpane.getHorizontalScrollBar(), 1, () -> { diff --git a/flatlaf-testing/src/main/java/com/formdev/flatlaf/testing/FlatSmoothScrollingTest.java b/flatlaf-testing/src/main/java/com/formdev/flatlaf/testing/FlatSmoothScrollingTest.java index 95e27d672..b606a21d6 100644 --- a/flatlaf-testing/src/main/java/com/formdev/flatlaf/testing/FlatSmoothScrollingTest.java +++ b/flatlaf-testing/src/main/java/com/formdev/flatlaf/testing/FlatSmoothScrollingTest.java @@ -78,26 +78,26 @@ public static void main( String[] args ) { editorPaneLabel.setIcon( new ColorIcon( Color.orange.darker() ) ); customLabel.setIcon( new ColorIcon( Color.pink ) ); - listScrollPane.getVerticalScrollBar().getModel().addChangeListener( new ScrollBarChangeHandler( "list vert", Color.red.darker() ) ); - listScrollPane.getHorizontalScrollBar().getModel().addChangeListener( new ScrollBarChangeHandler( "list horz", Color.red ) ); + listScrollPane.getVerticalScrollBar().getModel().addChangeListener( new ScrollBarChangeHandler( listScrollPane, true, "list vert", Color.red.darker() ) ); + listScrollPane.getHorizontalScrollBar().getModel().addChangeListener( new ScrollBarChangeHandler( listScrollPane, false, "list horz", Color.red ) ); - treeScrollPane.getVerticalScrollBar().getModel().addChangeListener( new ScrollBarChangeHandler( "tree vert", Color.blue.darker() ) ); - treeScrollPane.getHorizontalScrollBar().getModel().addChangeListener( new ScrollBarChangeHandler( "tree horz", Color.blue ) ); + treeScrollPane.getVerticalScrollBar().getModel().addChangeListener( new ScrollBarChangeHandler( treeScrollPane, true, "tree vert", Color.blue.darker() ) ); + treeScrollPane.getHorizontalScrollBar().getModel().addChangeListener( new ScrollBarChangeHandler( treeScrollPane, false, "tree horz", Color.blue ) ); - tableScrollPane.getVerticalScrollBar().getModel().addChangeListener( new ScrollBarChangeHandler( "table vert", Color.green.darker() ) ); - tableScrollPane.getHorizontalScrollBar().getModel().addChangeListener( new ScrollBarChangeHandler( "table horz", Color.green ) ); + tableScrollPane.getVerticalScrollBar().getModel().addChangeListener( new ScrollBarChangeHandler( tableScrollPane, true, "table vert", Color.green.darker() ) ); + tableScrollPane.getHorizontalScrollBar().getModel().addChangeListener( new ScrollBarChangeHandler( tableScrollPane, false, "table horz", Color.green ) ); - textAreaScrollPane.getVerticalScrollBar().getModel().addChangeListener( new ScrollBarChangeHandler( "textArea vert", Color.magenta.darker() ) ); - textAreaScrollPane.getHorizontalScrollBar().getModel().addChangeListener( new ScrollBarChangeHandler( "textArea horz", Color.magenta ) ); + textAreaScrollPane.getVerticalScrollBar().getModel().addChangeListener( new ScrollBarChangeHandler( textAreaScrollPane, true, "textArea vert", Color.magenta.darker() ) ); + textAreaScrollPane.getHorizontalScrollBar().getModel().addChangeListener( new ScrollBarChangeHandler( textAreaScrollPane, false, "textArea horz", Color.magenta ) ); - textPaneScrollPane.getVerticalScrollBar().getModel().addChangeListener( new ScrollBarChangeHandler( "textPane vert", Color.cyan.darker() ) ); - textPaneScrollPane.getHorizontalScrollBar().getModel().addChangeListener( new ScrollBarChangeHandler( "textPane horz", Color.cyan ) ); + textPaneScrollPane.getVerticalScrollBar().getModel().addChangeListener( new ScrollBarChangeHandler( textPaneScrollPane, true, "textPane vert", Color.cyan.darker() ) ); + textPaneScrollPane.getHorizontalScrollBar().getModel().addChangeListener( new ScrollBarChangeHandler( textPaneScrollPane, false, "textPane horz", Color.cyan ) ); - editorPaneScrollPane.getVerticalScrollBar().getModel().addChangeListener( new ScrollBarChangeHandler( "editorPane vert", Color.orange.darker() ) ); - editorPaneScrollPane.getHorizontalScrollBar().getModel().addChangeListener( new ScrollBarChangeHandler( "editorPane horz", Color.orange ) ); + editorPaneScrollPane.getVerticalScrollBar().getModel().addChangeListener( new ScrollBarChangeHandler( editorPaneScrollPane, true, "editorPane vert", Color.orange.darker() ) ); + editorPaneScrollPane.getHorizontalScrollBar().getModel().addChangeListener( new ScrollBarChangeHandler( editorPaneScrollPane, false, "editorPane horz", Color.orange ) ); - customScrollPane.getVerticalScrollBar().getModel().addChangeListener( new ScrollBarChangeHandler( "custom vert", Color.pink ) ); - customScrollPane.getHorizontalScrollBar().getModel().addChangeListener( new ScrollBarChangeHandler( "custom horz", Color.pink.darker() ) ); + customScrollPane.getVerticalScrollBar().getModel().addChangeListener( new ScrollBarChangeHandler( customScrollPane, true, "custom vert", Color.pink ) ); + customScrollPane.getHorizontalScrollBar().getModel().addChangeListener( new ScrollBarChangeHandler( customScrollPane, false, "custom horz", Color.pink.darker() ) ); ArrayList items = new ArrayList<>(); for( char ch = '0'; ch < 'z'; ch++ ) { @@ -151,6 +151,13 @@ public Object getValueAt( int rowIndex, int columnIndex ) { } } ); + // select some rows to better see smooth scrolling issues + for( int i = 5; i < items.size(); i += 10 ) { + list.addSelectionInterval( i, i ); + tree.addSelectionInterval( i, i ); + table.addRowSelectionInterval( i, i ); + } + // text components String text = items.stream().collect( Collectors.joining( "\n" ) ); textArea.setText( text ); @@ -207,7 +214,7 @@ private void initComponents() { list = new JList<>(); treeScrollPane = new FlatSmoothScrollingTest.DebugScrollPane(); tree = new JTree(); - tableScrollPane = new JScrollPane(); + tableScrollPane = new FlatSmoothScrollingTest.DebugScrollPane(); table = new JTable(); textAreaLabel = new JLabel(); textPaneLabel = new JLabel(); @@ -219,7 +226,7 @@ private void initComponents() { textPane = new JTextPane(); editorPaneScrollPane = new FlatSmoothScrollingTest.DebugScrollPane(); editorPane = new JEditorPane(); - customScrollPane = new JScrollPane(); + customScrollPane = new FlatSmoothScrollingTest.DebugScrollPane(); button1 = new JButton(); scrollPane1 = new JScrollPane(); lineChartPanel = new FlatSmoothScrollingTest.LineChartPanel(); @@ -382,7 +389,7 @@ private void initComponents() { private JList list; private FlatSmoothScrollingTest.DebugScrollPane treeScrollPane; private JTree tree; - private JScrollPane tableScrollPane; + private FlatSmoothScrollingTest.DebugScrollPane tableScrollPane; private JTable table; private JLabel textAreaLabel; private JLabel textPaneLabel; @@ -394,7 +401,7 @@ private void initComponents() { private JTextPane textPane; private FlatSmoothScrollingTest.DebugScrollPane editorPaneScrollPane; private JEditorPane editorPane; - private JScrollPane customScrollPane; + private FlatSmoothScrollingTest.DebugScrollPane customScrollPane; private JButton button1; private JScrollPane scrollPane1; private FlatSmoothScrollingTest.LineChartPanel lineChartPanel; @@ -413,32 +420,43 @@ private class ScrollBarChangeHandler private int count; private long lastTime; - ScrollBarChangeHandler( String name, Color chartColor ) { + ScrollBarChangeHandler( DebugScrollPane scrollPane, boolean vertical, String name, Color chartColor ) { this.name = name; this.chartColor = chartColor; + + // add change listener to viewport that is invoked from JViewport.setViewPosition() + scrollPane.getViewport().addChangeListener( e -> { + // add dot to chart if blit scroll mode is disabled + if( vertical == scrollPane.lastScrollingWasVertical && + scrollPane.getViewport().getScrollMode() != JViewport.BLIT_SCROLL_MODE ) + { + JScrollBar sb = vertical ? scrollPane.getVerticalScrollBar() : scrollPane.getHorizontalScrollBar(); + lineChartPanel.addValue( getChartValue( sb.getModel() ), true, chartColor ); + } + } ); } @Override public void stateChanged( ChangeEvent e ) { DefaultBoundedRangeModel m = (DefaultBoundedRangeModel) e.getSource(); - int value = m.getValue(); - boolean valueIsAdjusting = m.getValueIsAdjusting(); - if( chartColor != null ) { - double chartValue = (double) (value - m.getMinimum()) / (double) (m.getMaximum() - m.getExtent()); - lineChartPanel.addValue( chartValue, chartColor ); - } + lineChartPanel.addValue( getChartValue( m ), chartColor ); long t = System.nanoTime() / 1000000; System.out.printf( "%s (%d): %4d %3d ms %b%n", name, ++count, - value, + m.getValue(), t - lastTime, - valueIsAdjusting ); + m.getValueIsAdjusting() ); lastTime = t; } + + private double getChartValue( BoundedRangeModel m ) { + int value = m.getValue(); + return (double) (value - m.getMinimum()) / (double) (m.getMaximum() - m.getExtent()); + } } //---- class DebugViewport ------------------------------------------------ @@ -446,6 +464,8 @@ public void stateChanged( ChangeEvent e ) { private static class DebugScrollPane extends JScrollPane { + boolean lastScrollingWasVertical; + @Override protected JViewport createViewport() { return new JViewport() { @@ -455,6 +475,23 @@ public Point getViewPosition() { // System.out.println( " viewPosition " + viewPosition.x + "," + viewPosition.y ); return viewPosition; } + + @Override + public void setViewPosition( Point p ) { + // remember whether scrolling vertically or horizontally + Component view = getView(); + if( view != null ) { + int oldY = (view instanceof JComponent) + ? ((JComponent) view).getY() + : view.getBounds().y; + + int newY = -p.y; + lastScrollingWasVertical = (oldY != newY); + } else + lastScrollingWasVertical = true; + + super.setViewPosition( p ); + } }; } } diff --git a/flatlaf-testing/src/main/java/com/formdev/flatlaf/testing/FlatSmoothScrollingTest.jfd b/flatlaf-testing/src/main/java/com/formdev/flatlaf/testing/FlatSmoothScrollingTest.jfd index 1a2fbc3f3..fa3a555e9 100644 --- a/flatlaf-testing/src/main/java/com/formdev/flatlaf/testing/FlatSmoothScrollingTest.jfd +++ b/flatlaf-testing/src/main/java/com/formdev/flatlaf/testing/FlatSmoothScrollingTest.jfd @@ -1,4 +1,4 @@ -JFDML JFormDesigner: "7.0.2.0.298" Java: "14" encoding: "UTF-8" +JFDML JFormDesigner: "8.1.0.0.283" Java: "19.0.2" encoding: "UTF-8" new FormModel { contentType: "form/swing" @@ -75,7 +75,7 @@ new FormModel { }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { "value": "cell 1 2" } ) - add( new FormContainer( "javax.swing.JScrollPane", new FormLayoutManager( class javax.swing.JScrollPane ) ) { + add( new FormContainer( "com.formdev.flatlaf.testing.FlatSmoothScrollingTest$DebugScrollPane", new FormLayoutManager( class javax.swing.JScrollPane ) ) { name: "tableScrollPane" add( new FormComponent( "javax.swing.JTable" ) { name: "table" @@ -134,7 +134,7 @@ new FormModel { }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { "value": "cell 2 4" } ) - add( new FormContainer( "javax.swing.JScrollPane", new FormLayoutManager( class javax.swing.JScrollPane ) ) { + add( new FormContainer( "com.formdev.flatlaf.testing.FlatSmoothScrollingTest$DebugScrollPane", new FormLayoutManager( class javax.swing.JScrollPane ) ) { name: "customScrollPane" add( new FormComponent( "javax.swing.JButton" ) { name: "button1" From e2e3fd31e9d18d26de45ecec70bdec48880fd2a4 Mon Sep 17 00:00:00 2001 From: Karl Tauber Date: Mon, 5 Jun 2023 23:48:17 +0200 Subject: [PATCH 21/30] FlatSmoothScrollingTest: - added small vertical line to indicate data points in chart - added split pane to allow changing height of components - Alt+C clears chart without moving focus to "Clear" button - separate chart lines for smooth and non-smooth scrolling --- .../testing/FlatSmoothScrollingTest.java | 300 ++++++++++------ .../testing/FlatSmoothScrollingTest.jfd | 319 ++++++++++-------- 2 files changed, 369 insertions(+), 250 deletions(-) diff --git a/flatlaf-testing/src/main/java/com/formdev/flatlaf/testing/FlatSmoothScrollingTest.java b/flatlaf-testing/src/main/java/com/formdev/flatlaf/testing/FlatSmoothScrollingTest.java index b606a21d6..f76e3c7e7 100644 --- a/flatlaf-testing/src/main/java/com/formdev/flatlaf/testing/FlatSmoothScrollingTest.java +++ b/flatlaf-testing/src/main/java/com/formdev/flatlaf/testing/FlatSmoothScrollingTest.java @@ -70,6 +70,14 @@ public static void main( String[] args ) { KeyStroke.getKeyStroke( "alt " + (char) smoothScrollingCheckBox.getMnemonic() ), JComponent.WHEN_ANCESTOR_OF_FOCUSED_COMPONENT ); + // allow clearing chart with Alt+C without moving focus to button + registerKeyboardAction( + e -> { + clearChart(); + }, + KeyStroke.getKeyStroke( "alt " + (char) clearChartButton.getMnemonic() ), + JComponent.WHEN_ANCESTOR_OF_FOCUSED_COMPONENT ); + listLabel.setIcon( new ColorIcon( Color.red.darker() ) ); treeLabel.setIcon( new ColorIcon( Color.blue.darker() ) ); tableLabel.setIcon( new ColorIcon( Color.green.darker() ) ); @@ -205,6 +213,9 @@ public void updateUI() { private void initComponents() { // JFormDesigner - Component initialization - DO NOT MODIFY //GEN-BEGIN:initComponents smoothScrollingCheckBox = new JCheckBox(); + splitPane1 = new JSplitPane(); + splitPane2 = new JSplitPane(); + panel1 = new JPanel(); listLabel = new JLabel(); treeLabel = new JLabel(); tableLabel = new JLabel(); @@ -216,6 +227,7 @@ private void initComponents() { tree = new JTree(); tableScrollPane = new FlatSmoothScrollingTest.DebugScrollPane(); table = new JTable(); + panel2 = new JPanel(); textAreaLabel = new JLabel(); textPaneLabel = new JLabel(); editorPaneLabel = new JLabel(); @@ -228,7 +240,8 @@ private void initComponents() { editorPane = new JEditorPane(); customScrollPane = new FlatSmoothScrollingTest.DebugScrollPane(); button1 = new JButton(); - scrollPane1 = new JScrollPane(); + panel3 = new JPanel(); + chartScrollPane = new JScrollPane(); lineChartPanel = new FlatSmoothScrollingTest.LineChartPanel(); label1 = new JLabel(); updateChartDelayedCheckBox = new JCheckBox(); @@ -238,17 +251,10 @@ private void initComponents() { setLayout(new MigLayout( "ltr,insets dialog,hidemode 3", // columns - "[200,fill]" + - "[200,fill]" + - "[200,fill]" + - "[200,fill]", + "[200,grow,fill]", // rows "[]" + - "[]" + - "[200,grow,fill]" + - "[]" + - "[200,grow,fill]" + - "[300,grow,fill]" + + "[grow,fill]" + "[]")); //---- smoothScrollingCheckBox ---- @@ -258,128 +264,186 @@ private void initComponents() { smoothScrollingCheckBox.addActionListener(e -> smoothScrollingChanged()); add(smoothScrollingCheckBox, "cell 0 0,alignx left,growx 0"); - //---- listLabel ---- - listLabel.setText("JList:"); - listLabel.setHorizontalTextPosition(SwingConstants.LEADING); - add(listLabel, "cell 0 1,aligny top,growy 0"); - - //---- treeLabel ---- - treeLabel.setText("JTree:"); - treeLabel.setHorizontalTextPosition(SwingConstants.LEADING); - add(treeLabel, "cell 1 1"); - - //---- tableLabel ---- - tableLabel.setText("JTable:"); - tableLabel.setHorizontalTextPosition(SwingConstants.LEADING); - add(tableLabel, "cell 2 1 2 1"); - - //---- showTableGridCheckBox ---- - showTableGridCheckBox.setText("Show table grid"); - showTableGridCheckBox.setMnemonic('G'); - showTableGridCheckBox.addActionListener(e -> showTableGridChanged()); - add(showTableGridCheckBox, "cell 2 1 2 1,alignx right,growx 0"); - - //---- autoResizeModeCheckBox ---- - autoResizeModeCheckBox.setText("Auto-resize mode"); - autoResizeModeCheckBox.setSelected(true); - autoResizeModeCheckBox.addActionListener(e -> autoResizeModeChanged()); - add(autoResizeModeCheckBox, "cell 2 1 2 1,alignx right,growx 0"); - - //======== listScrollPane ======== + //======== splitPane1 ======== { - listScrollPane.setViewportView(list); - } - add(listScrollPane, "cell 0 2,growx"); + splitPane1.setOrientation(JSplitPane.VERTICAL_SPLIT); + splitPane1.setResizeWeight(1.0); - //======== treeScrollPane ======== - { - treeScrollPane.setViewportView(tree); - } - add(treeScrollPane, "cell 1 2"); + //======== splitPane2 ======== + { + splitPane2.setOrientation(JSplitPane.VERTICAL_SPLIT); + splitPane2.setResizeWeight(0.5); - //======== tableScrollPane ======== - { - tableScrollPane.setViewportView(table); - } - add(tableScrollPane, "cell 2 2 2 1,width 100,height 100"); - - //---- textAreaLabel ---- - textAreaLabel.setText("JTextArea:"); - textAreaLabel.setHorizontalTextPosition(SwingConstants.LEADING); - add(textAreaLabel, "cell 0 3"); - - //---- textPaneLabel ---- - textPaneLabel.setText("JTextPane:"); - textPaneLabel.setHorizontalTextPosition(SwingConstants.LEADING); - add(textPaneLabel, "cell 1 3"); - - //---- editorPaneLabel ---- - editorPaneLabel.setText("JEditorPane:"); - editorPaneLabel.setHorizontalTextPosition(SwingConstants.LEADING); - add(editorPaneLabel, "cell 2 3"); + //======== panel1 ======== + { + panel1.setLayout(new MigLayout( + "ltr,insets 3,hidemode 3", + // columns + "[200,grow,fill]" + + "[200,grow,fill]" + + "[200,grow,fill]" + + "[200,grow,fill]", + // rows + "[]0" + + "[200,grow,fill]")); + + //---- listLabel ---- + listLabel.setText("JList:"); + listLabel.setHorizontalTextPosition(SwingConstants.LEADING); + panel1.add(listLabel, "cell 0 0,aligny top,growy 0"); + + //---- treeLabel ---- + treeLabel.setText("JTree:"); + treeLabel.setHorizontalTextPosition(SwingConstants.LEADING); + panel1.add(treeLabel, "cell 1 0"); + + //---- tableLabel ---- + tableLabel.setText("JTable:"); + tableLabel.setHorizontalTextPosition(SwingConstants.LEADING); + panel1.add(tableLabel, "cell 2 0 2 1"); + + //---- showTableGridCheckBox ---- + showTableGridCheckBox.setText("Show table grid"); + showTableGridCheckBox.setMnemonic('G'); + showTableGridCheckBox.addActionListener(e -> showTableGridChanged()); + panel1.add(showTableGridCheckBox, "cell 2 0 2 1,alignx right,growx 0"); + + //---- autoResizeModeCheckBox ---- + autoResizeModeCheckBox.setText("Auto-resize mode"); + autoResizeModeCheckBox.setSelected(true); + autoResizeModeCheckBox.addActionListener(e -> autoResizeModeChanged()); + panel1.add(autoResizeModeCheckBox, "cell 2 0 2 1,alignx right,growx 0"); + + //======== listScrollPane ======== + { + listScrollPane.setViewportView(list); + } + panel1.add(listScrollPane, "cell 0 1,growx"); - //---- customLabel ---- - customLabel.setText("Custom:"); - add(customLabel, "cell 3 3"); + //======== treeScrollPane ======== + { + treeScrollPane.setViewportView(tree); + } + panel1.add(treeScrollPane, "cell 1 1"); - //======== textAreaScrollPane ======== - { - textAreaScrollPane.setViewportView(textArea); - } - add(textAreaScrollPane, "cell 0 4"); + //======== tableScrollPane ======== + { + tableScrollPane.setViewportView(table); + } + panel1.add(tableScrollPane, "cell 2 1 2 1,width 100,height 100"); + } + splitPane2.setTopComponent(panel1); - //======== textPaneScrollPane ======== - { - textPaneScrollPane.setViewportView(textPane); - } - add(textPaneScrollPane, "cell 1 4"); + //======== panel2 ======== + { + panel2.setLayout(new MigLayout( + "ltr,insets 3,hidemode 3", + // columns + "[200,grow,fill]" + + "[200,grow,fill]" + + "[200,grow,fill]" + + "[200,grow,fill]", + // rows + "[]0" + + "[200,grow,fill]")); + + //---- textAreaLabel ---- + textAreaLabel.setText("JTextArea:"); + textAreaLabel.setHorizontalTextPosition(SwingConstants.LEADING); + panel2.add(textAreaLabel, "cell 0 0"); + + //---- textPaneLabel ---- + textPaneLabel.setText("JTextPane:"); + textPaneLabel.setHorizontalTextPosition(SwingConstants.LEADING); + panel2.add(textPaneLabel, "cell 1 0"); + + //---- editorPaneLabel ---- + editorPaneLabel.setText("JEditorPane:"); + editorPaneLabel.setHorizontalTextPosition(SwingConstants.LEADING); + panel2.add(editorPaneLabel, "cell 2 0"); + + //---- customLabel ---- + customLabel.setText("Custom:"); + panel2.add(customLabel, "cell 3 0"); + + //======== textAreaScrollPane ======== + { + textAreaScrollPane.setViewportView(textArea); + } + panel2.add(textAreaScrollPane, "cell 0 1"); - //======== editorPaneScrollPane ======== - { - editorPaneScrollPane.setViewportView(editorPane); - } - add(editorPaneScrollPane, "cell 2 4"); + //======== textPaneScrollPane ======== + { + textPaneScrollPane.setViewportView(textPane); + } + panel2.add(textPaneScrollPane, "cell 1 1"); - //======== customScrollPane ======== - { + //======== editorPaneScrollPane ======== + { + editorPaneScrollPane.setViewportView(editorPane); + } + panel2.add(editorPaneScrollPane, "cell 2 1"); - //---- button1 ---- - button1.setText("I'm a large button, but do not implement Scrollable interface"); - button1.setPreferredSize(new Dimension(800, 800)); - button1.setHorizontalAlignment(SwingConstants.LEADING); - button1.setVerticalAlignment(SwingConstants.TOP); - customScrollPane.setViewportView(button1); - } - add(customScrollPane, "cell 3 4"); + //======== customScrollPane ======== + { - //======== scrollPane1 ======== - { - scrollPane1.putClientProperty("JScrollPane.smoothScrolling", false); - scrollPane1.setViewportView(lineChartPanel); + //---- button1 ---- + button1.setText("I'm a large button, but do not implement Scrollable interface"); + button1.setPreferredSize(new Dimension(800, 800)); + button1.setHorizontalAlignment(SwingConstants.LEADING); + button1.setVerticalAlignment(SwingConstants.TOP); + customScrollPane.setViewportView(button1); + } + panel2.add(customScrollPane, "cell 3 1"); + } + splitPane2.setBottomComponent(panel2); + } + splitPane1.setTopComponent(splitPane2); + + //======== panel3 ======== + { + panel3.setLayout(new MigLayout( + "insets 3,hidemode 3", + // columns + "[grow,fill]", + // rows + "[300,grow,fill]")); + + //======== chartScrollPane ======== + { + chartScrollPane.putClientProperty("JScrollPane.smoothScrolling", false); + chartScrollPane.setViewportView(lineChartPanel); + } + panel3.add(chartScrollPane, "cell 0 0"); + } + splitPane1.setBottomComponent(panel3); } - add(scrollPane1, "cell 0 5 4 1,width 100"); + add(splitPane1, "cell 0 1"); //---- label1 ---- label1.setText("X: time (200ms per line) / Y: scroll bar value (10% per line)"); - add(label1, "cell 0 6 4 1"); + add(label1, "cell 0 2"); //---- updateChartDelayedCheckBox ---- updateChartDelayedCheckBox.setText("Update chart delayed"); updateChartDelayedCheckBox.setMnemonic('U'); updateChartDelayedCheckBox.setSelected(true); updateChartDelayedCheckBox.addActionListener(e -> updateChartDelayedChanged()); - add(updateChartDelayedCheckBox, "cell 0 6 4 1,alignx right,growx 0"); + add(updateChartDelayedCheckBox, "cell 0 2,alignx right,growx 0"); //---- clearChartButton ---- clearChartButton.setText("Clear Chart"); clearChartButton.setMnemonic('C'); clearChartButton.addActionListener(e -> clearChart()); - add(clearChartButton, "cell 0 6 4 1,alignx right,growx 0"); + add(clearChartButton, "cell 0 2,alignx right,growx 0"); // JFormDesigner - End of component initialization //GEN-END:initComponents } // JFormDesigner - Variables declaration - DO NOT MODIFY //GEN-BEGIN:variables private JCheckBox smoothScrollingCheckBox; + private JSplitPane splitPane1; + private JSplitPane splitPane2; + private JPanel panel1; private JLabel listLabel; private JLabel treeLabel; private JLabel tableLabel; @@ -391,6 +455,7 @@ private void initComponents() { private JTree tree; private FlatSmoothScrollingTest.DebugScrollPane tableScrollPane; private JTable table; + private JPanel panel2; private JLabel textAreaLabel; private JLabel textPaneLabel; private JLabel editorPaneLabel; @@ -403,7 +468,8 @@ private void initComponents() { private JEditorPane editorPane; private FlatSmoothScrollingTest.DebugScrollPane customScrollPane; private JButton button1; - private JScrollPane scrollPane1; + private JPanel panel3; + private JScrollPane chartScrollPane; private FlatSmoothScrollingTest.LineChartPanel lineChartPanel; private JLabel label1; private JCheckBox updateChartDelayedCheckBox; @@ -416,13 +482,15 @@ private class ScrollBarChangeHandler implements ChangeListener { private final String name; - private final Color chartColor; + private final Color chartColor; // for smooth scrolling + private final Color chartColor2; // for non-smooth scrolling private int count; private long lastTime; ScrollBarChangeHandler( DebugScrollPane scrollPane, boolean vertical, String name, Color chartColor ) { this.name = name; this.chartColor = chartColor; + this.chartColor2 = chartColor.brighter(); // add change listener to viewport that is invoked from JViewport.setViewPosition() scrollPane.getViewport().addChangeListener( e -> { @@ -439,8 +507,9 @@ private class ScrollBarChangeHandler @Override public void stateChanged( ChangeEvent e ) { DefaultBoundedRangeModel m = (DefaultBoundedRangeModel) e.getSource(); + boolean smoothScrolling = smoothScrollingCheckBox.isSelected(); - lineChartPanel.addValue( getChartValue( m ), chartColor ); + lineChartPanel.addValue( getChartValue( m ), smoothScrolling ? chartColor : chartColor2 ); long t = System.nanoTime() / 1000000; @@ -643,6 +712,8 @@ private void paintImpl( Graphics2D g, int x, int y, int width, int height, doubl int px = 0; int py = 0; int pcount = 0; + int s1 = UIScale.scale( 1 ); + int s3 = UIScale.scale( 3 ); g.setColor( chartColor ); @@ -656,16 +727,18 @@ private void paintImpl( Graphics2D g, int x, int y, int width, int height, doubl int dotx = px; if( i > 0 && data.time > ptime + NEW_SEQUENCE_TIME_LAG ) dotx += seqGapWidth; - int o = UIScale.scale( 1 ); - int s = UIScale.scale( 3 ); - g.fillRect( dotx - o, dy - o, s, s ); + g.fillRect( dotx - s1, dy - s1, s3, s3 ); continue; } if( data.time > ptime + NEW_SEQUENCE_TIME_LAG ) { - if( !first && pcount == 0 ) + if( !first && pcount == 0 ) { g.drawLine( px, py, px + (int) (4 * scaleFactor), py ); + // small vertical line to indicate data point + g.drawLine( px, py - s1, px, py + s1 ); + } + // start new sequence seqTime = data.time; seqX = !first ? px + seqGapWidth : 0; @@ -680,11 +753,16 @@ private void paintImpl( Graphics2D g, int x, int y, int width, int height, doubl // line in sequence int dx = (int) (seqX + (((data.time - seqTime) / 1000.) * secondWidth)); g.drawLine( px, py, dx, dy ); - px = dx; - pcount++; if( isTemporaryValue ) g.setColor( chartColor ); + + // small vertical lines to indicate data point + g.drawLine( px, py - s1, px, py + s1 ); + g.drawLine( dx, dy - s1, dx, dy + s1 ); + + px = dx; + pcount++; } py = dy; diff --git a/flatlaf-testing/src/main/java/com/formdev/flatlaf/testing/FlatSmoothScrollingTest.jfd b/flatlaf-testing/src/main/java/com/formdev/flatlaf/testing/FlatSmoothScrollingTest.jfd index fa3a555e9..f6f3c5532 100644 --- a/flatlaf-testing/src/main/java/com/formdev/flatlaf/testing/FlatSmoothScrollingTest.jfd +++ b/flatlaf-testing/src/main/java/com/formdev/flatlaf/testing/FlatSmoothScrollingTest.jfd @@ -5,8 +5,8 @@ new FormModel { root: new FormRoot { add( new FormContainer( "com.formdev.flatlaf.testing.FlatTestPanel", new FormLayoutManager( class net.miginfocom.swing.MigLayout ) { "$layoutConstraints": "ltr,insets dialog,hidemode 3" - "$columnConstraints": "[200,fill][200,fill][200,fill][200,fill]" - "$rowConstraints": "[][][200,grow,fill][][200,grow,fill][300,grow,fill][]" + "$columnConstraints": "[200,grow,fill]" + "$rowConstraints": "[][grow,fill][]" } ) { name: "this" add( new FormComponent( "javax.swing.JCheckBox" ) { @@ -18,148 +18,189 @@ new FormModel { }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { "value": "cell 0 0,alignx left,growx 0" } ) - add( new FormComponent( "javax.swing.JLabel" ) { - name: "listLabel" - "text": "JList:" - "horizontalTextPosition": 10 - }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { - "value": "cell 0 1,aligny top,growy 0" - } ) - add( new FormComponent( "javax.swing.JLabel" ) { - name: "treeLabel" - "text": "JTree:" - "horizontalTextPosition": 10 - }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { - "value": "cell 1 1" - } ) - add( new FormComponent( "javax.swing.JLabel" ) { - name: "tableLabel" - "text": "JTable:" - "horizontalTextPosition": 10 - }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { - "value": "cell 2 1 2 1" - } ) - add( new FormComponent( "javax.swing.JCheckBox" ) { - name: "showTableGridCheckBox" - "text": "Show table grid" - "mnemonic": 71 - addEvent( new FormEvent( "java.awt.event.ActionListener", "actionPerformed", "showTableGridChanged", false ) ) - }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { - "value": "cell 2 1 2 1,alignx right,growx 0" - } ) - add( new FormComponent( "javax.swing.JCheckBox" ) { - name: "autoResizeModeCheckBox" - "text": "Auto-resize mode" - "selected": true - addEvent( new FormEvent( "java.awt.event.ActionListener", "actionPerformed", "autoResizeModeChanged", false ) ) - }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { - "value": "cell 2 1 2 1,alignx right,growx 0" - } ) - add( new FormContainer( "com.formdev.flatlaf.testing.FlatSmoothScrollingTest$DebugScrollPane", new FormLayoutManager( class javax.swing.JScrollPane ) ) { - name: "listScrollPane" - add( new FormComponent( "javax.swing.JList" ) { - name: "list" - auxiliary() { - "JavaCodeGenerator.typeParameters": "String" - "JavaCodeGenerator.variableLocal": false - } - } ) - }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { - "value": "cell 0 2,growx" - } ) - add( new FormContainer( "com.formdev.flatlaf.testing.FlatSmoothScrollingTest$DebugScrollPane", new FormLayoutManager( class javax.swing.JScrollPane ) ) { - name: "treeScrollPane" - add( new FormComponent( "javax.swing.JTree" ) { - name: "tree" - } ) - }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { - "value": "cell 1 2" - } ) - add( new FormContainer( "com.formdev.flatlaf.testing.FlatSmoothScrollingTest$DebugScrollPane", new FormLayoutManager( class javax.swing.JScrollPane ) ) { - name: "tableScrollPane" - add( new FormComponent( "javax.swing.JTable" ) { - name: "table" - } ) - }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { - "value": "cell 2 2 2 1,width 100,height 100" - } ) - add( new FormComponent( "javax.swing.JLabel" ) { - name: "textAreaLabel" - "text": "JTextArea:" - "horizontalTextPosition": 10 - }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { - "value": "cell 0 3" - } ) - add( new FormComponent( "javax.swing.JLabel" ) { - name: "textPaneLabel" - "text": "JTextPane:" - "horizontalTextPosition": 10 - }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { - "value": "cell 1 3" - } ) - add( new FormComponent( "javax.swing.JLabel" ) { - name: "editorPaneLabel" - "text": "JEditorPane:" - "horizontalTextPosition": 10 - }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { - "value": "cell 2 3" - } ) - add( new FormComponent( "javax.swing.JLabel" ) { - name: "customLabel" - "text": "Custom:" - }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { - "value": "cell 3 3" - } ) - add( new FormContainer( "com.formdev.flatlaf.testing.FlatSmoothScrollingTest$DebugScrollPane", new FormLayoutManager( class javax.swing.JScrollPane ) ) { - name: "textAreaScrollPane" - add( new FormComponent( "javax.swing.JTextArea" ) { - name: "textArea" - } ) - }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { - "value": "cell 0 4" - } ) - add( new FormContainer( "com.formdev.flatlaf.testing.FlatSmoothScrollingTest$DebugScrollPane", new FormLayoutManager( class javax.swing.JScrollPane ) ) { - name: "textPaneScrollPane" - add( new FormComponent( "javax.swing.JTextPane" ) { - name: "textPane" - } ) - }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { - "value": "cell 1 4" - } ) - add( new FormContainer( "com.formdev.flatlaf.testing.FlatSmoothScrollingTest$DebugScrollPane", new FormLayoutManager( class javax.swing.JScrollPane ) ) { - name: "editorPaneScrollPane" - add( new FormComponent( "javax.swing.JEditorPane" ) { - name: "editorPane" - } ) - }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { - "value": "cell 2 4" - } ) - add( new FormContainer( "com.formdev.flatlaf.testing.FlatSmoothScrollingTest$DebugScrollPane", new FormLayoutManager( class javax.swing.JScrollPane ) ) { - name: "customScrollPane" - add( new FormComponent( "javax.swing.JButton" ) { - name: "button1" - "text": "I'm a large button, but do not implement Scrollable interface" - "preferredSize": new java.awt.Dimension( 800, 800 ) - "horizontalAlignment": 10 - "verticalAlignment": 1 + add( new FormContainer( "javax.swing.JSplitPane", new FormLayoutManager( class javax.swing.JSplitPane ) ) { + name: "splitPane1" + "orientation": 0 + "resizeWeight": 1.0 + add( new FormContainer( "javax.swing.JSplitPane", new FormLayoutManager( class javax.swing.JSplitPane ) ) { + name: "splitPane2" + "orientation": 0 + "resizeWeight": 0.5 + add( new FormContainer( "javax.swing.JPanel", new FormLayoutManager( class net.miginfocom.swing.MigLayout ) { + "$columnConstraints": "[200,grow,fill][200,grow,fill][200,grow,fill][200,grow,fill]" + "$rowConstraints": "[]0[200,grow,fill]" + "$layoutConstraints": "ltr,insets 3,hidemode 3" + } ) { + name: "panel1" + add( new FormComponent( "javax.swing.JLabel" ) { + name: "listLabel" + "text": "JList:" + "horizontalTextPosition": 10 + }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { + "value": "cell 0 0,aligny top,growy 0" + } ) + add( new FormComponent( "javax.swing.JLabel" ) { + name: "treeLabel" + "text": "JTree:" + "horizontalTextPosition": 10 + }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { + "value": "cell 1 0" + } ) + add( new FormComponent( "javax.swing.JLabel" ) { + name: "tableLabel" + "text": "JTable:" + "horizontalTextPosition": 10 + }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { + "value": "cell 2 0 2 1" + } ) + add( new FormComponent( "javax.swing.JCheckBox" ) { + name: "showTableGridCheckBox" + "text": "Show table grid" + "mnemonic": 71 + addEvent( new FormEvent( "java.awt.event.ActionListener", "actionPerformed", "showTableGridChanged", false ) ) + }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { + "value": "cell 2 0 2 1,alignx right,growx 0" + } ) + add( new FormComponent( "javax.swing.JCheckBox" ) { + name: "autoResizeModeCheckBox" + "text": "Auto-resize mode" + "selected": true + addEvent( new FormEvent( "java.awt.event.ActionListener", "actionPerformed", "autoResizeModeChanged", false ) ) + }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { + "value": "cell 2 0 2 1,alignx right,growx 0" + } ) + add( new FormContainer( "com.formdev.flatlaf.testing.FlatSmoothScrollingTest$DebugScrollPane", new FormLayoutManager( class javax.swing.JScrollPane ) ) { + name: "listScrollPane" + add( new FormComponent( "javax.swing.JList" ) { + name: "list" + auxiliary() { + "JavaCodeGenerator.typeParameters": "String" + "JavaCodeGenerator.variableLocal": false + } + } ) + }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { + "value": "cell 0 1,growx" + } ) + add( new FormContainer( "com.formdev.flatlaf.testing.FlatSmoothScrollingTest$DebugScrollPane", new FormLayoutManager( class javax.swing.JScrollPane ) ) { + name: "treeScrollPane" + add( new FormComponent( "javax.swing.JTree" ) { + name: "tree" + } ) + }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { + "value": "cell 1 1" + } ) + add( new FormContainer( "com.formdev.flatlaf.testing.FlatSmoothScrollingTest$DebugScrollPane", new FormLayoutManager( class javax.swing.JScrollPane ) ) { + name: "tableScrollPane" + add( new FormComponent( "javax.swing.JTable" ) { + name: "table" + } ) + }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { + "value": "cell 2 1 2 1,width 100,height 100" + } ) + }, new FormLayoutConstraints( class java.lang.String ) { + "value": "left" + } ) + add( new FormContainer( "javax.swing.JPanel", new FormLayoutManager( class net.miginfocom.swing.MigLayout ) { + "$columnConstraints": "[200,grow,fill][200,grow,fill][200,grow,fill][200,grow,fill]" + "$rowConstraints": "[]0[200,grow,fill]" + "$layoutConstraints": "ltr,insets 3,hidemode 3" + } ) { + name: "panel2" + add( new FormComponent( "javax.swing.JLabel" ) { + name: "textAreaLabel" + "text": "JTextArea:" + "horizontalTextPosition": 10 + }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { + "value": "cell 0 0" + } ) + add( new FormComponent( "javax.swing.JLabel" ) { + name: "textPaneLabel" + "text": "JTextPane:" + "horizontalTextPosition": 10 + }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { + "value": "cell 1 0" + } ) + add( new FormComponent( "javax.swing.JLabel" ) { + name: "editorPaneLabel" + "text": "JEditorPane:" + "horizontalTextPosition": 10 + }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { + "value": "cell 2 0" + } ) + add( new FormComponent( "javax.swing.JLabel" ) { + name: "customLabel" + "text": "Custom:" + }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { + "value": "cell 3 0" + } ) + add( new FormContainer( "com.formdev.flatlaf.testing.FlatSmoothScrollingTest$DebugScrollPane", new FormLayoutManager( class javax.swing.JScrollPane ) ) { + name: "textAreaScrollPane" + add( new FormComponent( "javax.swing.JTextArea" ) { + name: "textArea" + } ) + }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { + "value": "cell 0 1" + } ) + add( new FormContainer( "com.formdev.flatlaf.testing.FlatSmoothScrollingTest$DebugScrollPane", new FormLayoutManager( class javax.swing.JScrollPane ) ) { + name: "textPaneScrollPane" + add( new FormComponent( "javax.swing.JTextPane" ) { + name: "textPane" + } ) + }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { + "value": "cell 1 1" + } ) + add( new FormContainer( "com.formdev.flatlaf.testing.FlatSmoothScrollingTest$DebugScrollPane", new FormLayoutManager( class javax.swing.JScrollPane ) ) { + name: "editorPaneScrollPane" + add( new FormComponent( "javax.swing.JEditorPane" ) { + name: "editorPane" + } ) + }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { + "value": "cell 2 1" + } ) + add( new FormContainer( "com.formdev.flatlaf.testing.FlatSmoothScrollingTest$DebugScrollPane", new FormLayoutManager( class javax.swing.JScrollPane ) ) { + name: "customScrollPane" + add( new FormComponent( "javax.swing.JButton" ) { + name: "button1" + "text": "I'm a large button, but do not implement Scrollable interface" + "preferredSize": new java.awt.Dimension( 800, 800 ) + "horizontalAlignment": 10 + "verticalAlignment": 1 + } ) + }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { + "value": "cell 3 1" + } ) + }, new FormLayoutConstraints( class java.lang.String ) { + "value": "right" + } ) + }, new FormLayoutConstraints( class java.lang.String ) { + "value": "left" } ) - }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { - "value": "cell 3 4" - } ) - add( new FormContainer( "javax.swing.JScrollPane", new FormLayoutManager( class javax.swing.JScrollPane ) ) { - name: "scrollPane1" - "$client.JScrollPane.smoothScrolling": false - add( new FormComponent( "com.formdev.flatlaf.testing.FlatSmoothScrollingTest$LineChartPanel" ) { - name: "lineChartPanel" + add( new FormContainer( "javax.swing.JPanel", new FormLayoutManager( class net.miginfocom.swing.MigLayout ) { + "$layoutConstraints": "insets 3,hidemode 3" + "$columnConstraints": "[grow,fill]" + "$rowConstraints": "[300,grow,fill]" + } ) { + name: "panel3" + add( new FormContainer( "javax.swing.JScrollPane", new FormLayoutManager( class javax.swing.JScrollPane ) ) { + name: "chartScrollPane" + "$client.JScrollPane.smoothScrolling": false + add( new FormComponent( "com.formdev.flatlaf.testing.FlatSmoothScrollingTest$LineChartPanel" ) { + name: "lineChartPanel" + } ) + }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { + "value": "cell 0 0" + } ) + }, new FormLayoutConstraints( class java.lang.String ) { + "value": "right" } ) }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { - "value": "cell 0 5 4 1,width 100" + "value": "cell 0 1" } ) add( new FormComponent( "javax.swing.JLabel" ) { name: "label1" "text": "X: time (200ms per line) / Y: scroll bar value (10% per line)" }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { - "value": "cell 0 6 4 1" + "value": "cell 0 2" } ) add( new FormComponent( "javax.swing.JCheckBox" ) { name: "updateChartDelayedCheckBox" @@ -168,7 +209,7 @@ new FormModel { "selected": true addEvent( new FormEvent( "java.awt.event.ActionListener", "actionPerformed", "updateChartDelayedChanged", false ) ) }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { - "value": "cell 0 6 4 1,alignx right,growx 0" + "value": "cell 0 2,alignx right,growx 0" } ) add( new FormComponent( "javax.swing.JButton" ) { name: "clearChartButton" @@ -176,11 +217,11 @@ new FormModel { "mnemonic": 67 addEvent( new FormEvent( "java.awt.event.ActionListener", "actionPerformed", "clearChart", false ) ) }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { - "value": "cell 0 6 4 1,alignx right,growx 0" + "value": "cell 0 2,alignx right,growx 0" } ) }, new FormLayoutConstraints( null ) { "location": new java.awt.Point( 0, 0 ) - "size": new java.awt.Dimension( 790, 715 ) + "size": new java.awt.Dimension( 875, 715 ) } ) } } From 6ce2198cd6e2e6002418dca96ed04f4454bac5e5 Mon Sep 17 00:00:00 2001 From: Karl Tauber Date: Wed, 23 Aug 2023 15:53:55 +0200 Subject: [PATCH 22/30] FlatSmoothScrollingTest: - added slider to horizontally scale chart - improved chart legend - record stack for points in chart and show in tooltip on hover --- .../flatlaf/testing/FlatAnimatorTest.java | 8 +- .../testing/FlatSmoothScrollingTest.java | 401 ++++++++++++++---- .../testing/FlatSmoothScrollingTest.jfd | 65 ++- 3 files changed, 380 insertions(+), 94 deletions(-) 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 b46195616..20d26bcd6 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 @@ -48,7 +48,7 @@ public static void main( String[] args ) { updateChartDelayedChanged(); - lineChartPanel.setSecondWidth( 500 ); + lineChartPanel.setOneSecondWidth( 500 ); mouseWheelTestPanel.lineChartPanel = lineChartPanel; } @@ -217,7 +217,7 @@ static class MouseWheelTestPanel value = startValue + Math.round( (targetValue - startValue) * fraction ); valueLabel.setText( String.valueOf( value ) ); - lineChartPanel.addValue( value / (double) MAX_VALUE, Color.red ); + lineChartPanel.addValue( value / (double) MAX_VALUE, false, Color.red, null ); }, () -> { targetValue = -1; } ); @@ -235,7 +235,7 @@ public void mouseWheelMoved( MouseWheelEvent e ) { // for unprecise wheels the rotation value is usually -1 or +1 // for precise wheels the rotation value is in range ca. -10 to +10, // depending how fast the wheel is rotated - lineChartPanel.addValue( 0.5 + (preciseWheelRotation / 20.), true, Color.red ); + lineChartPanel.addValue( 0.5 + (preciseWheelRotation / 20.), true, Color.red, null ); // increase/decrease target value if animation is in progress int newValue = (int) ((targetValue < 0 ? value : targetValue) + (STEP * preciseWheelRotation)); @@ -252,7 +252,7 @@ public void mouseWheelMoved( MouseWheelEvent e ) { value = newValue; valueLabel.setText( String.valueOf( value ) ); - lineChartPanel.addValue( value / (double) MAX_VALUE, Color.red ); + lineChartPanel.addValue( value / (double) MAX_VALUE, false, Color.red, null ); return; } diff --git a/flatlaf-testing/src/main/java/com/formdev/flatlaf/testing/FlatSmoothScrollingTest.java b/flatlaf-testing/src/main/java/com/formdev/flatlaf/testing/FlatSmoothScrollingTest.java index f76e3c7e7..776866688 100644 --- a/flatlaf-testing/src/main/java/com/formdev/flatlaf/testing/FlatSmoothScrollingTest.java +++ b/flatlaf-testing/src/main/java/com/formdev/flatlaf/testing/FlatSmoothScrollingTest.java @@ -24,11 +24,15 @@ 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.Arrays; import java.util.HashMap; import java.util.List; import java.util.Map; +import java.util.Objects; import java.util.stream.Collectors; import javax.swing.*; import javax.swing.event.ChangeEvent; @@ -37,6 +41,7 @@ import javax.swing.tree.*; import com.formdev.flatlaf.FlatLaf; import com.formdev.flatlaf.ui.FlatUIUtils; +import com.formdev.flatlaf.util.ColorFunctions; import com.formdev.flatlaf.util.HSLColor; import com.formdev.flatlaf.util.HiDPIUtils; import com.formdev.flatlaf.util.UIScale; @@ -59,8 +64,12 @@ public static void main( String[] args ) { FlatSmoothScrollingTest() { initComponents(); + oneSecondWidthChanged(); updateChartDelayedChanged(); + ToolTipManager.sharedInstance().setInitialDelay( 0 ); + ToolTipManager.sharedInstance().setDismissDelay( Integer.MAX_VALUE ); + // allow enabling/disabling smooth scrolling with Alt+S without moving focus to checkbox registerKeyboardAction( e -> { @@ -107,8 +116,17 @@ public static void main( String[] args ) { customScrollPane.getVerticalScrollBar().getModel().addChangeListener( new ScrollBarChangeHandler( customScrollPane, true, "custom vert", Color.pink ) ); customScrollPane.getHorizontalScrollBar().getModel().addChangeListener( new ScrollBarChangeHandler( customScrollPane, false, "custom horz", Color.pink.darker() ) ); + // clear chart on startup + addHierarchyListener( e -> { + if( (e.getChangeFlags() & HierarchyEvent.SHOWING_CHANGED) != 0 && isShowing() ) + EventQueue.invokeLater( this::clearChart ); + }); + ArrayList items = new ArrayList<>(); for( char ch = '0'; ch < 'z'; ch++ ) { + if( (ch > '9' && ch < 'A') || (ch > 'Z' && ch < 'a') ) + continue; + char[] chars = new char[ch - '0' + 1]; Arrays.fill( chars, ch ); items.add( new String( chars ) ); @@ -167,20 +185,47 @@ public Object getValueAt( int rowIndex, int columnIndex ) { } // text components - String text = items.stream().collect( Collectors.joining( "\n" ) ); - textArea.setText( text ); + String longText = items.stream().collect( Collectors.joining( " " ) ) + ' ' + + items.stream().limit( 20 ).collect( Collectors.joining( " " ) ); + String text = items.stream().collect( Collectors.joining( "\n" ) ) + '\n'; + textArea.setText( longText + '\n' + text ); textPane.setText( text ); editorPane.setText( text ); textArea.select( 0, 0 ); textPane.select( 0, 0 ); editorPane.select( 0, 0 ); + + EventQueue.invokeLater( () -> { + EventQueue.invokeLater( () -> { + list.requestFocusInWindow(); + } ); + } ); } private void smoothScrollingChanged() { UIManager.put( "ScrollPane.smoothScrolling", smoothScrollingCheckBox.isSelected() ); } + private void oneSecondWidthChanged() { + int oneSecondWidth = oneSecondWidthSlider.getValue(); + int msPerLineX = + oneSecondWidth <= 2000 ? 100 : + oneSecondWidth <= 4000 ? 50 : + oneSecondWidth <= 8000 ? 25 : + 10; + + lineChartPanel.setOneSecondWidth( oneSecondWidth ); + lineChartPanel.setMsPerLineX( msPerLineX ); + lineChartPanel.revalidate(); + lineChartPanel.repaint(); + + if( xLabelText == null ) + xLabelText = xLabel.getText(); + xLabel.setText( MessageFormat.format( xLabelText, msPerLineX ) ); + } + private String xLabelText; + private void clearChart() { lineChartPanel.clear(); } @@ -243,7 +288,13 @@ private void initComponents() { panel3 = new JPanel(); chartScrollPane = new JScrollPane(); lineChartPanel = new FlatSmoothScrollingTest.LineChartPanel(); - label1 = new JLabel(); + panel4 = new JPanel(); + xLabel = new JLabel(); + JLabel rectsLabel = new JLabel(); + JLabel yLabel = new JLabel(); + JLabel dotsLabel = new JLabel(); + JLabel oneSecondWidthLabel = new JLabel(); + oneSecondWidthSlider = new JSlider(); updateChartDelayedCheckBox = new JCheckBox(); clearChartButton = new JButton(); @@ -407,7 +458,7 @@ private void initComponents() { // columns "[grow,fill]", // rows - "[300,grow,fill]")); + "[100:300,grow,fill]")); //======== chartScrollPane ======== { @@ -420,13 +471,50 @@ private void initComponents() { } add(splitPane1, "cell 0 1"); - //---- label1 ---- - label1.setText("X: time (200ms per line) / Y: scroll bar value (10% per line)"); - add(label1, "cell 0 2"); + //======== panel4 ======== + { + panel4.setLayout(new MigLayout( + "insets 0,hidemode 3,gapy 0", + // columns + "[fill]para" + + "[fill]", + // rows + "[]" + + "[]")); + + //---- xLabel ---- + xLabel.setText("X: time ({0}ms per line)"); + panel4.add(xLabel, "cell 0 0"); + + //---- rectsLabel ---- + rectsLabel.setText("Rectangles: scrollbar values (mouse hover shows stack)"); + panel4.add(rectsLabel, "cell 1 0"); + + //---- yLabel ---- + yLabel.setText("Y: scroll bar value (10% per line)"); + panel4.add(yLabel, "cell 0 1"); + + //---- dotsLabel ---- + dotsLabel.setText("Dots: disabled blitting mode in JViewport"); + panel4.add(dotsLabel, "cell 1 1"); + } + add(panel4, "cell 0 2"); + + //---- oneSecondWidthLabel ---- + oneSecondWidthLabel.setText("Scale X:"); + oneSecondWidthLabel.setDisplayedMnemonic('A'); + oneSecondWidthLabel.setLabelFor(oneSecondWidthSlider); + add(oneSecondWidthLabel, "cell 0 2,alignx right,growx 0"); + + //---- oneSecondWidthSlider ---- + oneSecondWidthSlider.setMinimum(1000); + oneSecondWidthSlider.setMaximum(10000); + oneSecondWidthSlider.addChangeListener(e -> oneSecondWidthChanged()); + add(oneSecondWidthSlider, "cell 0 2,alignx right,growx 0,wmax 100"); //---- updateChartDelayedCheckBox ---- updateChartDelayedCheckBox.setText("Update chart delayed"); - updateChartDelayedCheckBox.setMnemonic('U'); + updateChartDelayedCheckBox.setMnemonic('P'); updateChartDelayedCheckBox.setSelected(true); updateChartDelayedCheckBox.addActionListener(e -> updateChartDelayedChanged()); add(updateChartDelayedCheckBox, "cell 0 2,alignx right,growx 0"); @@ -471,7 +559,9 @@ private void initComponents() { private JPanel panel3; private JScrollPane chartScrollPane; private FlatSmoothScrollingTest.LineChartPanel lineChartPanel; - private JLabel label1; + private JPanel panel4; + private JLabel xLabel; + private JSlider oneSecondWidthSlider; private JCheckBox updateChartDelayedCheckBox; private JButton clearChartButton; // JFormDesigner - End of variables declaration //GEN-END:variables @@ -490,7 +580,7 @@ private class ScrollBarChangeHandler ScrollBarChangeHandler( DebugScrollPane scrollPane, boolean vertical, String name, Color chartColor ) { this.name = name; this.chartColor = chartColor; - this.chartColor2 = chartColor.brighter(); + this.chartColor2 = ColorFunctions.lighten( chartColor, 0.1f ); // add change listener to viewport that is invoked from JViewport.setViewPosition() scrollPane.getViewport().addChangeListener( e -> { @@ -498,8 +588,15 @@ private class ScrollBarChangeHandler if( vertical == scrollPane.lastScrollingWasVertical && scrollPane.getViewport().getScrollMode() != JViewport.BLIT_SCROLL_MODE ) { - JScrollBar sb = vertical ? scrollPane.getVerticalScrollBar() : scrollPane.getHorizontalScrollBar(); - lineChartPanel.addValue( getChartValue( sb.getModel() ), true, chartColor ); + // calculate value from view position because scrollbar value is not yet up-to-date + JViewport viewport = scrollPane.getViewport(); + Point viewPosition = viewport.getViewPosition(); + Dimension viewSize = viewport.getViewSize(); + double value = vertical + ? ((double) viewPosition.y) / (viewSize.height - viewport.getHeight()) + : ((double) viewPosition.x) / (viewSize.width - viewport.getWidth()); + + lineChartPanel.addValue( value, true, chartColor, name ); } } ); } @@ -509,7 +606,7 @@ public void stateChanged( ChangeEvent e ) { DefaultBoundedRangeModel m = (DefaultBoundedRangeModel) e.getSource(); boolean smoothScrolling = smoothScrollingCheckBox.isSelected(); - lineChartPanel.addValue( getChartValue( m ), smoothScrolling ? chartColor : chartColor2 ); + lineChartPanel.addValue( getChartValue( m ), false, smoothScrolling ? chartColor : chartColor2, name ); long t = System.nanoTime() / 1000000; @@ -561,6 +658,20 @@ public void setViewPosition( Point p ) { super.setViewPosition( p ); } + + @Override + public void paint( Graphics g ) { + super.paint( g ); + + if( backingStoreImage != null ) { + System.out.println( "---------------------------------------------" ); + System.out.println( "WARNING: backingStoreImage was used for painting" ); + System.out.println( "View: " + getView() ); + System.out.println( "Clip: " + g.getClipBounds() ); + new Exception().printStackTrace( System.out ); + System.out.println( "---------------------------------------------" ); + } + } }; } } @@ -572,25 +683,31 @@ static class LineChartPanel implements Scrollable { private static final int NEW_SEQUENCE_TIME_LAG = 500; - private static final int NEW_SEQUENCE_GAP = 50; + private static final int NEW_SEQUENCE_GAP = 100; + private static final int HIT_OFFSET = 4; - private int secondWidth = 1000; + private int oneSecondWidth = 1000; + private int msPerLineX = 200; private static class Data { final double value; final boolean dot; final long time; // in milliseconds + final String name; + final Exception stack; - Data( double value, boolean dot, long time ) { + Data( double value, boolean dot, long time, String name, Exception stack ) { this.value = value; this.dot = dot; this.time = time; + this.name = name; + this.stack = stack; } @Override public String toString() { // for debugging - return String.valueOf( value ); + return "value=" + value + ", dot=" + dot + ", time=" + time + ", name=" + name; } } @@ -599,20 +716,23 @@ public String toString() { 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; + LineChartPanel() { int resolution = FlatUIUtils.getUIInt( "ScrollPane.smoothScrolling.resolution", 10 ); repaintTime = new Timer( resolution * 2, e -> repaintAndRevalidate() ); repaintTime.setRepeats( false ); - } - void addValue( double value, Color chartColor ) { - addValue( value, false, chartColor ); + ToolTipManager.sharedInstance().registerComponent( this ); } - void addValue( double value, boolean dot, Color chartColor ) { + void addValue( double value, boolean dot, Color chartColor, String name ) { List chartData = color2dataMap.computeIfAbsent( chartColor, k -> new ArrayList<>() ); - chartData.add( new Data( value, dot, System.nanoTime() / 1000000) ); + chartData.add( new Data( value, dot, System.nanoTime() / 1000000, name, new Exception() ) ); lastUsedChartColor = chartColor; @@ -635,8 +755,12 @@ void setUpdateDelayed( boolean updateDelayed ) { this.updateDelayed = updateDelayed; } - void setSecondWidth( int secondWidth ) { - this.secondWidth = secondWidth; + void setOneSecondWidth( int oneSecondWidth ) { + this.oneSecondWidth = oneSecondWidth; + } + + void setMsPerLineX( int msPerLineX ) { + this.msPerLineX = msPerLineX; } private void repaintAndRevalidate() { @@ -670,8 +794,9 @@ protected void paintComponent( Graphics g ) { private void paintImpl( Graphics2D g, int x, int y, int width, int height, double scaleFactor ) { FlatUIUtils.setRenderingHints( g ); - int secondWidth = (int) (this.secondWidth * scaleFactor); + int oneSecondWidth = (int) (this.oneSecondWidth * scaleFactor); int seqGapWidth = (int) (NEW_SEQUENCE_GAP * scaleFactor); + int hitOffset = (int) Math.round( UIScale.scale( HIT_OFFSET ) * scaleFactor ); Color lineColor = FlatUIUtils.getUIColor( "Component.borderColor", Color.lightGray ); Color lineColor2 = FlatLaf.isLafDark() @@ -692,51 +817,51 @@ private void paintImpl( Graphics2D g, int x, int y, int width, int height, doubl } // paint vertical lines - int twoHundredMillisWidth = secondWidth / 5; - for( int i = twoHundredMillisWidth; i < width; i += twoHundredMillisWidth ) { - g.setColor( (i % secondWidth != 0) ? lineColor : lineColor2 ); - g.drawLine( i, 0, i, height ); + 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 : color2dataMap.entrySet() ) { List chartData = e.getValue(); Color chartColor = e.getKey(); if( FlatLaf.isLafDark() ) chartColor = new HSLColor( chartColor ).adjustTone( 50 ); - Color temporaryValueColor = new Color( (chartColor.getRGB() & 0xffffff) | 0x40000000, true ); + Color temporaryValueColor = ColorFunctions.fade( chartColor, FlatLaf.isLafDark() ? 0.7f : 0.3f ); + Color dataPointColor = ColorFunctions.fade( chartColor, FlatLaf.isLafDark() ? 0.6f : 0.2f ); + // sequence start time and x coordinate long seqTime = 0; int seqX = 0; + + // "previous" data point time, x/y coordinates and count long ptime = 0; int px = 0; int py = 0; int pcount = 0; - int s1 = UIScale.scale( 1 ); - int s3 = UIScale.scale( 3 ); - - g.setColor( chartColor ); boolean first = true; + boolean isTemporaryValue = false; + int lastTemporaryValueIndex = -1; + int size = chartData.size(); for( int i = 0; i < size; i++ ) { Data data = chartData.get( i ); - int dy = (int) ((height - 1) * data.value); - if( data.dot ) { - int dotx = px; - if( i > 0 && data.time > ptime + NEW_SEQUENCE_TIME_LAG ) - dotx += seqGapWidth; - g.fillRect( dotx - s1, dy - s1, s3, s3 ); - continue; - } + boolean newSeq = (data.time > ptime + NEW_SEQUENCE_TIME_LAG); + ptime = data.time; - if( data.time > ptime + NEW_SEQUENCE_TIME_LAG ) { + if( newSeq ) { + // paint short horizontal line for previous sequence that has only one data point if( !first && pcount == 0 ) { - g.drawLine( px, py, px + (int) (4 * scaleFactor), py ); - - // small vertical line to indicate data point - g.drawLine( px, py - s1, px, py + s1 ); + g.setColor( chartColor ); + g.drawLine( px, py, px + (int) Math.round( UIScale.scale( 8 ) * scaleFactor ), py ); } // start new sequence @@ -745,52 +870,67 @@ private void paintImpl( Graphics2D g, int x, int y, int width, int height, doubl px = seqX; pcount = 0; first = false; - } else { - boolean isTemporaryValue = isTemporaryValue( chartData, i ) || isTemporaryValue( chartData, i - 1 ); - if( isTemporaryValue ) - g.setColor( temporaryValueColor ); + isTemporaryValue = false; + } - // line in sequence - int dx = (int) (seqX + (((data.time - seqTime) / 1000.) * secondWidth)); - g.drawLine( px, py, dx, dy ); + // x/y coordinates of current data point + int dy = (int) ((height - 1) * data.value); + int dx = (int) (seqX + (((data.time - seqTime) / 1000.) * oneSecondWidth)); - if( isTemporaryValue ) - g.setColor( chartColor ); + // paint rectangle to indicate data point + g.setColor( dataPointColor ); + g.drawRect( dx - hitOffset, dy - hitOffset, hitOffset * 2, hitOffset * 2 ); - // small vertical lines to indicate data point - g.drawLine( px, py - s1, px, py + s1 ); - g.drawLine( dx, dy - s1, dx, dy + s1 ); + // remember data point for tooltip + lastPoints.add( new Point( dx, dy ) ); + lastDatas.add( data ); - px = dx; - pcount++; + if( data.dot ) { + int s1 = (int) Math.round( UIScale.scale( 1 ) * scaleFactor ); + int s3 = (int) Math.round( UIScale.scale( 3 ) * scaleFactor ); + g.setColor( chartColor ); + g.fillRect( dx - s1, dy - s1, s3, s3 ); + continue; } - py = dy; - ptime = data.time; - } - } - } + if( !newSeq ) { + if( isTemporaryValue && i > lastTemporaryValueIndex ) + isTemporaryValue = false; - /** - * One or two values between two equal values are considered "temporary", - * which means that they are the target value for the following scroll animation. - */ - private boolean isTemporaryValue( List chartData, int i ) { - if( i == 0 || i == chartData.size() - 1 ) - return false; + g.setColor( isTemporaryValue ? temporaryValueColor : chartColor ); - Data dataBefore = chartData.get( i - 1 ); - Data dataAfter = chartData.get( i + 1 ); + // line in sequence + g.drawLine( px, py, dx, dy ); - if( dataBefore.dot || dataAfter.dot ) - return false; + px = dx; + pcount++; - double valueBefore = dataBefore.value; - double valueAfter = dataAfter.value; + // check next data points for "temporary" value(s) + if( !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.dot ) + 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++; + } + } + } - return valueBefore == valueAfter || - (i < chartData.size() - 2 && valueBefore == chartData.get( i + 2 ).value) || - (i > 1 && chartData.get( i - 2 ).value == valueAfter); + py = dy; + } + } } private int chartWidth() { @@ -817,7 +957,7 @@ private int chartWidth( List chartData, int[] lastSeqX ) { px = seqX; } else { // line in sequence - int dx = (int) (seqX + (((data.time - seqTime) / 1000.) * secondWidth)); + int dx = (int) (seqX + (((data.time - seqTime) / 1000.) * oneSecondWidth)); px = dx; } @@ -842,7 +982,7 @@ public Dimension getPreferredScrollableViewportSize() { @Override public int getScrollableUnitIncrement( Rectangle visibleRect, int orientation, int direction ) { - return secondWidth; + return oneSecondWidth; } @Override @@ -861,6 +1001,98 @@ public boolean getScrollableTracksViewportWidth() { 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.dot ) + buf.append( "DOT: " ); + buf.append( data.name ).append( ' ' ).append( data.value ).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(); + + // ignore methods from this class + if( className.startsWith( FlatSmoothScrollingTest.class.getName() ) ) + continue; + + int repeatCount = 0; + for( int k = j + 1; k < stackTrace.length; k++ ) { + if( !stackElement.equals( stackTrace[k] ) ) + break; + repeatCount++; + } + j += repeatCount; + + // append method + buf.append( className ) + .append( "." ) + .append( methodName ) + .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( "" ); + if( repeatCount > 0 ) + buf.append( " " ).append( repeatCount + 1 ).append( "x" ); + buf.append( "
" ); + + // break at some methods to make stack smaller + if( (className.startsWith( "java.awt.event.InvocationEvent" ) && methodName.equals( "dispatch" )) || + (className.startsWith( "java.awt.Component" ) && methodName.equals( "processMouseWheelEvent" )) || + (className.startsWith( "javax.swing.JComponent" ) && methodName.equals( "processKeyBinding" )) ) + 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(); + } } //---- class ColorIcon ---------------------------------------------------- @@ -892,6 +1124,5 @@ public int getIconWidth() { public int getIconHeight() { return UIScale.scale( 12 ); } - } } diff --git a/flatlaf-testing/src/main/java/com/formdev/flatlaf/testing/FlatSmoothScrollingTest.jfd b/flatlaf-testing/src/main/java/com/formdev/flatlaf/testing/FlatSmoothScrollingTest.jfd index f6f3c5532..f316a84d2 100644 --- a/flatlaf-testing/src/main/java/com/formdev/flatlaf/testing/FlatSmoothScrollingTest.jfd +++ b/flatlaf-testing/src/main/java/com/formdev/flatlaf/testing/FlatSmoothScrollingTest.jfd @@ -178,7 +178,7 @@ new FormModel { add( new FormContainer( "javax.swing.JPanel", new FormLayoutManager( class net.miginfocom.swing.MigLayout ) { "$layoutConstraints": "insets 3,hidemode 3" "$columnConstraints": "[grow,fill]" - "$rowConstraints": "[300,grow,fill]" + "$rowConstraints": "[100:300,grow,fill]" } ) { name: "panel3" add( new FormContainer( "javax.swing.JScrollPane", new FormLayoutManager( class javax.swing.JScrollPane ) ) { @@ -196,16 +196,71 @@ new FormModel { }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { "value": "cell 0 1" } ) - add( new FormComponent( "javax.swing.JLabel" ) { - name: "label1" - "text": "X: time (200ms per line) / Y: scroll bar value (10% per line)" + 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: "panel4" + 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: "rectsLabel" + "text": "Rectangles: scrollbar values (mouse hover shows stack)" + auxiliary() { + "JavaCodeGenerator.variableLocal": true + } + }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { + "value": "cell 1 0" + } ) + add( new FormComponent( "javax.swing.JLabel" ) { + name: "yLabel" + "text": "Y: scroll bar value (10% per line)" + auxiliary() { + "JavaCodeGenerator.variableLocal": true + } + }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { + "value": "cell 0 1" + } ) + add( new FormComponent( "javax.swing.JLabel" ) { + name: "dotsLabel" + "text": "Dots: disabled blitting mode in JViewport" + auxiliary() { + "JavaCodeGenerator.variableLocal": true + } + }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { + "value": "cell 1 1" + } ) }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { "value": "cell 0 2" } ) + 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 2,alignx right,growx 0" + } ) + add( new FormComponent( "javax.swing.JSlider" ) { + name: "oneSecondWidthSlider" + "minimum": 1000 + "maximum": 10000 + addEvent( new FormEvent( "javax.swing.event.ChangeListener", "stateChanged", "oneSecondWidthChanged", false ) ) + }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { + "value": "cell 0 2,alignx right,growx 0,wmax 100" + } ) add( new FormComponent( "javax.swing.JCheckBox" ) { name: "updateChartDelayedCheckBox" "text": "Update chart delayed" - "mnemonic": 85 + "mnemonic": 80 "selected": true addEvent( new FormEvent( "java.awt.event.ActionListener", "actionPerformed", "updateChartDelayedChanged", false ) ) }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { From 3628a03c9de0bf3166021b2dcf0ebb7f36f5f403 Mon Sep 17 00:00:00 2001 From: Karl Tauber Date: Thu, 24 Aug 2023 11:54:32 +0200 Subject: [PATCH 23/30] introduced FlatUIAction --- .../com/formdev/flatlaf/ui/FlatMenuBarUI.java | 9 ++- .../formdev/flatlaf/ui/FlatTabbedPaneUI.java | 21 +------ .../com/formdev/flatlaf/ui/FlatUIAction.java | 62 +++++++++++++++++++ 3 files changed, 70 insertions(+), 22 deletions(-) create mode 100644 flatlaf-core/src/main/java/com/formdev/flatlaf/ui/FlatUIAction.java diff --git a/flatlaf-core/src/main/java/com/formdev/flatlaf/ui/FlatMenuBarUI.java b/flatlaf-core/src/main/java/com/formdev/flatlaf/ui/FlatMenuBarUI.java index 4cdb44fa3..7bf0901f6 100644 --- a/flatlaf-core/src/main/java/com/formdev/flatlaf/ui/FlatMenuBarUI.java +++ b/flatlaf-core/src/main/java/com/formdev/flatlaf/ui/FlatMenuBarUI.java @@ -27,7 +27,6 @@ import java.beans.PropertyChangeListener; import java.util.Map; import java.util.concurrent.atomic.AtomicBoolean; -import javax.swing.AbstractAction; import javax.swing.ActionMap; import javax.swing.BoxLayout; import javax.swing.JComponent; @@ -149,7 +148,7 @@ protected void installKeyboardActions() { map = new ActionMapUIResource(); SwingUtilities.replaceUIActionMap( menuBar, map ); } - map.put( "takeFocus", new TakeFocus() ); + map.put( "takeFocus", new TakeFocus( "takeFocus" ) ); } /** @since 2 */ @@ -373,8 +372,12 @@ public void layoutContainer( Container target ) { * On other platforms, the popup of the first menu is shown. */ private static class TakeFocus - extends AbstractAction + extends FlatUIAction { + public TakeFocus( String name ) { + super( name ); + } + @Override public void actionPerformed( ActionEvent e ) { JMenuBar menuBar = (JMenuBar) e.getSource(); diff --git a/flatlaf-core/src/main/java/com/formdev/flatlaf/ui/FlatTabbedPaneUI.java b/flatlaf-core/src/main/java/com/formdev/flatlaf/ui/FlatTabbedPaneUI.java index 52bc470c8..c39510d95 100644 --- a/flatlaf-core/src/main/java/com/formdev/flatlaf/ui/FlatTabbedPaneUI.java +++ b/flatlaf-core/src/main/java/com/formdev/flatlaf/ui/FlatTabbedPaneUI.java @@ -3520,10 +3520,8 @@ else if( direction == EAST || direction == SOUTH ) //---- class RunWithOriginalLayoutManagerDelegateAction ------------------- private static class RunWithOriginalLayoutManagerDelegateAction - implements Action + extends FlatUIAction { - private final Action delegate; - static void install( ActionMap map, String key ) { Action oldAction = map.get( key ); if( oldAction == null || oldAction instanceof RunWithOriginalLayoutManagerDelegateAction ) @@ -3533,24 +3531,9 @@ static void install( ActionMap map, String key ) { } private RunWithOriginalLayoutManagerDelegateAction( Action delegate ) { - this.delegate = delegate; - } - - @Override - public Object getValue( String key ) { - return delegate.getValue( key ); + super( delegate ); } - @Override - public boolean isEnabled() { - return delegate.isEnabled(); - } - - @Override public void putValue( String key, Object value ) {} - @Override public void setEnabled( boolean b ) {} - @Override public void addPropertyChangeListener( PropertyChangeListener listener ) {} - @Override public void removePropertyChangeListener( PropertyChangeListener listener ) {} - @Override public void actionPerformed( ActionEvent e ) { JTabbedPane tabbedPane = (JTabbedPane) e.getSource(); diff --git a/flatlaf-core/src/main/java/com/formdev/flatlaf/ui/FlatUIAction.java b/flatlaf-core/src/main/java/com/formdev/flatlaf/ui/FlatUIAction.java new file mode 100644 index 000000000..b509c471b --- /dev/null +++ b/flatlaf-core/src/main/java/com/formdev/flatlaf/ui/FlatUIAction.java @@ -0,0 +1,62 @@ +/* + * 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 + * + * http://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.ui; + +import java.beans.PropertyChangeListener; +import javax.swing.Action; + +/** + * Base class for UI actions used in ActionMap. + * (similar to class sun.swing.UIAction) + * + * @author Karl Tauber + * @since 3.3 + */ +public abstract class FlatUIAction + implements Action +{ + protected final String name; + protected final Action delegate; + + protected FlatUIAction( String name ) { + this.name = name; + this.delegate = null; + } + + protected FlatUIAction( Action delegate ) { + this.name = null; + this.delegate = delegate; + } + + @Override + public Object getValue( String key ) { + if( key == NAME && delegate == null ) + return name; + return (delegate != null) ? delegate.getValue( key ) : null; + } + + @Override + public boolean isEnabled() { + return (delegate != null) ? delegate.isEnabled() : true; + } + + // do nothing in following methods because this class is immutable + @Override public void putValue( String key, Object value ) {} + @Override public void setEnabled( boolean b ) {} + @Override public void addPropertyChangeListener( PropertyChangeListener listener ) {} + @Override public void removePropertyChangeListener( PropertyChangeListener listener ) {} +} From 542e7d5f60c863770d657ec88a430ca0fedae70c Mon Sep 17 00:00:00 2001 From: Karl Tauber Date: Thu, 24 Aug 2023 22:38:52 +0200 Subject: [PATCH 24/30] Smooth Scrolling: fixes too slow repeating block (page) scrolling (e.g. hold down PageUp key) for Tree, TextArea, TextPane and EditorPane --- .../formdev/flatlaf/ui/FlatEditorPaneUI.java | 21 ++++ .../formdev/flatlaf/ui/FlatScrollBarUI.java | 28 +---- .../formdev/flatlaf/ui/FlatScrollPaneUI.java | 117 +++++++++++++++++- .../formdev/flatlaf/ui/FlatTextAreaUI.java | 6 + .../formdev/flatlaf/ui/FlatTextPaneUI.java | 11 ++ .../com/formdev/flatlaf/ui/FlatTreeUI.java | 16 +++ 6 files changed, 172 insertions(+), 27 deletions(-) diff --git a/flatlaf-core/src/main/java/com/formdev/flatlaf/ui/FlatEditorPaneUI.java b/flatlaf-core/src/main/java/com/formdev/flatlaf/ui/FlatEditorPaneUI.java index 2614ddee0..925d337b6 100644 --- a/flatlaf-core/src/main/java/com/formdev/flatlaf/ui/FlatEditorPaneUI.java +++ b/flatlaf-core/src/main/java/com/formdev/flatlaf/ui/FlatEditorPaneUI.java @@ -31,6 +31,7 @@ import javax.swing.plaf.ComponentUI; import javax.swing.plaf.basic.BasicEditorPaneUI; import javax.swing.text.Caret; +import javax.swing.text.DefaultEditorKit; import javax.swing.text.JTextComponent; import com.formdev.flatlaf.FlatClientProperties; import com.formdev.flatlaf.ui.FlatStylingSupport.Styleable; @@ -145,6 +146,21 @@ protected void uninstallListeners() { focusListener = null; } + @Override + protected void installKeyboardActions() { + super.installKeyboardActions(); + installKeyboardActions( getComponent() ); + } + + static void installKeyboardActions( JTextComponent c ) { + FlatScrollPaneUI.installSmoothScrollingDelegateActions( c, false, + /* page-down */ DefaultEditorKit.pageDownAction, // PAGE_DOWN + /* page-up */ DefaultEditorKit.pageUpAction, // PAGE_UP + /* DefaultEditorKit.selectionPageDownAction */ "selection-page-down", // shift PAGE_DOWN + /* DefaultEditorKit.selectionPageUpAction */ "selection-page-up" // shift PAGE_UP + ); + } + @Override protected Caret createCaret() { return new FlatCaret( null, false ); @@ -159,6 +175,11 @@ protected void propertyChange( PropertyChangeEvent e ) { super.propertyChange( e ); propertyChange( getComponent(), e, this::installStyle ); + + // BasicEditorPaneUI.propertyChange() re-applied actions from editor kit, + // which removed our delegate actions + if( "editorKit".equals( propertyName ) ) + installKeyboardActions( getComponent() ); } static void propertyChange( JTextComponent c, PropertyChangeEvent e, Runnable installStyle ) { diff --git a/flatlaf-core/src/main/java/com/formdev/flatlaf/ui/FlatScrollBarUI.java b/flatlaf-core/src/main/java/com/formdev/flatlaf/ui/FlatScrollBarUI.java index f999e3991..7b042e63a 100644 --- a/flatlaf-core/src/main/java/com/formdev/flatlaf/ui/FlatScrollBarUI.java +++ b/flatlaf-core/src/main/java/com/formdev/flatlaf/ui/FlatScrollBarUI.java @@ -34,7 +34,6 @@ import javax.swing.JComponent; import javax.swing.JScrollBar; import javax.swing.JScrollPane; -import javax.swing.JViewport; import javax.swing.SwingUtilities; import javax.swing.UIManager; import javax.swing.plaf.ComponentUI; @@ -481,7 +480,8 @@ public void runAndSetValueAnimated( Runnable r ) { // remember current scrollbar value so that we can start scroll animation from there int oldValue = scrollbar.getValue(); - runWithoutBlitting( scrollbar.getParent(), () ->{ + // run given runnable, which computes and sets the new scrollbar value + FlatScrollPaneUI.runWithoutBlitting( scrollbar.getParent(), () ->{ // if invoked while animation is running, calculation of new value // should start at the previous target value if( targetValue != Integer.MIN_VALUE ) @@ -510,32 +510,16 @@ public void runAndSetValueAnimated( Runnable r ) { inRunAndSetValueAnimated = false; } - private void runWithoutBlitting( Container scrollPane, Runnable r ) { - // prevent the viewport to immediately repaint using blitting - JViewport viewport = null; - int oldScrollMode = 0; - if( scrollPane instanceof JScrollPane ) { - viewport = ((JScrollPane) scrollPane).getViewport(); - if( viewport != null ) { - oldScrollMode = viewport.getScrollMode(); - viewport.setScrollMode( JViewport.BACKINGSTORE_SCROLL_MODE ); - } - } - - try { - r.run(); - } finally { - if( viewport != null ) - viewport.setScrollMode( oldScrollMode ); - } - } - private boolean inRunAndSetValueAnimated; private Animator animator; private int startValue = Integer.MIN_VALUE; private int targetValue = Integer.MIN_VALUE; private boolean useValueIsAdjusting = true; + int getTargetValue() { + return targetValue; + } + public void setValueAnimated( int initialValue, int value ) { // do some check if animation already running if( animator != null && animator.isRunning() && targetValue != Integer.MIN_VALUE ) { diff --git a/flatlaf-core/src/main/java/com/formdev/flatlaf/ui/FlatScrollPaneUI.java b/flatlaf-core/src/main/java/com/formdev/flatlaf/ui/FlatScrollPaneUI.java index c59e1ed26..d4b67c9bf 100644 --- a/flatlaf-core/src/main/java/com/formdev/flatlaf/ui/FlatScrollPaneUI.java +++ b/flatlaf-core/src/main/java/com/formdev/flatlaf/ui/FlatScrollPaneUI.java @@ -17,10 +17,12 @@ package com.formdev.flatlaf.ui; import java.awt.Component; +import java.awt.Container; import java.awt.Graphics; import java.awt.Insets; import java.awt.KeyboardFocusManager; import java.awt.Rectangle; +import java.awt.event.ActionEvent; import java.awt.event.ContainerEvent; import java.awt.event.ContainerListener; import java.awt.event.FocusEvent; @@ -31,6 +33,8 @@ import java.beans.PropertyChangeListener; import java.util.Map; import java.util.concurrent.atomic.AtomicBoolean; +import javax.swing.Action; +import javax.swing.ActionMap; import javax.swing.BorderFactory; import javax.swing.JButton; import javax.swing.JComponent; @@ -458,8 +462,8 @@ protected void syncScrollPaneWithViewport() { // if the viewport has been scrolled by using JComponent.scrollRectToVisible() // (e.g. by moving selection), then it is necessary to update the scroll bar values if( isSmoothScrollingEnabled() ) { - runAndSyncScrollBarValueAnimated( scrollpane.getVerticalScrollBar(), 0, () -> { - runAndSyncScrollBarValueAnimated( scrollpane.getHorizontalScrollBar(), 1, () -> { + runAndSyncScrollBarValueAnimated( scrollpane.getVerticalScrollBar(), 0, false, () -> { + runAndSyncScrollBarValueAnimated( scrollpane.getHorizontalScrollBar(), 1, false, () -> { super.syncScrollPaneWithViewport(); } ); } ); @@ -467,8 +471,30 @@ protected void syncScrollPaneWithViewport() { super.syncScrollPaneWithViewport(); } - private void runAndSyncScrollBarValueAnimated( JScrollBar sb, int i, Runnable r ) { - if( inRunAndSyncValueAnimated[i] || sb == null ) { + /** + * Runs the given runnable, if smooth scrolling is enabled, with disabled + * viewport blitting mode and with scroll bar value set to "target" value. + * This is necessary when calculating new view position during animation. + * Otherwise calculation would use wrong view position and (repeating) scrolling + * would be much slower than without smooth scrolling. + */ + private void runWithScrollBarsTargetValues( boolean blittingOnly, Runnable r ) { + if( isSmoothScrollingEnabled() ) { + runWithoutBlitting( scrollpane, () -> { + if( blittingOnly ) + r.run(); + else { + runAndSyncScrollBarValueAnimated( scrollpane.getVerticalScrollBar(), 0, true, () -> { + runAndSyncScrollBarValueAnimated( scrollpane.getHorizontalScrollBar(), 1, true, r ); + } ); + } + } ); + } else + r.run(); + } + + private void runAndSyncScrollBarValueAnimated( JScrollBar sb, int i, boolean useTargetValue, Runnable r ) { + if( inRunAndSyncValueAnimated[i] || sb == null || !(sb.getUI() instanceof FlatScrollBarUI) ) { r.run(); return; } @@ -480,6 +506,10 @@ private void runAndSyncScrollBarValueAnimated( JScrollBar sb, int i, Runnable r int oldMinimum = sb.getMinimum(); int oldMaximum = sb.getMaximum(); + FlatScrollBarUI ui = (FlatScrollBarUI) sb.getUI(); + if( useTargetValue && ui.getTargetValue() != Integer.MIN_VALUE ) + sb.setValue( ui.getTargetValue() ); + r.run(); int newValue = sb.getValue(); @@ -490,7 +520,7 @@ private void runAndSyncScrollBarValueAnimated( JScrollBar sb, int i, Runnable r sb.getMaximum() == oldMaximum && sb.getUI() instanceof FlatScrollBarUI ) { - ((FlatScrollBarUI)sb.getUI()).setValueAnimated( oldValue, newValue ); + ui.setValueAnimated( oldValue, newValue ); } inRunAndSyncValueAnimated[i] = false; @@ -498,6 +528,53 @@ private void runAndSyncScrollBarValueAnimated( JScrollBar sb, int i, Runnable r private final boolean[] inRunAndSyncValueAnimated = new boolean[2]; + /** + * Runs the given runnable with disabled viewport blitting mode. + * If blitting mode is enabled, the viewport immediately repaints parts of the + * view if the view position is changed via JViewport.setViewPosition(). + * This causes scrolling artifacts if smooth scrolling is enabled and the view position + * is "temporary" changed to its new target position, changed back to its old position + * and again moved animated to the target position. + */ + static void runWithoutBlitting( Container scrollPane, Runnable r ) { + // prevent the viewport to immediately repaint using blitting + JViewport viewport = null; + int oldScrollMode = 0; + if( scrollPane instanceof JScrollPane ) { + viewport = ((JScrollPane) scrollPane).getViewport(); + if( viewport != null ) { + oldScrollMode = viewport.getScrollMode(); + viewport.setScrollMode( JViewport.BACKINGSTORE_SCROLL_MODE ); + } + } + + try { + r.run(); + } finally { + if( viewport != null ) + viewport.setScrollMode( oldScrollMode ); + } + } + + public static void installSmoothScrollingDelegateActions( JComponent c, boolean blittingOnly, String... actionKeys ) { + // get shared action map, used for all components of same type + ActionMap map = SwingUtilities.getUIActionMap( c ); + if( map == null ) + return; + + // install actions, but only if not already installed + for( String actionKey : actionKeys ) + installSmoothScrollingDelegateAction( map, blittingOnly, actionKey ); + } + + private static void installSmoothScrollingDelegateAction( ActionMap map, boolean blittingOnly, String actionKey ) { + Action oldAction = map.get( actionKey ); + if( oldAction == null || oldAction instanceof SmoothScrollingDelegateAction ) + return; // not found or already installed + + map.put( actionKey, new SmoothScrollingDelegateAction( oldAction, blittingOnly ) ); + } + //---- class Handler ------------------------------------------------------ /** @@ -529,4 +606,34 @@ public void focusLost( FocusEvent e ) { scrollpane.repaint(); } } + + //---- class SmoothScrollingDelegateAction -------------------------------- + + /** + * Used to run component actions with disabled blitting mode and + * with scroll bar target values. + */ + private static class SmoothScrollingDelegateAction + extends FlatUIAction + { + private final boolean blittingOnly; + + private SmoothScrollingDelegateAction( Action delegate, boolean blittingOnly ) { + super( delegate ); + this.blittingOnly = blittingOnly; + } + + @Override + public void actionPerformed( ActionEvent e ) { + Object source = e.getSource(); + JScrollPane scrollPane = (source instanceof Component) + ? (JScrollPane) SwingUtilities.getAncestorOfClass( JScrollPane.class, (Component) source ) + : null; + if( scrollPane != null && scrollPane.getUI() instanceof FlatScrollPaneUI ) { + ((FlatScrollPaneUI)scrollPane.getUI()).runWithScrollBarsTargetValues( blittingOnly, + () -> delegate.actionPerformed( e ) ); + } else + delegate.actionPerformed( e ); + } + } } diff --git a/flatlaf-core/src/main/java/com/formdev/flatlaf/ui/FlatTextAreaUI.java b/flatlaf-core/src/main/java/com/formdev/flatlaf/ui/FlatTextAreaUI.java index 6b5cd3f8f..3c4d09586 100644 --- a/flatlaf-core/src/main/java/com/formdev/flatlaf/ui/FlatTextAreaUI.java +++ b/flatlaf-core/src/main/java/com/formdev/flatlaf/ui/FlatTextAreaUI.java @@ -141,6 +141,12 @@ protected void uninstallListeners() { focusListener = null; } + @Override + protected void installKeyboardActions() { + super.installKeyboardActions(); + FlatEditorPaneUI.installKeyboardActions( getComponent() ); + } + @Override protected Caret createCaret() { return new FlatCaret( null, false ); diff --git a/flatlaf-core/src/main/java/com/formdev/flatlaf/ui/FlatTextPaneUI.java b/flatlaf-core/src/main/java/com/formdev/flatlaf/ui/FlatTextPaneUI.java index 1452ebc77..b9986ed94 100644 --- a/flatlaf-core/src/main/java/com/formdev/flatlaf/ui/FlatTextPaneUI.java +++ b/flatlaf-core/src/main/java/com/formdev/flatlaf/ui/FlatTextPaneUI.java @@ -142,6 +142,12 @@ protected void uninstallListeners() { focusListener = null; } + @Override + protected void installKeyboardActions() { + super.installKeyboardActions(); + FlatEditorPaneUI.installKeyboardActions( getComponent() ); + } + @Override protected Caret createCaret() { return new FlatCaret( null, false ); @@ -156,6 +162,11 @@ protected void propertyChange( PropertyChangeEvent e ) { super.propertyChange( e ); FlatEditorPaneUI.propertyChange( getComponent(), e, this::installStyle ); + + // BasicEditorPaneUI.propertyChange() re-applied actions from editor kit, + // which removed our delegate actions + if( "editorKit".equals( propertyName ) ) + FlatEditorPaneUI.installKeyboardActions( getComponent() ); } /** @since 2 */ diff --git a/flatlaf-core/src/main/java/com/formdev/flatlaf/ui/FlatTreeUI.java b/flatlaf-core/src/main/java/com/formdev/flatlaf/ui/FlatTreeUI.java index 31d898e22..e3522d244 100644 --- a/flatlaf-core/src/main/java/com/formdev/flatlaf/ui/FlatTreeUI.java +++ b/flatlaf-core/src/main/java/com/formdev/flatlaf/ui/FlatTreeUI.java @@ -238,6 +238,22 @@ protected void uninstallDefaults() { oldStyleValues = null; } + @Override + protected void installKeyboardActions() { + super.installKeyboardActions(); + + FlatScrollPaneUI.installSmoothScrollingDelegateActions( tree, false, + "scrollDownChangeSelection", // PAGE_DOWN + "scrollUpChangeSelection", // PAGE_UP + "scrollDownChangeLead", // ctrl PAGE_DOWN + "scrollUpChangeLead", // ctrl PAGE_UP + "scrollDownExtendSelection", // shift PAGE_DOWN, shift ctrl PAGE_DOWN + "scrollUpExtendSelection", // shift PAGE_UP, shift ctrl PAGE_UP + "selectNextChangeLead", // ctrl DOWN + "selectPreviousChangeLead" // ctrl UP + ); + } + @Override protected void updateRenderer() { super.updateRenderer(); From 6dfc204e40f7134e86f5e27508689430a0c1a20c Mon Sep 17 00:00:00 2001 From: Karl Tauber Date: Fri, 25 Aug 2023 00:06:43 +0200 Subject: [PATCH 25/30] SmoothScrollingTest added (from https://github.com/JFormDesigner/FlatLaf/pull/683#issuecomment-1585667066) --- .../testing/contrib/SmoothScrollingTest.java | 305 ++++++++++++++++++ 1 file changed, 305 insertions(+) create mode 100644 flatlaf-testing/src/main/java/com/formdev/flatlaf/testing/contrib/SmoothScrollingTest.java diff --git a/flatlaf-testing/src/main/java/com/formdev/flatlaf/testing/contrib/SmoothScrollingTest.java b/flatlaf-testing/src/main/java/com/formdev/flatlaf/testing/contrib/SmoothScrollingTest.java new file mode 100644 index 000000000..204b24bc7 --- /dev/null +++ b/flatlaf-testing/src/main/java/com/formdev/flatlaf/testing/contrib/SmoothScrollingTest.java @@ -0,0 +1,305 @@ +package com.formdev.flatlaf.testing.contrib; + +import java.awt.BorderLayout; +import java.awt.Component; +import java.awt.Dimension; +import java.awt.FlowLayout; +import java.awt.GridLayout; +import java.awt.Point; +import java.awt.Rectangle; +import java.awt.event.ItemEvent; + +import javax.swing.BorderFactory; +import javax.swing.Box; +import javax.swing.JButton; +import javax.swing.JCheckBox; +import javax.swing.JComboBox; +import javax.swing.JFrame; +import javax.swing.JLayeredPane; +import javax.swing.JList; +import javax.swing.JPanel; +import javax.swing.JPopupMenu; +import javax.swing.JRootPane; +import javax.swing.JScrollPane; +import javax.swing.JTable; +import javax.swing.JTree; +import javax.swing.JViewport; +import javax.swing.ListCellRenderer; +import javax.swing.Scrollable; +import javax.swing.SwingConstants; +import javax.swing.SwingUtilities; +import javax.swing.ToolTipManager; +import javax.swing.UIManager; +import javax.swing.table.AbstractTableModel; +import javax.swing.table.TableModel; +import javax.swing.tree.DefaultMutableTreeNode; +import javax.swing.tree.DefaultTreeModel; +import javax.swing.tree.TreeModel; + +import com.formdev.flatlaf.FlatClientProperties; +import com.formdev.flatlaf.FlatLightLaf; + +/** + * from https://github.com/JFormDesigner/FlatLaf/pull/683#issuecomment-1585667066 + * + * @author Chrriis + */ +public class SmoothScrollingTest { + + private static class CustomTree extends JTree { + + public CustomTree() { + super(getDefaultTreeModel()); + } + + protected static TreeModel getDefaultTreeModel() { + DefaultMutableTreeNode root = new DefaultMutableTreeNode("JTree"); + for(int i=0; i<1000; i++) { + DefaultMutableTreeNode parent; + parent = new DefaultMutableTreeNode("colors-" + i); + root.add(parent); + parent.add(new DefaultMutableTreeNode("blue")); + parent.add(new DefaultMutableTreeNode("violet")); + parent.add(new DefaultMutableTreeNode("red")); + parent.add(new DefaultMutableTreeNode("yellow")); + parent = new DefaultMutableTreeNode("sports-" + i); + root.add(parent); + parent.add(new DefaultMutableTreeNode("basketball")); + parent.add(new DefaultMutableTreeNode("soccer")); + parent.add(new DefaultMutableTreeNode("football")); + parent.add(new DefaultMutableTreeNode("hockey")); + parent = new DefaultMutableTreeNode("food-" + i); + root.add(parent); + parent.add(new DefaultMutableTreeNode("hot dogs")); + parent.add(new DefaultMutableTreeNode("pizza")); + parent.add(new DefaultMutableTreeNode("ravioli")); + parent.add(new DefaultMutableTreeNode("bananas")); + } + return new DefaultTreeModel(root); + } + } + + private static class CustomTable extends JTable { + public CustomTable() { + super(createDefaultTableModel()); + setAutoResizeMode(JTable.AUTO_RESIZE_OFF); + setCellSelectionEnabled(true); + } + private static TableModel createDefaultTableModel() { + int columnChunkCount = 90; + Object[][] data = new Object[1000][3 * columnChunkCount]; + String[] prefixes = {"", " ", " ", " "}; + String[] titles = new String[columnChunkCount * 3]; + for(int j=0; j getColumnClass(int columnIndex) { + return columnIndex % 3 == 2 ? Boolean.class : String.class; + } + }; + return tableModel; + } + } + + private static class ScrollableCustomPane extends JPanel implements Scrollable { + public ScrollableCustomPane() { + super(new GridLayout(100, 0)); + for(int i=0; i<10000; i++) { + add(new JButton("Button " + (i + 1))); + } + } + @Override + public Dimension getPreferredScrollableViewportSize() { + return getPreferredSize(); + } + @Override + public boolean getScrollableTracksViewportWidth() { + return false; + } + @Override + public boolean getScrollableTracksViewportHeight() { + return false; + } + @Override + public int getScrollableUnitIncrement(Rectangle visibleRect, int orientation, int direction) { + Dimension referenceSize = getComponent(0).getSize(); + switch(orientation) { + case SwingConstants.VERTICAL: return referenceSize.height; + case SwingConstants.HORIZONTAL: return referenceSize.width; + } + return 20; + } + @Override + public int getScrollableBlockIncrement(Rectangle visibleRect, int orientation, int direction) { + Dimension referenceSize = getComponent(0).getSize(); + switch(orientation) { + case SwingConstants.VERTICAL: return referenceSize.height * 10; + case SwingConstants.HORIZONTAL: return referenceSize.width * 5; + } + return 100; + } + } + + public static void main(String[] args) throws Exception { + UIManager.setLookAndFeel(new FlatLightLaf()); +// UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName()); + UIManager.getDefaults().put("ScrollBar.showButtons", true); + ToolTipManager.sharedInstance().setDismissDelay(Integer.MAX_VALUE); + JFrame frame = new JFrame(); + frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); + JPanel contentPane = new JPanel(new BorderLayout()); + contentPane.setBorder(BorderFactory.createEmptyBorder(10, 10, 10, 10)); + JCheckBox smoothCheckBox = new JCheckBox("Smooth", true); + JComboBox scrollModeComboBox = new JComboBox<>(new Integer[] {JViewport.BLIT_SCROLL_MODE, JViewport.BACKINGSTORE_SCROLL_MODE, JViewport.SIMPLE_SCROLL_MODE}); + JCheckBox blitBlockCheckBox = new JCheckBox("Prevent Blit"); + @SuppressWarnings( "rawtypes" ) + ListCellRenderer defaultComboRenderer = scrollModeComboBox.getRenderer(); + scrollModeComboBox.setRenderer(new ListCellRenderer() { + @SuppressWarnings( "unchecked" ) + @Override + public Component getListCellRendererComponent(JList list, Integer value, int index, boolean isSelected, boolean cellHasFocus) { + String sValue = null; + switch(value) { + case JViewport.BLIT_SCROLL_MODE: sValue = "Blit"; break; + case JViewport.BACKINGSTORE_SCROLL_MODE: sValue = "Backing Store"; break; + case JViewport.SIMPLE_SCROLL_MODE: sValue = "Simple"; break; + } + return defaultComboRenderer.getListCellRendererComponent(list, sValue, index, isSelected, cellHasFocus); + } + }); + JPanel northBar = new JPanel(); + JButton lightPopupButton = new JButton("Light Popup"); + lightPopupButton.addActionListener(e -> { + JPopupMenu popupMenu = new JPopupMenu(); + popupMenu.setLightWeightPopupEnabled(true); + popupMenu.putClientProperty(FlatClientProperties.POPUP_BORDER_CORNER_RADIUS, 0); + JTable table = new CustomTable(); + table.setColumnSelectionInterval(1, 1); + table.setRowSelectionInterval(28, 28); + JScrollPane tableScrollPane = new JScrollPane(table); + tableScrollPane.setPreferredSize(new Dimension(400, 600)); + tableScrollPane.putClientProperty(FlatClientProperties.SCROLL_PANE_SMOOTH_SCROLLING, !smoothCheckBox.isSelected()? Boolean.FALSE: null); + tableScrollPane.getViewport().setScrollMode((Integer)scrollModeComboBox.getSelectedItem()); + JRootPane popupRootPane = new JRootPane(); + popupRootPane.getContentPane().add(tableScrollPane); + popupMenu.add(popupRootPane); + popupMenu.show(lightPopupButton, 0, lightPopupButton.getHeight()); + }); + northBar.add(lightPopupButton); + JButton heavyPopupButton = new JButton("Heavy Popup"); + heavyPopupButton.addActionListener(e -> { + JPopupMenu popupMenu = new JPopupMenu(); + popupMenu.setLightWeightPopupEnabled(false); + JTable table = new CustomTable(); + table.setColumnSelectionInterval(1, 1); + table.setRowSelectionInterval(28, 28); + JScrollPane tableScrollPane = new JScrollPane(table); + tableScrollPane.setPreferredSize(new Dimension(400, 600)); + tableScrollPane.putClientProperty(FlatClientProperties.SCROLL_PANE_SMOOTH_SCROLLING, !smoothCheckBox.isSelected()? Boolean.FALSE: null); + tableScrollPane.getViewport().setScrollMode((Integer)scrollModeComboBox.getSelectedItem()); + JRootPane popupRootPane = new JRootPane(); + popupRootPane.getContentPane().add(tableScrollPane); + popupMenu.add(popupRootPane); + popupMenu.show(heavyPopupButton, 0, heavyPopupButton.getHeight()); + }); + northBar.add(heavyPopupButton); + contentPane.add(northBar, BorderLayout.NORTH); + JPanel centerPane = new JPanel(new GridLayout(1, 0)); + JTree tree = new CustomTree(); + JScrollPane treeScrollPane = new JScrollPane(tree); + centerPane.add(treeScrollPane); + for(int i=tree.getRowCount()-1; i>=0; i--) { + tree.expandRow(i); + } + tree.setSelectionRow(28); + JTable table = new CustomTable(); + table.setColumnSelectionInterval(1, 1); + table.setRowSelectionInterval(28, 28); + JScrollPane tableScrollPane = new JScrollPane(table); + centerPane.add(tableScrollPane); + ScrollableCustomPane scrollableCustomPane = new ScrollableCustomPane(); + JScrollPane customPaneScrollPane = new JScrollPane(scrollableCustomPane); + centerPane.add(customPaneScrollPane); + contentPane.add(centerPane, BorderLayout.CENTER); + JPanel southBar = new JPanel(new FlowLayout()); + smoothCheckBox.addItemListener(e -> { + boolean isSmooth = e.getStateChange() == ItemEvent.SELECTED; + treeScrollPane.putClientProperty(FlatClientProperties.SCROLL_PANE_SMOOTH_SCROLLING, !isSmooth? Boolean.FALSE: null); + tableScrollPane.putClientProperty(FlatClientProperties.SCROLL_PANE_SMOOTH_SCROLLING, !isSmooth? Boolean.FALSE: null); + customPaneScrollPane.putClientProperty(FlatClientProperties.SCROLL_PANE_SMOOTH_SCROLLING, !isSmooth? Boolean.FALSE: null); + }); + southBar.add(smoothCheckBox); + southBar.add(Box.createHorizontalStrut(30)); + JButton scrollButton = new JButton("Scroll rect"); + scrollButton.addActionListener(e -> { + treeScrollPane.getViewport().setViewPosition(new Point(9, Integer.MAX_VALUE / 2)); + tableScrollPane.getViewport().setViewPosition(new Point(9, Integer.MAX_VALUE / 2)); + customPaneScrollPane.getViewport().setViewPosition(new Point(9, Integer.MAX_VALUE / 2)); + }); + southBar.add(scrollButton); + southBar.add(Box.createHorizontalStrut(30)); + scrollModeComboBox.addItemListener(e -> { + SwingUtilities.invokeLater(() -> { + int scrollMode = (Integer)scrollModeComboBox.getSelectedItem(); + treeScrollPane.getViewport().setScrollMode(scrollMode); + tableScrollPane.getViewport().setScrollMode(scrollMode); + customPaneScrollPane.getViewport().setScrollMode(scrollMode); + }); + }); + southBar.add(scrollModeComboBox); + southBar.add(Box.createHorizontalStrut(30)); + JButton xButton1 = new JButton("Blit Blocker"); + xButton1.setBounds(20, 400, xButton1.getPreferredSize().width, xButton1.getPreferredSize().height); + JButton xButton2 = new JButton("Blit Blocker"); + xButton2.setBounds(600, 400, xButton2.getPreferredSize().width, xButton2.getPreferredSize().height); + JButton xButton3 = new JButton("Blit Blocker"); + xButton3.setBounds(800, 400, xButton3.getPreferredSize().width, xButton3.getPreferredSize().height); + blitBlockCheckBox.addItemListener(e -> { + boolean isBlockingBlit = e.getStateChange() == ItemEvent.SELECTED; + JLayeredPane layeredPane = frame.getLayeredPane(); + if(isBlockingBlit) { + layeredPane.add(xButton1); + layeredPane.add(xButton2); + layeredPane.add(xButton3); + } else { + layeredPane.remove(xButton1); + layeredPane.remove(xButton2); + layeredPane.remove(xButton3); + } + layeredPane.revalidate(); + layeredPane.repaint(); + }); + southBar.add(blitBlockCheckBox); + contentPane.add(southBar, BorderLayout.SOUTH); + frame.getContentPane().add(contentPane); + frame.setSize(1200, 800); + frame.setLocationByPlatform(true); + frame.setVisible(true); + } + +} From 5cdef5409b29810e63d2ea36b897e0b267977cf6 Mon Sep 17 00:00:00 2001 From: Karl Tauber Date: Fri, 25 Aug 2023 15:24:28 +0200 Subject: [PATCH 26/30] Smooth Scrolling: fixed jittery scrolling with trackpad or Magic Mouse (if smooth scrolling is enabled) --- .../formdev/flatlaf/ui/FlatScrollPaneUI.java | 27 +++++++++++++++++-- 1 file changed, 25 insertions(+), 2 deletions(-) diff --git a/flatlaf-core/src/main/java/com/formdev/flatlaf/ui/FlatScrollPaneUI.java b/flatlaf-core/src/main/java/com/formdev/flatlaf/ui/FlatScrollPaneUI.java index d4b67c9bf..3e42cece3 100644 --- a/flatlaf-core/src/main/java/com/formdev/flatlaf/ui/FlatScrollPaneUI.java +++ b/flatlaf-core/src/main/java/com/formdev/flatlaf/ui/FlatScrollPaneUI.java @@ -144,8 +144,7 @@ protected MouseWheelListener createMouseWheelListener() { scrollpane.isWheelScrollingEnabled() ) { if( e.getScrollType() == MouseWheelEvent.WHEEL_UNIT_SCROLL && - e.getPreciseWheelRotation() != 0 && - e.getPreciseWheelRotation() != e.getWheelRotation() ) + isPreciseWheelEvent( e ) ) { // precise scrolling mouseWheelMovedPrecise( e ); @@ -179,6 +178,30 @@ protected boolean isSmoothScrollingEnabled() { return UIManager.getBoolean( "ScrollPane.smoothScrolling" ); } + private long lastPreciseWheelWhen; + + private boolean isPreciseWheelEvent( MouseWheelEvent e ) { + double preciseWheelRotation = e.getPreciseWheelRotation(); + if( preciseWheelRotation != 0 && preciseWheelRotation != e.getWheelRotation() ) { + // precise wheel event + lastPreciseWheelWhen = e.getWhen(); + return true; + } + + // If a non-precise wheel event occurs shortly after a precise wheel event, + // then it is probably still a precise wheel but the precise value + // is by chance an integer value (e.g. 1.0 or 2.0). + // Not handling this special case, would start an animation for smooth scrolling, + // which would be interrupted soon when the next precise wheel event occurs. + // This would result in jittery scrolling. E.g. on a MacBook using Trackpad or Magic Mouse. + if( e.getWhen() - lastPreciseWheelWhen < 1000 ) + return true; + + // non-precise wheel event + lastPreciseWheelWhen = 0; + return false; + } + private void mouseWheelMovedPrecise( MouseWheelEvent e ) { // return if there is no viewport JViewport viewport = scrollpane.getViewport(); From 04658c2ef02e4d665a2d6bac056a9daa8921b8f9 Mon Sep 17 00:00:00 2001 From: Karl Tauber Date: Fri, 25 Aug 2023 17:43:58 +0200 Subject: [PATCH 27/30] SmoothScrollingTest: fixed error reported by Error Prone --- .../formdev/flatlaf/testing/contrib/SmoothScrollingTest.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/flatlaf-testing/src/main/java/com/formdev/flatlaf/testing/contrib/SmoothScrollingTest.java b/flatlaf-testing/src/main/java/com/formdev/flatlaf/testing/contrib/SmoothScrollingTest.java index 204b24bc7..36abd8d39 100644 --- a/flatlaf-testing/src/main/java/com/formdev/flatlaf/testing/contrib/SmoothScrollingTest.java +++ b/flatlaf-testing/src/main/java/com/formdev/flatlaf/testing/contrib/SmoothScrollingTest.java @@ -95,7 +95,7 @@ private static TableModel createDefaultTableModel() { titles[j * 3 + 1] = "Column" + j * 3 + 2; titles[j * 3 + 2] = "Column" + j * 3 + 3; for(int i=0; i Date: Sun, 27 Aug 2023 14:31:30 +0200 Subject: [PATCH 28/30] Smooth Scrolling: - fixed jittery repeating-scrolling with PageUp/Down keys when reaching the top/bottom/left/right of the viewport (see FlatScrollBarUI.setValueAnimated()) - temporary change viewport scroll mode only if it is JViewport.BLIT_SCROLL_MODE - use JViewport.SIMPLE_SCROLL_MODE when temporary disabling blitting --- .../formdev/flatlaf/ui/FlatScrollBarUI.java | 20 ++++++++++--------- .../formdev/flatlaf/ui/FlatScrollPaneUI.java | 17 ++++++---------- 2 files changed, 17 insertions(+), 20 deletions(-) diff --git a/flatlaf-core/src/main/java/com/formdev/flatlaf/ui/FlatScrollBarUI.java b/flatlaf-core/src/main/java/com/formdev/flatlaf/ui/FlatScrollBarUI.java index 7b042e63a..0b61fe7bf 100644 --- a/flatlaf-core/src/main/java/com/formdev/flatlaf/ui/FlatScrollBarUI.java +++ b/flatlaf-core/src/main/java/com/formdev/flatlaf/ui/FlatScrollBarUI.java @@ -521,23 +521,25 @@ int getTargetValue() { } public void setValueAnimated( int initialValue, int value ) { + if( useValueIsAdjusting ) + scrollbar.setValueIsAdjusting( true ); + + // (always) set scrollbar value to initial value + scrollbar.setValue( initialValue ); + // do some check if animation already running if( animator != null && animator.isRunning() && targetValue != Integer.MIN_VALUE ) { - // ignore requests if animation still running and scroll direction is the same - // and new value is within currently running animation - // (this may occur when repeat-scrolling via keyboard) + // Ignore requests if animation still running and scroll direction is the same + // and new value is within currently running animation. + // Without this check, repeating-scrolling via keyboard would become + // very slow when reaching the top/bottom/left/right of the viewport, + // because it would start a new 200ms animation to scroll a few pixels. if( value == targetValue || (value > startValue && value < targetValue) || // scroll down/right (value < startValue && value > targetValue) ) // scroll up/left return; } - if( useValueIsAdjusting ) - scrollbar.setValueIsAdjusting( true ); - - // set scrollbar value to initial value - scrollbar.setValue( initialValue ); - startValue = initialValue; targetValue = value; diff --git a/flatlaf-core/src/main/java/com/formdev/flatlaf/ui/FlatScrollPaneUI.java b/flatlaf-core/src/main/java/com/formdev/flatlaf/ui/FlatScrollPaneUI.java index 3e42cece3..2e5ee7282 100644 --- a/flatlaf-core/src/main/java/com/formdev/flatlaf/ui/FlatScrollPaneUI.java +++ b/flatlaf-core/src/main/java/com/formdev/flatlaf/ui/FlatScrollPaneUI.java @@ -561,21 +561,16 @@ private void runAndSyncScrollBarValueAnimated( JScrollBar sb, int i, boolean use */ static void runWithoutBlitting( Container scrollPane, Runnable r ) { // prevent the viewport to immediately repaint using blitting - JViewport viewport = null; - int oldScrollMode = 0; - if( scrollPane instanceof JScrollPane ) { - viewport = ((JScrollPane) scrollPane).getViewport(); - if( viewport != null ) { - oldScrollMode = viewport.getScrollMode(); - viewport.setScrollMode( JViewport.BACKINGSTORE_SCROLL_MODE ); - } - } + JViewport viewport = (scrollPane instanceof JScrollPane) ? ((JScrollPane)scrollPane).getViewport() : null; + boolean isBlitScrollMode = (viewport != null) ? viewport.getScrollMode() == JViewport.BLIT_SCROLL_MODE : false; + if( isBlitScrollMode ) + viewport.setScrollMode( JViewport.SIMPLE_SCROLL_MODE ); try { r.run(); } finally { - if( viewport != null ) - viewport.setScrollMode( oldScrollMode ); + if( isBlitScrollMode ) + viewport.setScrollMode( JViewport.BLIT_SCROLL_MODE ); } } From b32b8db97a042ff8a31133bc432f3438012a32f2 Mon Sep 17 00:00:00 2001 From: Karl Tauber Date: Mon, 28 Aug 2023 19:59:46 +0200 Subject: [PATCH 29/30] FlatSmoothScrollingTest: refactored line chart panel into own class for easier use in other test apps --- .../flatlaf/testing/FlatAnimatorTest.java | 55 +- .../flatlaf/testing/FlatAnimatorTest.jfd | 40 +- .../testing/FlatSmoothScrollingTest.java | 583 +-------------- .../testing/FlatSmoothScrollingTest.jfd | 100 +-- .../flatlaf/testing/LineChartPanel.java | 664 ++++++++++++++++++ .../flatlaf/testing/LineChartPanel.jfd | 121 ++++ 6 files changed, 818 insertions(+), 745 deletions(-) create mode 100644 flatlaf-testing/src/main/java/com/formdev/flatlaf/testing/LineChartPanel.java create mode 100644 flatlaf-testing/src/main/java/com/formdev/flatlaf/testing/LineChartPanel.jfd 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 20d26bcd6..37c1e7950 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 @@ -21,7 +21,6 @@ import java.awt.event.MouseWheelListener; import javax.swing.*; import javax.swing.border.*; -import com.formdev.flatlaf.testing.FlatSmoothScrollingTest.LineChartPanel; import com.formdev.flatlaf.ui.FlatUIUtils; import com.formdev.flatlaf.util.Animator; import com.formdev.flatlaf.util.CubicBezierEasing; @@ -46,9 +45,6 @@ public static void main( String[] args ) { FlatAnimatorTest() { initComponents(); - updateChartDelayedChanged(); - - lineChartPanel.setOneSecondWidth( 500 ); mouseWheelTestPanel.lineChartPanel = lineChartPanel; } @@ -82,14 +78,6 @@ private void startEaseInOut() { } } - private void updateChartDelayedChanged() { - lineChartPanel.setUpdateDelayed( updateChartDelayedCheckBox.isSelected() ); - } - - private void clearChart() { - lineChartPanel.clear(); - } - private void initComponents() { // JFormDesigner - Component initialization - DO NOT MODIFY //GEN-BEGIN:initComponents JLabel linearLabel = new JLabel(); @@ -99,11 +87,7 @@ private void initComponents() { startButton = new JButton(); JLabel mouseWheelTestLabel = new JLabel(); mouseWheelTestPanel = new FlatAnimatorTest.MouseWheelTestPanel(); - JScrollPane lineChartScrollPane = new JScrollPane(); - lineChartPanel = new FlatSmoothScrollingTest.LineChartPanel(); - JLabel lineChartInfoLabel = new JLabel(); - updateChartDelayedCheckBox = new JCheckBox(); - JButton clearChartButton = new JButton(); + lineChartPanel = new LineChartPanel(); //======== this ======== setLayout(new MigLayout( @@ -116,8 +100,7 @@ private void initComponents() { "[]" + "[]para" + "[top]" + - "[400,grow,fill]" + - "[]")); + "[400,grow,fill]")); //---- linearLabel ---- linearLabel.setText("Linear:"); @@ -150,28 +133,9 @@ private void initComponents() { mouseWheelTestPanel.setBorder(new LineBorder(Color.red)); add(mouseWheelTestPanel, "cell 1 3,height 100"); - //======== lineChartScrollPane ======== - { - lineChartScrollPane.putClientProperty("JScrollPane.smoothScrolling", false); - lineChartScrollPane.setViewportView(lineChartPanel); - } - add(lineChartScrollPane, "cell 0 4 2 1"); - - //---- lineChartInfoLabel ---- - lineChartInfoLabel.setText("X: time (500ms per line) / Y: value (10% per line)"); - add(lineChartInfoLabel, "cell 0 5 2 1"); - - //---- updateChartDelayedCheckBox ---- - updateChartDelayedCheckBox.setText("Update chart delayed"); - updateChartDelayedCheckBox.setMnemonic('U'); - updateChartDelayedCheckBox.addActionListener(e -> updateChartDelayedChanged()); - add(updateChartDelayedCheckBox, "cell 0 5 2 1,alignx right,growx 0"); - - //---- clearChartButton ---- - clearChartButton.setText("Clear Chart"); - clearChartButton.setMnemonic('C'); - clearChartButton.addActionListener(e -> clearChart()); - add(clearChartButton, "cell 0 5 2 1,alignx right,growx 0"); + //---- lineChartPanel ---- + lineChartPanel.setUpdateChartDelayed(false); + add(lineChartPanel, "cell 0 4 2 1"); // JFormDesigner - End of component initialization //GEN-END:initComponents } @@ -180,8 +144,7 @@ private void initComponents() { private JScrollBar easeInOutScrollBar; private JButton startButton; private FlatAnimatorTest.MouseWheelTestPanel mouseWheelTestPanel; - private FlatSmoothScrollingTest.LineChartPanel lineChartPanel; - private JCheckBox updateChartDelayedCheckBox; + private LineChartPanel lineChartPanel; // JFormDesigner - End of variables declaration //GEN-END:variables //---- class MouseWheelTestPanel ------------------------------------------ @@ -217,7 +180,7 @@ static class MouseWheelTestPanel value = startValue + Math.round( (targetValue - startValue) * fraction ); valueLabel.setText( String.valueOf( value ) ); - lineChartPanel.addValue( value / (double) MAX_VALUE, false, Color.red, null ); + lineChartPanel.addValue( value / (double) MAX_VALUE, value, false, Color.red, null ); }, () -> { targetValue = -1; } ); @@ -235,7 +198,7 @@ public void mouseWheelMoved( MouseWheelEvent e ) { // for unprecise wheels the rotation value is usually -1 or +1 // for precise wheels the rotation value is in range ca. -10 to +10, // depending how fast the wheel is rotated - lineChartPanel.addValue( 0.5 + (preciseWheelRotation / 20.), true, Color.red, null ); + lineChartPanel.addValue( 0.5 + (preciseWheelRotation / 20.), (int) (preciseWheelRotation * 1000), true, Color.red, null ); // increase/decrease target value if animation is in progress int newValue = (int) ((targetValue < 0 ? value : targetValue) + (STEP * preciseWheelRotation)); @@ -252,7 +215,7 @@ public void mouseWheelMoved( MouseWheelEvent e ) { value = newValue; valueLabel.setText( String.valueOf( value ) ); - lineChartPanel.addValue( value / (double) MAX_VALUE, false, Color.red, null ); + lineChartPanel.addValue( value / (double) MAX_VALUE, value, false, Color.red, null ); return; } 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 970818f39..ba9363e81 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: "15" encoding: "UTF-8" +JFDML JFormDesigner: "8.1.0.0.283" Java: "19.0.2" encoding: "UTF-8" new FormModel { contentType: "form/swing" @@ -9,7 +9,7 @@ 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": "[][][]para[top][400,grow,fill][]" + "$rowConstraints": "[][][]para[top][400,grow,fill]" } ) { name: "this" add( new FormComponent( "javax.swing.JLabel" ) { @@ -69,42 +69,14 @@ new FormModel { }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { "value": "cell 1 3,height 100" } ) - add( new FormContainer( "javax.swing.JScrollPane", new FormLayoutManager( class javax.swing.JScrollPane ) ) { - name: "lineChartScrollPane" - "$client.JScrollPane.smoothScrolling": false - add( new FormComponent( "com.formdev.flatlaf.testing.FlatSmoothScrollingTest$LineChartPanel" ) { - name: "lineChartPanel" - auxiliary() { - "JavaCodeGenerator.variableLocal": false - } - } ) - }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { - "value": "cell 0 4 2 1" - } ) - add( new FormComponent( "javax.swing.JLabel" ) { - name: "lineChartInfoLabel" - "text": "X: time (500ms per line) / Y: value (10% per line)" - }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { - "value": "cell 0 5 2 1" - } ) - add( new FormComponent( "javax.swing.JCheckBox" ) { - name: "updateChartDelayedCheckBox" - "text": "Update chart delayed" - "mnemonic": 85 + add( new FormComponent( "com.formdev.flatlaf.testing.LineChartPanel" ) { + name: "lineChartPanel" + "updateChartDelayed": false auxiliary() { "JavaCodeGenerator.variableLocal": false } - addEvent( new FormEvent( "java.awt.event.ActionListener", "actionPerformed", "updateChartDelayedChanged", false ) ) }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { - "value": "cell 0 5 2 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 5 2 1,alignx right,growx 0" + "value": "cell 0 4 2 1" } ) }, new FormLayoutConstraints( null ) { "location": new java.awt.Point( 0, 0 ) diff --git a/flatlaf-testing/src/main/java/com/formdev/flatlaf/testing/FlatSmoothScrollingTest.java b/flatlaf-testing/src/main/java/com/formdev/flatlaf/testing/FlatSmoothScrollingTest.java index 776866688..d228d3d69 100644 --- a/flatlaf-testing/src/main/java/com/formdev/flatlaf/testing/FlatSmoothScrollingTest.java +++ b/flatlaf-testing/src/main/java/com/formdev/flatlaf/testing/FlatSmoothScrollingTest.java @@ -21,29 +21,16 @@ 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.Arrays; -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import java.util.Objects; import java.util.stream.Collectors; import javax.swing.*; import javax.swing.event.ChangeEvent; import javax.swing.event.ChangeListener; import javax.swing.table.AbstractTableModel; import javax.swing.tree.*; -import com.formdev.flatlaf.FlatLaf; -import com.formdev.flatlaf.ui.FlatUIUtils; import com.formdev.flatlaf.util.ColorFunctions; -import com.formdev.flatlaf.util.HSLColor; -import com.formdev.flatlaf.util.HiDPIUtils; import com.formdev.flatlaf.util.UIScale; import net.miginfocom.swing.*; @@ -64,9 +51,6 @@ public static void main( String[] args ) { FlatSmoothScrollingTest() { initComponents(); - oneSecondWidthChanged(); - updateChartDelayedChanged(); - ToolTipManager.sharedInstance().setInitialDelay( 0 ); ToolTipManager.sharedInstance().setDismissDelay( Integer.MAX_VALUE ); @@ -79,14 +63,6 @@ public static void main( String[] args ) { KeyStroke.getKeyStroke( "alt " + (char) smoothScrollingCheckBox.getMnemonic() ), JComponent.WHEN_ANCESTOR_OF_FOCUSED_COMPONENT ); - // allow clearing chart with Alt+C without moving focus to button - registerKeyboardAction( - e -> { - clearChart(); - }, - KeyStroke.getKeyStroke( "alt " + (char) clearChartButton.getMnemonic() ), - JComponent.WHEN_ANCESTOR_OF_FOCUSED_COMPONENT ); - listLabel.setIcon( new ColorIcon( Color.red.darker() ) ); treeLabel.setIcon( new ColorIcon( Color.blue.darker() ) ); tableLabel.setIcon( new ColorIcon( Color.green.darker() ) ); @@ -116,12 +92,6 @@ public static void main( String[] args ) { customScrollPane.getVerticalScrollBar().getModel().addChangeListener( new ScrollBarChangeHandler( customScrollPane, true, "custom vert", Color.pink ) ); customScrollPane.getHorizontalScrollBar().getModel().addChangeListener( new ScrollBarChangeHandler( customScrollPane, false, "custom horz", Color.pink.darker() ) ); - // clear chart on startup - addHierarchyListener( e -> { - if( (e.getChangeFlags() & HierarchyEvent.SHOWING_CHANGED) != 0 && isShowing() ) - EventQueue.invokeLater( this::clearChart ); - }); - ArrayList items = new ArrayList<>(); for( char ch = '0'; ch < 'z'; ch++ ) { if( (ch > '9' && ch < 'A') || (ch > 'Z' && ch < 'a') ) @@ -207,33 +177,6 @@ private void smoothScrollingChanged() { UIManager.put( "ScrollPane.smoothScrolling", smoothScrollingCheckBox.isSelected() ); } - private void oneSecondWidthChanged() { - int oneSecondWidth = oneSecondWidthSlider.getValue(); - int msPerLineX = - oneSecondWidth <= 2000 ? 100 : - oneSecondWidth <= 4000 ? 50 : - oneSecondWidth <= 8000 ? 25 : - 10; - - lineChartPanel.setOneSecondWidth( oneSecondWidth ); - lineChartPanel.setMsPerLineX( msPerLineX ); - lineChartPanel.revalidate(); - lineChartPanel.repaint(); - - if( xLabelText == null ) - xLabelText = xLabel.getText(); - xLabel.setText( MessageFormat.format( xLabelText, msPerLineX ) ); - } - private String xLabelText; - - private void clearChart() { - lineChartPanel.clear(); - } - - private void updateChartDelayedChanged() { - lineChartPanel.setUpdateDelayed( updateChartDelayedCheckBox.isSelected() ); - } - private void showTableGridChanged() { boolean showGrid = showTableGridCheckBox.isSelected(); table.setShowHorizontalLines( showGrid ); @@ -285,18 +228,7 @@ private void initComponents() { editorPane = new JEditorPane(); customScrollPane = new FlatSmoothScrollingTest.DebugScrollPane(); button1 = new JButton(); - panel3 = new JPanel(); - chartScrollPane = new JScrollPane(); - lineChartPanel = new FlatSmoothScrollingTest.LineChartPanel(); - panel4 = new JPanel(); - xLabel = new JLabel(); - JLabel rectsLabel = new JLabel(); - JLabel yLabel = new JLabel(); - JLabel dotsLabel = new JLabel(); - JLabel oneSecondWidthLabel = new JLabel(); - oneSecondWidthSlider = new JSlider(); - updateChartDelayedCheckBox = new JCheckBox(); - clearChartButton = new JButton(); + lineChartPanel = new LineChartPanel(); //======== this ======== setLayout(new MigLayout( @@ -305,8 +237,7 @@ private void initComponents() { "[200,grow,fill]", // rows "[]" + - "[grow,fill]" + - "[]")); + "[grow,fill]")); //---- smoothScrollingCheckBox ---- smoothScrollingCheckBox.setText("Smooth scrolling"); @@ -451,79 +382,13 @@ private void initComponents() { } splitPane1.setTopComponent(splitPane2); - //======== panel3 ======== - { - panel3.setLayout(new MigLayout( - "insets 3,hidemode 3", - // columns - "[grow,fill]", - // rows - "[100:300,grow,fill]")); - - //======== chartScrollPane ======== - { - chartScrollPane.putClientProperty("JScrollPane.smoothScrolling", false); - chartScrollPane.setViewportView(lineChartPanel); - } - panel3.add(chartScrollPane, "cell 0 0"); - } - splitPane1.setBottomComponent(panel3); + //---- lineChartPanel ---- + lineChartPanel.setLegend1Text("Rectangles: scrollbar values (mouse hover shows stack)"); + lineChartPanel.setLegend2Text("Dots: disabled blitting mode in JViewport"); + lineChartPanel.setLegendYValueText("scroll bar value"); + splitPane1.setBottomComponent(lineChartPanel); } add(splitPane1, "cell 0 1"); - - //======== panel4 ======== - { - panel4.setLayout(new MigLayout( - "insets 0,hidemode 3,gapy 0", - // columns - "[fill]para" + - "[fill]", - // rows - "[]" + - "[]")); - - //---- xLabel ---- - xLabel.setText("X: time ({0}ms per line)"); - panel4.add(xLabel, "cell 0 0"); - - //---- rectsLabel ---- - rectsLabel.setText("Rectangles: scrollbar values (mouse hover shows stack)"); - panel4.add(rectsLabel, "cell 1 0"); - - //---- yLabel ---- - yLabel.setText("Y: scroll bar value (10% per line)"); - panel4.add(yLabel, "cell 0 1"); - - //---- dotsLabel ---- - dotsLabel.setText("Dots: disabled blitting mode in JViewport"); - panel4.add(dotsLabel, "cell 1 1"); - } - add(panel4, "cell 0 2"); - - //---- oneSecondWidthLabel ---- - oneSecondWidthLabel.setText("Scale X:"); - oneSecondWidthLabel.setDisplayedMnemonic('A'); - oneSecondWidthLabel.setLabelFor(oneSecondWidthSlider); - add(oneSecondWidthLabel, "cell 0 2,alignx right,growx 0"); - - //---- oneSecondWidthSlider ---- - oneSecondWidthSlider.setMinimum(1000); - oneSecondWidthSlider.setMaximum(10000); - oneSecondWidthSlider.addChangeListener(e -> oneSecondWidthChanged()); - add(oneSecondWidthSlider, "cell 0 2,alignx right,growx 0,wmax 100"); - - //---- updateChartDelayedCheckBox ---- - updateChartDelayedCheckBox.setText("Update chart delayed"); - updateChartDelayedCheckBox.setMnemonic('P'); - updateChartDelayedCheckBox.setSelected(true); - updateChartDelayedCheckBox.addActionListener(e -> updateChartDelayedChanged()); - add(updateChartDelayedCheckBox, "cell 0 2,alignx right,growx 0"); - - //---- clearChartButton ---- - clearChartButton.setText("Clear Chart"); - clearChartButton.setMnemonic('C'); - clearChartButton.addActionListener(e -> clearChart()); - add(clearChartButton, "cell 0 2,alignx right,growx 0"); // JFormDesigner - End of component initialization //GEN-END:initComponents } @@ -556,14 +421,7 @@ private void initComponents() { private JEditorPane editorPane; private FlatSmoothScrollingTest.DebugScrollPane customScrollPane; private JButton button1; - private JPanel panel3; - private JScrollPane chartScrollPane; - private FlatSmoothScrollingTest.LineChartPanel lineChartPanel; - private JPanel panel4; - private JLabel xLabel; - private JSlider oneSecondWidthSlider; - private JCheckBox updateChartDelayedCheckBox; - private JButton clearChartButton; + private LineChartPanel lineChartPanel; // JFormDesigner - End of variables declaration //GEN-END:variables //---- class ScrollBarChangeHandler --------------------------------------- @@ -595,8 +453,9 @@ private class ScrollBarChangeHandler double value = vertical ? ((double) viewPosition.y) / (viewSize.height - viewport.getHeight()) : ((double) viewPosition.x) / (viewSize.width - viewport.getWidth()); + int ivalue = vertical ? viewPosition.y : viewPosition.x; - lineChartPanel.addValue( value, true, chartColor, name ); + lineChartPanel.addValue( value, ivalue, true, chartColor, name ); } } ); } @@ -606,7 +465,8 @@ public void stateChanged( ChangeEvent e ) { DefaultBoundedRangeModel m = (DefaultBoundedRangeModel) e.getSource(); boolean smoothScrolling = smoothScrollingCheckBox.isSelected(); - lineChartPanel.addValue( getChartValue( m ), false, smoothScrolling ? chartColor : chartColor2, name ); + lineChartPanel.addValue( getChartValue( m ), m.getValue(), false, + smoothScrolling ? chartColor : chartColor2, name ); long t = System.nanoTime() / 1000000; @@ -676,425 +536,6 @@ public void paint( Graphics g ) { } } - //---- class LineChartPanel ----------------------------------------------- - - static class LineChartPanel - extends JComponent - implements Scrollable - { - private static final int NEW_SEQUENCE_TIME_LAG = 500; - private static final int NEW_SEQUENCE_GAP = 100; - private static final int HIT_OFFSET = 4; - - private int oneSecondWidth = 1000; - private int msPerLineX = 200; - - private static class Data { - final double value; - final boolean dot; - final long time; // in milliseconds - final String name; - final Exception stack; - - Data( double value, boolean dot, long time, String name, Exception stack ) { - this.value = value; - this.dot = dot; - this.time = time; - this.name = name; - this.stack = stack; - } - - @Override - public String toString() { - // for debugging - return "value=" + value + ", dot=" + dot + ", time=" + time + ", name=" + name; - } - } - - private final Map> color2dataMap = 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; - - LineChartPanel() { - int resolution = FlatUIUtils.getUIInt( "ScrollPane.smoothScrolling.resolution", 10 ); - - repaintTime = new Timer( resolution * 2, e -> repaintAndRevalidate() ); - repaintTime.setRepeats( false ); - - ToolTipManager.sharedInstance().registerComponent( this ); - } - - void addValue( double value, boolean dot, Color chartColor, String name ) { - List chartData = color2dataMap.computeIfAbsent( chartColor, k -> new ArrayList<>() ); - chartData.add( new Data( value, dot, System.nanoTime() / 1000000, name, new Exception() ) ); - - lastUsedChartColor = chartColor; - - if( updateDelayed ) { - repaintTime.stop(); - repaintTime.start(); - } else - repaintAndRevalidate(); - } - - void clear() { - color2dataMap.clear(); - lastUsedChartColor = null; - - repaint(); - revalidate(); - } - - void setUpdateDelayed( boolean updateDelayed ) { - this.updateDelayed = updateDelayed; - } - - void setOneSecondWidth( int oneSecondWidth ) { - this.oneSecondWidth = oneSecondWidth; - } - - void setMsPerLineX( int msPerLineX ) { - this.msPerLineX = msPerLineX; - } - - 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( color2dataMap.get( lastUsedChartColor ), 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::paintImpl ); - } finally { - g2.dispose(); - } - } - - private void paintImpl( Graphics2D g, int x, int y, int width, int height, double scaleFactor ) { - FlatUIUtils.setRenderingHints( g ); - - int oneSecondWidth = (int) (this.oneSecondWidth * scaleFactor); - int seqGapWidth = (int) (NEW_SEQUENCE_GAP * scaleFactor); - 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 : color2dataMap.entrySet() ) { - List chartData = e.getValue(); - Color chartColor = e.getKey(); - if( FlatLaf.isLafDark() ) - chartColor = new HSLColor( chartColor ).adjustTone( 50 ); - Color temporaryValueColor = ColorFunctions.fade( chartColor, FlatLaf.isLafDark() ? 0.7f : 0.3f ); - Color dataPointColor = ColorFunctions.fade( chartColor, FlatLaf.isLafDark() ? 0.6f : 0.2f ); - - // sequence start time and x coordinate - long seqTime = 0; - int seqX = 0; - - // "previous" data point time, x/y coordinates and count - long ptime = 0; - int px = 0; - int py = 0; - int pcount = 0; - - boolean first = true; - boolean isTemporaryValue = false; - int lastTemporaryValueIndex = -1; - - int size = chartData.size(); - for( int i = 0; i < size; i++ ) { - Data data = chartData.get( i ); - - boolean newSeq = (data.time > ptime + NEW_SEQUENCE_TIME_LAG); - ptime = data.time; - - if( newSeq ) { - // paint short horizontal line for previous sequence that has only one data point - if( !first && pcount == 0 ) { - g.setColor( chartColor ); - g.drawLine( px, py, px + (int) Math.round( UIScale.scale( 8 ) * scaleFactor ), py ); - } - - // start new sequence - seqTime = data.time; - seqX = !first ? px + seqGapWidth : 0; - px = seqX; - pcount = 0; - first = false; - isTemporaryValue = false; - } - - // x/y coordinates of current data point - int dy = (int) ((height - 1) * data.value); - int dx = (int) (seqX + (((data.time - seqTime) / 1000.) * oneSecondWidth)); - - // paint rectangle to indicate data point - g.setColor( dataPointColor ); - g.drawRect( dx - hitOffset, dy - hitOffset, hitOffset * 2, hitOffset * 2 ); - - // remember data point for tooltip - lastPoints.add( new Point( dx, dy ) ); - lastDatas.add( data ); - - if( data.dot ) { - int s1 = (int) Math.round( UIScale.scale( 1 ) * scaleFactor ); - int s3 = (int) Math.round( UIScale.scale( 3 ) * scaleFactor ); - g.setColor( chartColor ); - g.fillRect( dx - s1, dy - s1, s3, s3 ); - continue; - } - - if( !newSeq ) { - if( isTemporaryValue && i > lastTemporaryValueIndex ) - isTemporaryValue = false; - - g.setColor( isTemporaryValue ? temporaryValueColor : chartColor ); - - // line in sequence - g.drawLine( px, py, dx, dy ); - - px = dx; - pcount++; - - // check next data points for "temporary" value(s) - if( !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.dot ) - 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++; - } - } - } - - py = dy; - } - } - } - - private int chartWidth() { - int width = 0; - for( List chartData : color2dataMap.values() ) - width = Math.max( width, chartWidth( chartData, null ) ); - return width; - } - - private int chartWidth( List chartData, int[] lastSeqX ) { - long seqTime = 0; - int seqX = 0; - long ptime = 0; - int px = 0; - - int size = chartData.size(); - for( int i = 0; i < size; i++ ) { - Data data = chartData.get( i ); - - if( data.time > ptime + NEW_SEQUENCE_TIME_LAG ) { - // start new sequence - seqTime = data.time; - seqX = (i > 0) ? px + NEW_SEQUENCE_GAP : 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 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.dot ) - buf.append( "DOT: " ); - buf.append( data.name ).append( ' ' ).append( data.value ).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(); - - // ignore methods from this class - if( className.startsWith( FlatSmoothScrollingTest.class.getName() ) ) - continue; - - int repeatCount = 0; - for( int k = j + 1; k < stackTrace.length; k++ ) { - if( !stackElement.equals( stackTrace[k] ) ) - break; - repeatCount++; - } - j += repeatCount; - - // append method - buf.append( className ) - .append( "." ) - .append( methodName ) - .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( "" ); - if( repeatCount > 0 ) - buf.append( " " ).append( repeatCount + 1 ).append( "x" ); - buf.append( "
" ); - - // break at some methods to make stack smaller - if( (className.startsWith( "java.awt.event.InvocationEvent" ) && methodName.equals( "dispatch" )) || - (className.startsWith( "java.awt.Component" ) && methodName.equals( "processMouseWheelEvent" )) || - (className.startsWith( "javax.swing.JComponent" ) && methodName.equals( "processKeyBinding" )) ) - 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(); - } - } - //---- class ColorIcon ---------------------------------------------------- private static class ColorIcon diff --git a/flatlaf-testing/src/main/java/com/formdev/flatlaf/testing/FlatSmoothScrollingTest.jfd b/flatlaf-testing/src/main/java/com/formdev/flatlaf/testing/FlatSmoothScrollingTest.jfd index f316a84d2..3e762bb6f 100644 --- a/flatlaf-testing/src/main/java/com/formdev/flatlaf/testing/FlatSmoothScrollingTest.jfd +++ b/flatlaf-testing/src/main/java/com/formdev/flatlaf/testing/FlatSmoothScrollingTest.jfd @@ -6,7 +6,7 @@ new FormModel { add( new FormContainer( "com.formdev.flatlaf.testing.FlatTestPanel", new FormLayoutManager( class net.miginfocom.swing.MigLayout ) { "$layoutConstraints": "ltr,insets dialog,hidemode 3" "$columnConstraints": "[200,grow,fill]" - "$rowConstraints": "[][grow,fill][]" + "$rowConstraints": "[][grow,fill]" } ) { name: "this" add( new FormComponent( "javax.swing.JCheckBox" ) { @@ -175,105 +175,17 @@ new FormModel { }, new FormLayoutConstraints( class java.lang.String ) { "value": "left" } ) - add( new FormContainer( "javax.swing.JPanel", new FormLayoutManager( class net.miginfocom.swing.MigLayout ) { - "$layoutConstraints": "insets 3,hidemode 3" - "$columnConstraints": "[grow,fill]" - "$rowConstraints": "[100:300,grow,fill]" - } ) { - name: "panel3" - add( new FormContainer( "javax.swing.JScrollPane", new FormLayoutManager( class javax.swing.JScrollPane ) ) { - name: "chartScrollPane" - "$client.JScrollPane.smoothScrolling": false - add( new FormComponent( "com.formdev.flatlaf.testing.FlatSmoothScrollingTest$LineChartPanel" ) { - name: "lineChartPanel" - } ) - }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { - "value": "cell 0 0" - } ) + add( new FormComponent( "com.formdev.flatlaf.testing.LineChartPanel" ) { + name: "lineChartPanel" + "legend1Text": "Rectangles: scrollbar values (mouse hover shows stack)" + "legend2Text": "Dots: disabled blitting mode in JViewport" + "legendYValueText": "scroll bar value" }, new FormLayoutConstraints( class java.lang.String ) { "value": "right" } ) }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { "value": "cell 0 1" } ) - 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: "panel4" - 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: "rectsLabel" - "text": "Rectangles: scrollbar values (mouse hover shows stack)" - auxiliary() { - "JavaCodeGenerator.variableLocal": true - } - }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { - "value": "cell 1 0" - } ) - add( new FormComponent( "javax.swing.JLabel" ) { - name: "yLabel" - "text": "Y: scroll bar value (10% per line)" - auxiliary() { - "JavaCodeGenerator.variableLocal": true - } - }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { - "value": "cell 0 1" - } ) - add( new FormComponent( "javax.swing.JLabel" ) { - name: "dotsLabel" - "text": "Dots: disabled blitting mode in JViewport" - auxiliary() { - "JavaCodeGenerator.variableLocal": true - } - }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { - "value": "cell 1 1" - } ) - }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { - "value": "cell 0 2" - } ) - 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 2,alignx right,growx 0" - } ) - add( new FormComponent( "javax.swing.JSlider" ) { - name: "oneSecondWidthSlider" - "minimum": 1000 - "maximum": 10000 - addEvent( new FormEvent( "javax.swing.event.ChangeListener", "stateChanged", "oneSecondWidthChanged", false ) ) - }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { - "value": "cell 0 2,alignx right,growx 0,wmax 100" - } ) - add( new FormComponent( "javax.swing.JCheckBox" ) { - name: "updateChartDelayedCheckBox" - "text": "Update chart delayed" - "mnemonic": 80 - "selected": true - addEvent( new FormEvent( "java.awt.event.ActionListener", "actionPerformed", "updateChartDelayedChanged", false ) ) - }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { - "value": "cell 0 2,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 2,alignx right,growx 0" - } ) }, new FormLayoutConstraints( null ) { "location": new java.awt.Point( 0, 0 ) "size": new java.awt.Dimension( 875, 715 ) 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..362dce3bb --- /dev/null +++ b/flatlaf-testing/src/main/java/com/formdev/flatlaf/testing/LineChartPanel.java @@ -0,0 +1,664 @@ +/* +/* + * 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.ColorFunctions; +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 ); + } ); + } + + @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 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 boolean isUpdateChartDelayed() { + return updateChartDelayedCheckBox.isSelected(); + } + + public void setUpdateChartDelayed( boolean updateChartDelayed ) { + updateChartDelayedCheckBox.setSelected( updateChartDelayed ); + } + + void addValue( double value, int ivalue, boolean dot, Color chartColor, String name ) { + lineChart.addValue( value, ivalue, dot, chartColor, name ); + } + + private void oneSecondWidthChanged() { + int oneSecondWidth = oneSecondWidthSlider.getValue(); + int msPerLineX = + oneSecondWidth <= 2000 ? 100 : + oneSecondWidth <= 4000 ? 50 : + oneSecondWidth <= 8000 ? 25 : + 10; + + lineChart.setOneSecondWidth( oneSecondWidth ); + lineChart.setMsPerLineX( msPerLineX ); + lineChart.revalidate(); + lineChart.repaint(); + + if( xLabelText == null ) + xLabelText = xLabel.getText(); + xLabel.setText( MessageFormat.format( xLabelText, msPerLineX ) ); + } + private String xLabelText; + + private void updateChartDelayedChanged() { + lineChart.setUpdateDelayed( 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(1000); + oneSecondWidthSlider.setMaximum(10000); + oneSecondWidthSlider.addChangeListener(e -> oneSecondWidthChanged()); + add(oneSecondWidthSlider, "cell 0 1,alignx right,growx 0,wmax 100"); + + //---- updateChartDelayedCheckBox ---- + updateChartDelayedCheckBox.setText("Update chart delayed"); + updateChartDelayedCheckBox.setMnemonic('P'); + updateChartDelayedCheckBox.setSelected(true); + 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 = 500; + private static final int NEW_SEQUENCE_GAP = 100; + private static final int HIT_OFFSET = 4; + + private int oneSecondWidth = 1000; + private int msPerLineX = 200; + + private static class Data { + final double value; + final int ivalue; + final boolean dot; + final long time; // in milliseconds + final String name; + final Exception stack; + + Data( double value, int ivalue, boolean dot, long time, String name, Exception stack ) { + this.value = value; + this.ivalue = ivalue; + this.dot = dot; + this.time = time; + this.name = name; + this.stack = stack; + } + + @Override + public String toString() { + // for debugging + return "value=" + value + ", ivalue=" + ivalue + ", dot=" + dot + ", time=" + time + ", name=" + name; + } + } + + private final Map> color2dataMap = 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 ); + } + + void addValue( double value, int ivalue, boolean dot, Color chartColor, String name ) { + List chartData = color2dataMap.computeIfAbsent( chartColor, k -> new ArrayList<>() ); + chartData.add( new Data( value, ivalue, dot, System.nanoTime() / 1000000, name, new Exception() ) ); + + lastUsedChartColor = chartColor; + + if( updateDelayed ) { + repaintTime.stop(); + repaintTime.start(); + } else + repaintAndRevalidate(); + } + + void clear() { + color2dataMap.clear(); + lastUsedChartColor = null; + + repaint(); + revalidate(); + } + + void setUpdateDelayed( boolean updateDelayed ) { + this.updateDelayed = updateDelayed; + } + + void setOneSecondWidth( int oneSecondWidth ) { + this.oneSecondWidth = oneSecondWidth; + } + + void setMsPerLineX( int msPerLineX ) { + this.msPerLineX = msPerLineX; + } + + 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( color2dataMap.get( lastUsedChartColor ), 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::paintImpl ); + } finally { + g2.dispose(); + } + } + + private void paintImpl( 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 = (int) (NEW_SEQUENCE_GAP * scaleFactor); + 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 : color2dataMap.entrySet() ) { + List chartData = e.getValue(); + Color chartColor = e.getKey(); + if( FlatLaf.isLafDark() ) + chartColor = new HSLColor( chartColor ).adjustTone( 50 ); + Color temporaryValueColor = ColorFunctions.fade( chartColor, FlatLaf.isLafDark() ? 0.7f : 0.3f ); + Color dataPointColor = ColorFunctions.fade( chartColor, FlatLaf.isLafDark() ? 0.6f : 0.2f ); + + // sequence start time and x coordinate + long seqTime = 0; + int seqX = 0; + + // "previous" data point time, x/y coordinates and count + long ptime = 0; + int px = 0; + int py = 0; + int pcount = 0; + + boolean first = true; + boolean isTemporaryValue = false; + int lastTemporaryValueIndex = -1; + + int size = chartData.size(); + for( int i = 0; i < size; i++ ) { + Data data = chartData.get( i ); + + boolean newSeq = (data.time > ptime + NEW_SEQUENCE_TIME_LAG); + ptime = data.time; + + if( newSeq ) { + // paint short horizontal line for previous sequence that has only one data point + if( !first && pcount == 0 ) { + g.setColor( chartColor ); + g.drawLine( px, py, px + (int) Math.round( UIScale.scale( 8 ) * scaleFactor ), py ); + } + + // start new sequence + seqTime = data.time; + seqX = !first ? px + seqGapWidth : 0; + px = seqX; + pcount = 0; + first = false; + isTemporaryValue = false; + } + + // x/y coordinates of current data point + int dy = (int) ((height - 1) * data.value); + int dx = (int) (seqX + (((data.time - seqTime) / 1000.) * oneSecondWidth)); + + // paint rectangle to indicate data point + g.setColor( dataPointColor ); + g.drawRect( dx - hitOffset, dy - hitOffset, hitOffset * 2, hitOffset * 2 ); + + // remember data point for tooltip + lastPoints.add( new Point( dx, dy ) ); + lastDatas.add( data ); + + if( data.dot ) { + int s1 = (int) Math.round( UIScale.scale( 1 ) * scaleFactor ); + int s3 = (int) Math.round( UIScale.scale( 3 ) * scaleFactor ); + g.setColor( chartColor ); + g.fillRect( dx - s1, dy - s1, s3, s3 ); + continue; + } + + if( !newSeq ) { + if( isTemporaryValue && i > lastTemporaryValueIndex ) + isTemporaryValue = false; + + g.setColor( isTemporaryValue ? temporaryValueColor : chartColor ); + + // line in sequence + g.drawLine( px, py, dx, dy ); + + px = dx; + pcount++; + + // check next data points for "temporary" value(s) + if( !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.dot ) + 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++; + } + } + } + + py = dy; + } + } + } + + private int chartWidth() { + int width = 0; + for( List chartData : color2dataMap.values() ) + width = Math.max( width, chartWidth( chartData, null ) ); + return width; + } + + private int chartWidth( List chartData, int[] lastSeqX ) { + long seqTime = 0; + int seqX = 0; + long ptime = 0; + int px = 0; + + int size = chartData.size(); + for( int i = 0; i < size; i++ ) { + Data data = chartData.get( i ); + + if( data.time > ptime + NEW_SEQUENCE_TIME_LAG ) { + // start new sequence + seqTime = data.time; + seqX = (i > 0) ? px + NEW_SEQUENCE_GAP : 0; + px = seqX; + } else { + // line in sequence + int dx = (int) (seqX + (((data.time - seqTime) / 1000.) * UIScale.scale( 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.dot ) + buf.append( "DOT: " ); + buf.append( data.name ).append( ' ' ).append( data.ivalue ).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(); + + // 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; + + // append method + buf.append( className ) + .append( "." ) + .append( methodName ) + .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( "" ); + if( repeatCount > 0 ) + buf.append( " " ).append( repeatCount + 1 ).append( "x" ); + buf.append( "
" ); + + // break at some methods to make stack smaller + if( (className.startsWith( "java.awt.event.InvocationEvent" ) && methodName.equals( "dispatch" )) || + (className.startsWith( "java.awt.Component" ) && methodName.equals( "processMouseWheelEvent" )) || + (className.startsWith( "javax.swing.JComponent" ) && methodName.equals( "processKeyBinding" )) ) + 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(); + } + } +} 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..c81e0a659 --- /dev/null +++ b/flatlaf-testing/src/main/java/com/formdev/flatlaf/testing/LineChartPanel.jfd @@ -0,0 +1,121 @@ +JFDML JFormDesigner: "8.1.0.0.283" Java: "19.0.2" 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": 1000 + "maximum": 10000 + 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,wmax 100" + } ) + add( new FormComponent( "javax.swing.JCheckBox" ) { + name: "updateChartDelayedCheckBox" + "text": "Update chart delayed" + "mnemonic": 80 + "selected": true + 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 ) + } ) + } +} From 44a04cca2c4de416d5f2a00b191f8fe968071538 Mon Sep 17 00:00:00 2001 From: Karl Tauber Date: Fri, 1 Sep 2023 17:12:53 +0200 Subject: [PATCH 30/30] FlatSmoothScrollingTest: - better list/tree/etc items for easier recognizing jittery scrolling - sliders to modify animation duration and resolution - slider to invoke `scrollRectToVisible()` - option to show row header for table - use viewport.viewPosition for chart (instead of scrollbar.value) - highlight methods in stack of tooltip (e.g. JViewport.setViewPosition()) --- .../flatlaf/testing/FlatAnimatorTest.java | 6 +- .../testing/FlatSmoothScrollingTest.java | 293 ++++++++++++++---- .../testing/FlatSmoothScrollingTest.jfd | 81 ++++- .../flatlaf/testing/LineChartPanel.java | 78 ++++- 4 files changed, 367 insertions(+), 91 deletions(-) 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 37c1e7950..7dbe44311 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 @@ -180,7 +180,7 @@ static class MouseWheelTestPanel value = startValue + Math.round( (targetValue - startValue) * fraction ); valueLabel.setText( String.valueOf( value ) ); - lineChartPanel.addValue( value / (double) MAX_VALUE, value, false, Color.red, null ); + lineChartPanel.addValue( Color.red, value / (double) MAX_VALUE, value, null ); }, () -> { targetValue = -1; } ); @@ -198,7 +198,7 @@ public void mouseWheelMoved( MouseWheelEvent e ) { // for unprecise wheels the rotation value is usually -1 or +1 // for precise wheels the rotation value is in range ca. -10 to +10, // depending how fast the wheel is rotated - lineChartPanel.addValue( 0.5 + (preciseWheelRotation / 20.), (int) (preciseWheelRotation * 1000), true, Color.red, null ); + lineChartPanel.addDot( Color.red, 0.5 + (preciseWheelRotation / 20.), (int) (preciseWheelRotation * 1000), null, null ); // increase/decrease target value if animation is in progress int newValue = (int) ((targetValue < 0 ? value : targetValue) + (STEP * preciseWheelRotation)); @@ -215,7 +215,7 @@ public void mouseWheelMoved( MouseWheelEvent e ) { value = newValue; valueLabel.setText( String.valueOf( value ) ); - lineChartPanel.addValue( value / (double) MAX_VALUE, value, false, Color.red, null ); + lineChartPanel.addValue( Color.red, value / (double) MAX_VALUE, value, null ); return; } diff --git a/flatlaf-testing/src/main/java/com/formdev/flatlaf/testing/FlatSmoothScrollingTest.java b/flatlaf-testing/src/main/java/com/formdev/flatlaf/testing/FlatSmoothScrollingTest.java index d228d3d69..22fb02a37 100644 --- a/flatlaf-testing/src/main/java/com/formdev/flatlaf/testing/FlatSmoothScrollingTest.java +++ b/flatlaf-testing/src/main/java/com/formdev/flatlaf/testing/FlatSmoothScrollingTest.java @@ -22,6 +22,7 @@ import java.awt.EventQueue; import java.awt.Graphics; import java.awt.Point; +import java.awt.Rectangle; import java.util.ArrayList; import java.util.Arrays; import java.util.stream.Collectors; @@ -30,6 +31,8 @@ import javax.swing.event.ChangeListener; import javax.swing.table.AbstractTableModel; import javax.swing.tree.*; +import com.formdev.flatlaf.ui.FlatUIUtils; +import com.formdev.flatlaf.util.Animator; import com.formdev.flatlaf.util.ColorFunctions; import com.formdev.flatlaf.util.UIScale; import net.miginfocom.swing.*; @@ -51,8 +54,7 @@ public static void main( String[] args ) { FlatSmoothScrollingTest() { initComponents(); - ToolTipManager.sharedInstance().setInitialDelay( 0 ); - ToolTipManager.sharedInstance().setDismissDelay( Integer.MAX_VALUE ); + initializeDurationAndResolution(); // allow enabling/disabling smooth scrolling with Alt+S without moving focus to checkbox registerKeyboardAction( @@ -93,13 +95,15 @@ public static void main( String[] args ) { customScrollPane.getHorizontalScrollBar().getModel().addChangeListener( new ScrollBarChangeHandler( customScrollPane, false, "custom horz", Color.pink.darker() ) ); ArrayList items = new ArrayList<>(); - for( char ch = '0'; ch < 'z'; ch++ ) { - if( (ch > '9' && ch < 'A') || (ch > 'Z' && ch < 'a') ) - continue; - - char[] chars = new char[ch - '0' + 1]; - Arrays.fill( chars, ch ); - items.add( new String( chars ) ); + for( int i = 0; i < 10; i++ ) { + for( int j = 0; j < 10; j++ ) { + char[] chars = new char[i*10 + j + 1]; + Arrays.fill( chars, ' ' ); + chars[chars.length - 1] = (char) ('0' + j); + if( i >= 5 ) + chars[50 - 1 - ((i-5)*10) - j] = (char) ('0' + j); + items.add( new String( chars ) ); + } } // list model @@ -137,12 +141,10 @@ public int getRowCount() { } @Override public int getColumnCount() { - return 4; + return (table.getAutoResizeMode() == JTable.AUTO_RESIZE_OFF) ? 100 : 2; } @Override public Object getValueAt( int rowIndex, int columnIndex ) { - if( columnIndex > 0 ) - rowIndex = (items.size() + rowIndex - ((items.size() / 4) * columnIndex)) % items.size(); return items.get( rowIndex ); } } ); @@ -155,17 +157,49 @@ public Object getValueAt( int rowIndex, int columnIndex ) { } // text components - String longText = items.stream().collect( Collectors.joining( " " ) ) + ' ' - + items.stream().limit( 20 ).collect( Collectors.joining( " " ) ); + String longText = ""; + for( int i = 0; i < 100; i++ ) + longText += String.format( "%-5d ", i ); + longText += "100"; String text = items.stream().collect( Collectors.joining( "\n" ) ) + '\n'; textArea.setText( longText + '\n' + text ); textPane.setText( text ); editorPane.setText( text ); + // move selection to beginning in text components textArea.select( 0, 0 ); textPane.select( 0, 0 ); editorPane.select( 0, 0 ); + // custom scrollable + StringBuilder buf = new StringBuilder() + .append( "" ) + .append( customButton.getText() ); + for( String item : items ) { + buf.append( "
" ); + for( int i = 0; i < item.length(); i++ ) { + char ch = item.charAt( i ); + if( ch == ' ' ) + buf.append( " " ); + else + buf.append( ch ); + } + } + buf.append( "" ); + customButton.setText( buf.toString() ); + + // line chart + lineChartPanel.addMethodHighlight( JViewport.class.getName() + ".setViewPosition", "#0000bb" ); + lineChartPanel.addMethodHighlight( JViewport.class.getName() + ".scrollRectToVisible", "#00bbbb" ); + lineChartPanel.addMethodHighlight( JScrollBar.class.getName() + ".setValue", "#00aa00" ); + lineChartPanel.addMethodHighlight( Animator.class.getName() + ".timingEvent", "#bb0000" ); + lineChartPanel.addMethodHighlight( JComponent.class.getName() + ".processKeyBinding", "#bb0000" ); + lineChartPanel.addMethodHighlight( Component.class.getName() + ".processMouseEvent", "#bb0000" ); + lineChartPanel.addMethodHighlight( Component.class.getName() + ".processMouseWheelEvent", "#bb0000" ); + lineChartPanel.addMethodHighlight( Component.class.getName() + ".processMouseMotionEvent", "#bb0000" ); + lineChartPanel.addMethodHighlight( "actionPerformed", "#bbbb00" ); + + // request focus for list EventQueue.invokeLater( () -> { EventQueue.invokeLater( () -> { list.requestFocusInWindow(); @@ -177,6 +211,82 @@ private void smoothScrollingChanged() { UIManager.put( "ScrollPane.smoothScrolling", smoothScrollingCheckBox.isSelected() ); } + private void initializeDurationAndResolution() { + int duration = FlatUIUtils.getUIInt( "ScrollPane.smoothScrolling.duration", 200 ); + int resolution = FlatUIUtils.getUIInt( "ScrollPane.smoothScrolling.resolution", 10 ); + + durationSlider.setValue( duration ); + resolutionSlider.setValue( resolution ); + + updateDurationAndResolutionLabels( duration, resolution ); + } + + private void durationOrResolutionChanged() { + int duration = durationSlider.getValue(); + int resolution = resolutionSlider.getValue(); + + updateDurationAndResolutionLabels( duration, resolution ); + + UIManager.put( "ScrollPane.smoothScrolling.duration", duration ); + UIManager.put( "ScrollPane.smoothScrolling.resolution", resolution ); + + // update UI of scroll bars to force re-creation of animator + JScrollPane[] scrollPanes = { listScrollPane, treeScrollPane, tableScrollPane, + textAreaScrollPane, textPaneScrollPane, editorPaneScrollPane, customScrollPane }; + for( JScrollPane scrollPane : scrollPanes ) { + scrollPane.getVerticalScrollBar().updateUI(); + scrollPane.getHorizontalScrollBar().updateUI(); + } + } + + private void updateDurationAndResolutionLabels( int duration, int resolution ) { + durationValueLabel.setText( duration + " ms" ); + resolutionValueLabel.setText( resolution + " ms" ); + } + + private void scrollToChanged() { + if( scrollToSlider.getValueIsAdjusting() ) + return; + + int value = scrollToSlider.getValue(); + JComponent[] comps = { list, tree, table, textArea, textPane, editorPane, customButton }; + for( JComponent c : comps ) { + int x = (c.getWidth() * value) / 100; + int y = (c.getHeight() * value) / 100; + c.scrollRectToVisible( new Rectangle( x, y, 1, 1 ) ); + } + } + + private void rowHeaderChanged() { + JTable rowHeader = null; + if( rowHeaderCheckBox.isSelected() ) { + rowHeader = new JTable(); + rowHeader.setPreferredScrollableViewportSize( new Dimension( UIScale.scale( 50 ), 100 ) ); + rowHeader.setModel( new AbstractTableModel() { + @Override + public int getRowCount() { + return table.getRowCount(); + } + @Override + public int getColumnCount() { + return 1; + } + @Override + public Object getValueAt( int rowIndex, int columnIndex ) { + char[] chars = new char[10]; + Arrays.fill( chars, ' ' ); + int i = rowIndex % 10; + if( (rowIndex / 10) % 2 == 0 ) + chars[i] = (char) ('0' + i); + else + chars[9 - i] = (char) ('A' + i); + return new String( chars ); + } + } ); + } + tableScrollPane.setRowHeaderView( rowHeader ); + } + private void showTableGridChanged() { boolean showGrid = showTableGridCheckBox.isSelected(); table.setShowHorizontalLines( showGrid ); @@ -187,6 +297,7 @@ private void showTableGridChanged() { private void autoResizeModeChanged() { table.setAutoResizeMode( autoResizeModeCheckBox.isSelected() ? JTable.AUTO_RESIZE_SUBSEQUENT_COLUMNS : JTable.AUTO_RESIZE_OFF ); + ((AbstractTableModel)table.getModel()).fireTableStructureChanged(); } @Override @@ -201,12 +312,20 @@ public void updateUI() { private void initComponents() { // JFormDesigner - Component initialization - DO NOT MODIFY //GEN-BEGIN:initComponents smoothScrollingCheckBox = new JCheckBox(); + JPanel hSpacer1 = new JPanel(null); + JLabel durationLabel = new JLabel(); + durationSlider = new JSlider(); + durationValueLabel = new JLabel(); + JLabel resolutionLabel = new JLabel(); + resolutionSlider = new JSlider(); + resolutionValueLabel = new JLabel(); splitPane1 = new JSplitPane(); splitPane2 = new JSplitPane(); panel1 = new JPanel(); listLabel = new JLabel(); treeLabel = new JLabel(); tableLabel = new JLabel(); + rowHeaderCheckBox = new JCheckBox(); showTableGridCheckBox = new JCheckBox(); autoResizeModeCheckBox = new JCheckBox(); listScrollPane = new FlatSmoothScrollingTest.DebugScrollPane(); @@ -215,6 +334,7 @@ private void initComponents() { tree = new JTree(); tableScrollPane = new FlatSmoothScrollingTest.DebugScrollPane(); table = new JTable(); + scrollToSlider = new JSlider(); panel2 = new JPanel(); textAreaLabel = new JLabel(); textPaneLabel = new JLabel(); @@ -227,7 +347,7 @@ private void initComponents() { editorPaneScrollPane = new FlatSmoothScrollingTest.DebugScrollPane(); editorPane = new JEditorPane(); customScrollPane = new FlatSmoothScrollingTest.DebugScrollPane(); - button1 = new JButton(); + customButton = new JButton(); lineChartPanel = new LineChartPanel(); //======== this ======== @@ -245,6 +365,41 @@ private void initComponents() { smoothScrollingCheckBox.setMnemonic('S'); smoothScrollingCheckBox.addActionListener(e -> smoothScrollingChanged()); add(smoothScrollingCheckBox, "cell 0 0,alignx left,growx 0"); + add(hSpacer1, "cell 0 0,growx"); + + //---- durationLabel ---- + durationLabel.setText("Duration:"); + add(durationLabel, "cell 0 0"); + + //---- durationSlider ---- + durationSlider.setMaximum(5000); + durationSlider.setValue(200); + durationSlider.setSnapToTicks(true); + durationSlider.setMinimum(100); + durationSlider.setMinorTickSpacing(50); + durationSlider.addChangeListener(e -> durationOrResolutionChanged()); + add(durationSlider, "cell 0 0"); + + //---- durationValueLabel ---- + durationValueLabel.setText("0000 ms"); + add(durationValueLabel, "cell 0 0,width 50"); + + //---- resolutionLabel ---- + resolutionLabel.setText("Resolution:"); + add(resolutionLabel, "cell 0 0"); + + //---- resolutionSlider ---- + resolutionSlider.setMaximum(1000); + resolutionSlider.setMinimum(10); + resolutionSlider.setValue(10); + resolutionSlider.setMinorTickSpacing(10); + resolutionSlider.setSnapToTicks(true); + resolutionSlider.addChangeListener(e -> durationOrResolutionChanged()); + add(resolutionSlider, "cell 0 0"); + + //---- resolutionValueLabel ---- + resolutionValueLabel.setText("0000 ms"); + add(resolutionValueLabel, "cell 0 0,width 50"); //======== splitPane1 ======== { @@ -264,7 +419,8 @@ private void initComponents() { "[200,grow,fill]" + "[200,grow,fill]" + "[200,grow,fill]" + - "[200,grow,fill]", + "[200,grow,fill]" + + "[fill]", // rows "[]0" + "[200,grow,fill]")); @@ -284,6 +440,11 @@ private void initComponents() { tableLabel.setHorizontalTextPosition(SwingConstants.LEADING); panel1.add(tableLabel, "cell 2 0 2 1"); + //---- rowHeaderCheckBox ---- + rowHeaderCheckBox.setText("Row header"); + rowHeaderCheckBox.addActionListener(e -> rowHeaderChanged()); + panel1.add(rowHeaderCheckBox, "cell 2 0 2 1,alignx right,growx 0"); + //---- showTableGridCheckBox ---- showTableGridCheckBox.setText("Show table grid"); showTableGridCheckBox.setMnemonic('G'); @@ -313,6 +474,13 @@ private void initComponents() { tableScrollPane.setViewportView(table); } panel1.add(tableScrollPane, "cell 2 1 2 1,width 100,height 100"); + + //---- scrollToSlider ---- + scrollToSlider.setOrientation(SwingConstants.VERTICAL); + scrollToSlider.setValue(0); + scrollToSlider.setInverted(true); + scrollToSlider.addChangeListener(e -> scrollToChanged()); + panel1.add(scrollToSlider, "cell 4 1"); } splitPane2.setTopComponent(panel1); @@ -369,12 +537,11 @@ private void initComponents() { //======== customScrollPane ======== { - //---- button1 ---- - button1.setText("I'm a large button, but do not implement Scrollable interface"); - button1.setPreferredSize(new Dimension(800, 800)); - button1.setHorizontalAlignment(SwingConstants.LEADING); - button1.setVerticalAlignment(SwingConstants.TOP); - customScrollPane.setViewportView(button1); + //---- customButton ---- + customButton.setText("I'm a large button, but do not implement Scrollable interface"); + customButton.setHorizontalAlignment(SwingConstants.LEADING); + customButton.setVerticalAlignment(SwingConstants.TOP); + customScrollPane.setViewportView(customButton); } panel2.add(customScrollPane, "cell 3 1"); } @@ -394,12 +561,17 @@ private void initComponents() { // JFormDesigner - Variables declaration - DO NOT MODIFY //GEN-BEGIN:variables private JCheckBox smoothScrollingCheckBox; + private JSlider durationSlider; + private JLabel durationValueLabel; + private JSlider resolutionSlider; + private JLabel resolutionValueLabel; private JSplitPane splitPane1; private JSplitPane splitPane2; private JPanel panel1; private JLabel listLabel; private JLabel treeLabel; private JLabel tableLabel; + private JCheckBox rowHeaderCheckBox; private JCheckBox showTableGridCheckBox; private JCheckBox autoResizeModeCheckBox; private FlatSmoothScrollingTest.DebugScrollPane listScrollPane; @@ -408,6 +580,7 @@ private void initComponents() { private JTree tree; private FlatSmoothScrollingTest.DebugScrollPane tableScrollPane; private JTable table; + private JSlider scrollToSlider; private JPanel panel2; private JLabel textAreaLabel; private JLabel textPaneLabel; @@ -420,7 +593,7 @@ private void initComponents() { private FlatSmoothScrollingTest.DebugScrollPane editorPaneScrollPane; private JEditorPane editorPane; private FlatSmoothScrollingTest.DebugScrollPane customScrollPane; - private JButton button1; + private JButton customButton; private LineChartPanel lineChartPanel; // JFormDesigner - End of variables declaration //GEN-END:variables @@ -433,6 +606,7 @@ private class ScrollBarChangeHandler private final Color chartColor; // for smooth scrolling private final Color chartColor2; // for non-smooth scrolling private int count; + private int lastValue; private long lastTime; ScrollBarChangeHandler( DebugScrollPane scrollPane, boolean vertical, String name, Color chartColor ) { @@ -442,20 +616,27 @@ private class ScrollBarChangeHandler // add change listener to viewport that is invoked from JViewport.setViewPosition() scrollPane.getViewport().addChangeListener( e -> { - // add dot to chart if blit scroll mode is disabled - if( vertical == scrollPane.lastScrollingWasVertical && - scrollPane.getViewport().getScrollMode() != JViewport.BLIT_SCROLL_MODE ) + JViewport viewport = scrollPane.getViewport(); + Point viewPosition = viewport.getViewPosition(); + + if( (vertical && viewPosition.y != scrollPane.previousViewPosition.y) || + (!vertical && viewPosition.x != scrollPane.previousViewPosition.x) ) { // calculate value from view position because scrollbar value is not yet up-to-date - JViewport viewport = scrollPane.getViewport(); - Point viewPosition = viewport.getViewPosition(); Dimension viewSize = viewport.getViewSize(); double value = vertical ? ((double) viewPosition.y) / (viewSize.height - viewport.getHeight()) : ((double) viewPosition.x) / (viewSize.width - viewport.getWidth()); int ivalue = vertical ? viewPosition.y : viewPosition.x; - lineChartPanel.addValue( value, ivalue, true, chartColor, name ); + // add dot to chart if blit scroll mode is disabled + boolean dot = (scrollPane.getViewport().getScrollMode() != JViewport.BLIT_SCROLL_MODE); + + Color color = smoothScrollingCheckBox.isSelected() ? this.chartColor : chartColor2; + if( dot ) + lineChartPanel.addValueWithDot( color, value, ivalue, null, name ); + else + lineChartPanel.addValue( color, value, ivalue, name ); } } ); } @@ -463,26 +644,25 @@ private class ScrollBarChangeHandler @Override public void stateChanged( ChangeEvent e ) { DefaultBoundedRangeModel m = (DefaultBoundedRangeModel) e.getSource(); - boolean smoothScrolling = smoothScrollingCheckBox.isSelected(); - - lineChartPanel.addValue( getChartValue( m ), m.getValue(), false, - smoothScrolling ? chartColor : chartColor2, name ); - + int value = m.getValue(); +/* + double chartValue = (double) (value - m.getMinimum()) / (double) (m.getMaximum() - m.getExtent()); + lineChartPanel.addValue( chartValue, value, false, false, + smoothScrollingCheckBox.isSelected() ? chartColor : chartColor2, name ); +*/ long t = System.nanoTime() / 1000000; - System.out.printf( "%s (%d): %4d %3d ms %b%n", + System.out.printf( "%s (%d): %4d --> %4d %3d ms %-5b %s%n", name, ++count, - m.getValue(), + lastValue, + value, t - lastTime, - m.getValueIsAdjusting() ); + m.getValueIsAdjusting(), + value > lastValue ? "down" : value < lastValue ? "up" : "" ); + lastValue = value; lastTime = t; } - - private double getChartValue( BoundedRangeModel m ) { - int value = m.getValue(); - return (double) (value - m.getMinimum()) / (double) (m.getMaximum() - m.getExtent()); - } } //---- class DebugViewport ------------------------------------------------ @@ -490,7 +670,7 @@ private double getChartValue( BoundedRangeModel m ) { private static class DebugScrollPane extends JScrollPane { - boolean lastScrollingWasVertical; + Point previousViewPosition = new Point(); @Override protected JViewport createViewport() { @@ -504,34 +684,11 @@ public Point getViewPosition() { @Override public void setViewPosition( Point p ) { - // remember whether scrolling vertically or horizontally - Component view = getView(); - if( view != null ) { - int oldY = (view instanceof JComponent) - ? ((JComponent) view).getY() - : view.getBounds().y; - - int newY = -p.y; - lastScrollingWasVertical = (oldY != newY); - } else - lastScrollingWasVertical = true; + // remember previous view position + previousViewPosition = getViewPosition(); super.setViewPosition( p ); } - - @Override - public void paint( Graphics g ) { - super.paint( g ); - - if( backingStoreImage != null ) { - System.out.println( "---------------------------------------------" ); - System.out.println( "WARNING: backingStoreImage was used for painting" ); - System.out.println( "View: " + getView() ); - System.out.println( "Clip: " + g.getClipBounds() ); - new Exception().printStackTrace( System.out ); - System.out.println( "---------------------------------------------" ); - } - } }; } } diff --git a/flatlaf-testing/src/main/java/com/formdev/flatlaf/testing/FlatSmoothScrollingTest.jfd b/flatlaf-testing/src/main/java/com/formdev/flatlaf/testing/FlatSmoothScrollingTest.jfd index 3e762bb6f..1e582acb9 100644 --- a/flatlaf-testing/src/main/java/com/formdev/flatlaf/testing/FlatSmoothScrollingTest.jfd +++ b/flatlaf-testing/src/main/java/com/formdev/flatlaf/testing/FlatSmoothScrollingTest.jfd @@ -18,6 +18,66 @@ new FormModel { }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { "value": "cell 0 0,alignx left,growx 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 0,growx" + } ) + add( new FormComponent( "javax.swing.JLabel" ) { + name: "durationLabel" + "text": "Duration:" + auxiliary() { + "JavaCodeGenerator.variableLocal": true + } + }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { + "value": "cell 0 0" + } ) + add( new FormComponent( "javax.swing.JSlider" ) { + name: "durationSlider" + "maximum": 5000 + "value": 200 + "snapToTicks": true + "minimum": 100 + "minorTickSpacing": 50 + addEvent( new FormEvent( "javax.swing.event.ChangeListener", "stateChanged", "durationOrResolutionChanged", false ) ) + }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { + "value": "cell 0 0" + } ) + add( new FormComponent( "javax.swing.JLabel" ) { + name: "durationValueLabel" + "text": "0000 ms" + }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { + "value": "cell 0 0,width 50" + } ) + add( new FormComponent( "javax.swing.JLabel" ) { + name: "resolutionLabel" + "text": "Resolution:" + auxiliary() { + "JavaCodeGenerator.variableLocal": true + } + }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { + "value": "cell 0 0" + } ) + add( new FormComponent( "javax.swing.JSlider" ) { + name: "resolutionSlider" + "maximum": 1000 + "minimum": 10 + "value": 10 + "minorTickSpacing": 10 + "snapToTicks": true + addEvent( new FormEvent( "javax.swing.event.ChangeListener", "stateChanged", "durationOrResolutionChanged", false ) ) + }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { + "value": "cell 0 0" + } ) + add( new FormComponent( "javax.swing.JLabel" ) { + name: "resolutionValueLabel" + "text": "0000 ms" + }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { + "value": "cell 0 0,width 50" + } ) add( new FormContainer( "javax.swing.JSplitPane", new FormLayoutManager( class javax.swing.JSplitPane ) ) { name: "splitPane1" "orientation": 0 @@ -27,7 +87,7 @@ new FormModel { "orientation": 0 "resizeWeight": 0.5 add( new FormContainer( "javax.swing.JPanel", new FormLayoutManager( class net.miginfocom.swing.MigLayout ) { - "$columnConstraints": "[200,grow,fill][200,grow,fill][200,grow,fill][200,grow,fill]" + "$columnConstraints": "[200,grow,fill][200,grow,fill][200,grow,fill][200,grow,fill][fill]" "$rowConstraints": "[]0[200,grow,fill]" "$layoutConstraints": "ltr,insets 3,hidemode 3" } ) { @@ -53,6 +113,13 @@ new FormModel { }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { "value": "cell 2 0 2 1" } ) + add( new FormComponent( "javax.swing.JCheckBox" ) { + name: "rowHeaderCheckBox" + "text": "Row header" + addEvent( new FormEvent( "java.awt.event.ActionListener", "actionPerformed", "rowHeaderChanged", false ) ) + }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { + "value": "cell 2 0 2 1,alignx right,growx 0" + } ) add( new FormComponent( "javax.swing.JCheckBox" ) { name: "showTableGridCheckBox" "text": "Show table grid" @@ -97,6 +164,15 @@ new FormModel { }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { "value": "cell 2 1 2 1,width 100,height 100" } ) + add( new FormComponent( "javax.swing.JSlider" ) { + name: "scrollToSlider" + "orientation": 1 + "value": 0 + "inverted": true + addEvent( new FormEvent( "javax.swing.event.ChangeListener", "stateChanged", "scrollToChanged", false ) ) + }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { + "value": "cell 4 1" + } ) }, new FormLayoutConstraints( class java.lang.String ) { "value": "left" } ) @@ -160,9 +236,8 @@ new FormModel { add( new FormContainer( "com.formdev.flatlaf.testing.FlatSmoothScrollingTest$DebugScrollPane", new FormLayoutManager( class javax.swing.JScrollPane ) ) { name: "customScrollPane" add( new FormComponent( "javax.swing.JButton" ) { - name: "button1" + name: "customButton" "text": "I'm a large button, but do not implement Scrollable interface" - "preferredSize": new java.awt.Dimension( 800, 800 ) "horizontalAlignment": 10 "verticalAlignment": 1 } ) 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 index 362dce3bb..827b4e674 100644 --- a/flatlaf-testing/src/main/java/com/formdev/flatlaf/testing/LineChartPanel.java +++ b/flatlaf-testing/src/main/java/com/formdev/flatlaf/testing/LineChartPanel.java @@ -61,6 +61,10 @@ class LineChartPanel 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 @@ -106,8 +110,24 @@ public void setUpdateChartDelayed( boolean updateChartDelayed ) { updateChartDelayedCheckBox.setSelected( updateChartDelayed ); } - void addValue( double value, int ivalue, boolean dot, Color chartColor, String name ) { - lineChart.addValue( value, ivalue, dot, chartColor, name ); + 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() { @@ -255,19 +275,22 @@ private static class LineChart private int oneSecondWidth = 1000; private int msPerLineX = 200; + private final HashMap methodHighlightMap = new HashMap<>(); private static class Data { final double value; final int ivalue; - final boolean dot; + final Color dotColor; + final boolean dotOnly; final long time; // in milliseconds final String name; final Exception stack; - Data( double value, int ivalue, boolean dot, long time, String name, Exception stack ) { + Data( double value, int ivalue, Color dotColor, boolean dotOnly, long time, String name, Exception stack ) { this.value = value; this.ivalue = ivalue; - this.dot = dot; + this.dotColor = dotColor; + this.dotOnly = dotOnly; this.time = time; this.name = name; this.stack = stack; @@ -276,7 +299,8 @@ private static class Data { @Override public String toString() { // for debugging - return "value=" + value + ", ivalue=" + ivalue + ", dot=" + dot + ", time=" + time + ", name=" + name; + return "value=" + value + ", ivalue=" + ivalue + ", dotColor=" + dotColor + + ", dotOnly=" + dotOnly + ", time=" + time + ", name=" + name; } } @@ -297,9 +321,9 @@ public String toString() { ToolTipManager.sharedInstance().registerComponent( this ); } - void addValue( double value, int ivalue, boolean dot, Color chartColor, String name ) { + void addValue( Color chartColor, double value, int ivalue, Color dotColor, boolean dotOnly, String name ) { List chartData = color2dataMap.computeIfAbsent( chartColor, k -> new ArrayList<>() ); - chartData.add( new Data( value, ivalue, dot, System.nanoTime() / 1000000, name, new Exception() ) ); + chartData.add( new Data( value, ivalue, dotColor, dotOnly, System.nanoTime() / 1000000, name, new Exception() ) ); lastUsedChartColor = chartColor; @@ -452,12 +476,13 @@ private void paintImpl( Graphics2D g, int x, int y, int width, int height, doubl lastPoints.add( new Point( dx, dy ) ); lastDatas.add( data ); - if( data.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( chartColor ); + g.setColor( data.dotColor ); g.fillRect( dx - s1, dy - s1, s3, s3 ); - continue; + if( data.dotOnly ) + continue; } if( !newSeq ) { @@ -479,7 +504,7 @@ private void paintImpl( Graphics2D g, int x, int y, int width, int height, doubl int stage = 0; for( int j = i + 1; j < size && stage <= 2 && !isTemporaryValue; j++ ) { Data nextData = chartData.get( j ); - if( nextData.dot ) + if( nextData.dotOnly ) continue; // ignore dots // check whether next data point is within 10 milliseconds @@ -592,7 +617,7 @@ public String getToolTipText( MouseEvent e ) { Data data = lastDatas.get( i ); buf.append( "

" ); - if( data.dot ) + if( data.dotOnly ) buf.append( "DOT: " ); buf.append( data.name ).append( ' ' ).append( data.ivalue ).append( "

" ); @@ -601,6 +626,7 @@ public String getToolTipText( MouseEvent e ) { 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() ) ) @@ -614,11 +640,24 @@ public String getToolTipText( MouseEvent e ) { } 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( " " ); + .append( "" ); + if( highlight != null ) + buf.append( "" ); + + // append source + buf.append( " " ); if( stackElement.getFileName() != null ) { buf.append( '(' ); buf.append( stackElement.getFileName() ); @@ -628,14 +667,19 @@ public String getToolTipText( MouseEvent e ) { } 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( (className.startsWith( "java.awt.event.InvocationEvent" ) && methodName.equals( "dispatch" )) || - (className.startsWith( "java.awt.Component" ) && methodName.equals( "processMouseWheelEvent" )) || - (className.startsWith( "javax.swing.JComponent" ) && methodName.equals( "processKeyBinding" )) ) + 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( "com.formdev.flatlaf.util.Animator.timingEvent" ) ) break; } buf.append( "..." );