From 203426bd55bc86706244e800f6a029461b3084f0 Mon Sep 17 00:00:00 2001 From: Karl Tauber Date: Wed, 7 Oct 2020 10:36:16 +0200 Subject: [PATCH 01/10] TabbedPane: support scrolling tabs with mouse wheel (if tabLayoutPolicy is SCROLL_TAB_LAYOUT) (issue #40) --- CHANGELOG.md | 8 ++ .../formdev/flatlaf/ui/FlatTabbedPaneUI.java | 117 +++++++++++++++--- 2 files changed, 105 insertions(+), 20 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 98ab6d61..20bea270 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,14 @@ FlatLaf Change Log ================== +## 0.44-SNAPSHOT + +#### New features and improvements + +- TabbedPane: Support scrolling tabs with mouse wheel (if `tabLayoutPolicy` is + `SCROLL_TAB_LAYOUT`). (issue #40) + + ## 0.43 #### New features and improvements 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 ed319855..1a4de095 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 @@ -20,17 +20,21 @@ import static com.formdev.flatlaf.util.UIScale.scale; import static com.formdev.flatlaf.FlatClientProperties.*; import java.awt.Color; import java.awt.Component; -import java.awt.Container; +import java.awt.Dimension; import java.awt.Font; import java.awt.FontMetrics; import java.awt.Graphics; import java.awt.Graphics2D; import java.awt.Insets; import java.awt.KeyboardFocusManager; +import java.awt.Point; import java.awt.Rectangle; import java.awt.Shape; import java.awt.event.InputEvent; import java.awt.event.KeyEvent; +import java.awt.event.MouseAdapter; +import java.awt.event.MouseEvent; +import java.awt.event.MouseWheelEvent; import java.awt.geom.Path2D; import java.awt.geom.Rectangle2D; import java.beans.PropertyChangeEvent; @@ -40,6 +44,7 @@ import java.util.Set; import javax.swing.JButton; import javax.swing.JComponent; import javax.swing.JTabbedPane; +import javax.swing.JViewport; import javax.swing.KeyStroke; import javax.swing.UIManager; import javax.swing.plaf.ComponentUI; @@ -117,6 +122,9 @@ public class FlatTabbedPaneUI protected boolean hasFullBorder; protected boolean tabsOverlapBorder; + protected JViewport tabViewport; + protected FlatWheelTabScroller wheelTabScroller; + public static ComponentUI createUI( JComponent c ) { return new FlatTabbedPaneUI(); } @@ -187,6 +195,50 @@ public class FlatTabbedPaneUI MigLayoutVisualPadding.uninstall( tabPane ); } + @Override + protected void installComponents() { + super.installComponents(); + + // find scrollable tab viewport + tabViewport = null; + if( isScrollTabLayout() ) { + for( Component c : tabPane.getComponents() ) { + if( c instanceof JViewport && c.getClass().getName().equals( "javax.swing.plaf.basic.BasicTabbedPaneUI$ScrollableTabViewport" ) ) { + tabViewport = (JViewport) c; + break; + } + } + } + } + + @Override + protected void installListeners() { + super.installListeners(); + + if( tabViewport != null && (wheelTabScroller = createWheelTabScroller()) != null ) { + // ideally we would add the mouse listeners to the viewport, but then the + // mouse listener of the tabbed pane would not receive events while + // the mouse pointer is over the viewport + tabPane.addMouseWheelListener( wheelTabScroller ); + tabPane.addMouseListener( wheelTabScroller ); + } + } + + @Override + protected void uninstallListeners() { + super.uninstallListeners(); + + if( wheelTabScroller != null ) { + tabPane.removeMouseWheelListener( wheelTabScroller ); + tabPane.removeMouseListener( wheelTabScroller ); + wheelTabScroller = null; + } + } + + protected FlatWheelTabScroller createWheelTabScroller() { + return new FlatWheelTabScroller(); + } + @Override protected PropertyChangeListener createPropertyChangeListener() { return new BasicTabbedPaneUI.PropertyChangeHandler() { @@ -489,17 +541,13 @@ public class FlatTabbedPaneUI // repaint selection in scroll-tab-layout because it may be painted before // the content border was painted (from BasicTabbedPaneUI$ScrollableTabPanel) - if( isScrollTabLayout() && selectedIndex >= 0 ) { - Component scrollableTabViewport = findComponentByClassName( tabPane, - BasicTabbedPaneUI.class.getName() + "$ScrollableTabViewport" ); - if( scrollableTabViewport != null ) { - Rectangle tabRect = getTabBounds( tabPane, selectedIndex ); + if( isScrollTabLayout() && selectedIndex >= 0 && tabViewport != null ) { + Rectangle tabRect = getTabBounds( tabPane, selectedIndex ); - Shape oldClip = g.getClip(); - g.setClip( scrollableTabViewport.getBounds() ); - paintTabSelection( g, tabPlacement, tabRect.x, tabRect.y, tabRect.width, tabRect.height ); - g.setClip( oldClip ); - } + Shape oldClip = g.getClip(); + g.setClip( tabViewport.getBounds() ); + paintTabSelection( g, tabPlacement, tabRect.x, tabRect.y, tabRect.width, tabRect.height ); + g.setClip( oldClip ); } } @@ -518,17 +566,46 @@ public class FlatTabbedPaneUI return tabPane.getTabLayoutPolicy() == JTabbedPane.SCROLL_TAB_LAYOUT; } - private Component findComponentByClassName( Container c, String className ) { - for( Component child : c.getComponents() ) { - if( className.equals( child.getClass().getName() ) ) - return child; + //---- class FlatWheelTabScroller ----------------------------------------- - if( child instanceof Container ) { - Component c2 = findComponentByClassName( (Container) child, className ); - if( c2 != null ) - return c2; + protected class FlatWheelTabScroller + extends MouseAdapter + { + @Override + public void mouseWheelMoved( MouseWheelEvent e ) { + // because this listener receives mouse events for the whole tabbed pane, + // we have to check whether the mouse is located over the viewport + if( tabViewport == null || !tabViewport.getBounds().contains( e.getX(), e.getY() ) ) + return; + + // compute new view position + Point viewPosition = tabViewport.getViewPosition(); + Dimension viewSize = tabViewport.getViewSize(); + int x = viewPosition.x; + int y = viewPosition.y; + int tabPlacement = tabPane.getTabPlacement(); + if( tabPlacement == TOP || tabPlacement == BOTTOM ) { + x += maxTabHeight * e.getWheelRotation(); + x = Math.min( Math.max( x, 0 ), viewSize.width - tabViewport.getWidth() ); + } else { + y += maxTabHeight * e.getWheelRotation(); + y = Math.min( Math.max( y, 0 ), viewSize.height - tabViewport.getHeight() ); } + + // update view position + tabViewport.setViewPosition( new Point( x, y ) ); + + updateHover( e ); + } + + @Override + public void mousePressed( MouseEvent e ) { + // for the case that the tab was only partly visible before the user clicked it + updateHover( e ); + } + + private void updateHover( MouseEvent e ) { + setRolloverTab( tabForCoordinate( tabPane, e.getX(), e.getY() ) ); } - return null; } } From 2f3427e6ad672cc5b3fe5dd958327d3f6aa8cc43 Mon Sep 17 00:00:00 2001 From: Karl Tauber Date: Wed, 7 Oct 2020 13:29:15 +0200 Subject: [PATCH 02/10] TabbedPane: scroll selected tab into visible area (500ms delayed) if mouse exits scroll viewport after wheel scrolling (issue #40) --- .../formdev/flatlaf/ui/FlatTabbedPaneUI.java | 75 ++++++++++++++++++- 1 file changed, 72 insertions(+), 3 deletions(-) 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 1a4de095..ec85c592 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 @@ -46,6 +46,7 @@ import javax.swing.JComponent; import javax.swing.JTabbedPane; import javax.swing.JViewport; import javax.swing.KeyStroke; +import javax.swing.Timer; import javax.swing.UIManager; import javax.swing.plaf.ComponentUI; import javax.swing.plaf.UIResource; @@ -220,6 +221,7 @@ public class FlatTabbedPaneUI // mouse listener of the tabbed pane would not receive events while // the mouse pointer is over the viewport tabPane.addMouseWheelListener( wheelTabScroller ); + tabPane.addMouseMotionListener( wheelTabScroller ); tabPane.addMouseListener( wheelTabScroller ); } } @@ -229,7 +231,10 @@ public class FlatTabbedPaneUI super.uninstallListeners(); if( wheelTabScroller != null ) { + wheelTabScroller.uninstall(); + tabPane.removeMouseWheelListener( wheelTabScroller ); + tabPane.removeMouseMotionListener( wheelTabScroller ); tabPane.removeMouseListener( wheelTabScroller ); wheelTabScroller = null; } @@ -571,11 +576,20 @@ public class FlatTabbedPaneUI protected class FlatWheelTabScroller extends MouseAdapter { + private boolean inViewport; + private boolean scrolled; + private Timer timer; + + protected void uninstall() { + if( timer != null ) + timer.stop(); + } + @Override public void mouseWheelMoved( MouseWheelEvent e ) { // because this listener receives mouse events for the whole tabbed pane, // we have to check whether the mouse is located over the viewport - if( tabViewport == null || !tabViewport.getBounds().contains( e.getX(), e.getY() ) ) + if( !isInViewport( e ) ) return; // compute new view position @@ -593,19 +607,74 @@ public class FlatTabbedPaneUI } // update view position - tabViewport.setViewPosition( new Point( x, y ) ); + Point newViewPosition = new Point( x, y ); + tabViewport.setViewPosition( newViewPosition ); + + if( !newViewPosition.equals( viewPosition )) + scrolled = true; updateHover( e ); } + @Override + public void mouseMoved( MouseEvent e ) { + boolean wasInViewport = inViewport; + inViewport = isInViewport( e ); + + if( inViewport != wasInViewport ) { + if( !inViewport ) + viewportExited(); + else if( timer != null ) + timer.stop(); + } + } + + @Override + public void mouseExited( MouseEvent e ) { + inViewport = false; + viewportExited(); + } + @Override public void mousePressed( MouseEvent e ) { // for the case that the tab was only partly visible before the user clicked it updateHover( e ); } - private void updateHover( MouseEvent e ) { + protected boolean isInViewport( MouseEvent e ) { + return (tabViewport != null && tabViewport.getBounds().contains( e.getX(), e.getY() ) ); + } + + protected void updateHover( MouseEvent e ) { setRolloverTab( tabForCoordinate( tabPane, e.getX(), e.getY() ) ); } + + protected void viewportExited() { + if( !scrolled ) + return; + + if( timer == null ) { + timer = new Timer( 500, e -> ensureSelectedTabVisible() ); + timer.setRepeats( false ); + } + + timer.start(); + } + + protected void ensureSelectedTabVisible() { + // check whether UI delegate was uninstalled because this method is invoked via timer + if( tabPane == null || tabViewport == null ) + return; + + if( !scrolled || tabViewport == null ) + return; + scrolled = false; + + int selectedIndex = tabPane.getSelectedIndex(); + if( selectedIndex >= 0 ) { + Rectangle tabBounds = getTabBounds( tabPane, selectedIndex ); + tabViewport.scrollRectToVisible( tabBounds ); + } + } } } From de870c546c5d61664c84b6fc24ec3e23b2f296ee Mon Sep 17 00:00:00 2001 From: Karl Tauber Date: Wed, 7 Oct 2020 15:14:47 +0200 Subject: [PATCH 03/10] TabbedPane: repeat scrolling as long as arrow buttons are pressed (issue #40) --- CHANGELOG.md | 1 + .../formdev/flatlaf/ui/FlatTabbedPaneUI.java | 65 ++++++++++- .../flatlaf/testing/FlatContainerTest.java | 101 +++++++++++------- .../flatlaf/testing/FlatContainerTest.jfd | 58 +++++++--- 4 files changed, 167 insertions(+), 58 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 20bea270..0e38dd63 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,7 @@ FlatLaf Change Log - TabbedPane: Support scrolling tabs with mouse wheel (if `tabLayoutPolicy` is `SCROLL_TAB_LAYOUT`). (issue #40) +- TabbedPane: Repeat scrolling as long as arrow buttons are pressed. (issue #40) ## 0.43 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 ec85c592..52cb8836 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 @@ -34,6 +34,7 @@ import java.awt.event.InputEvent; import java.awt.event.KeyEvent; import java.awt.event.MouseAdapter; import java.awt.event.MouseEvent; +import java.awt.event.MouseListener; import java.awt.event.MouseWheelEvent; import java.awt.geom.Path2D; import java.awt.geom.Rectangle2D; @@ -46,6 +47,7 @@ import javax.swing.JComponent; import javax.swing.JTabbedPane; import javax.swing.JViewport; import javax.swing.KeyStroke; +import javax.swing.SwingUtilities; import javax.swing.Timer; import javax.swing.UIManager; import javax.swing.plaf.ComponentUI; @@ -266,11 +268,7 @@ public class FlatTabbedPaneUI @Override protected JButton createScrollButton( int direction ) { - // this method is invoked before installDefaults(), so we can not use color fields here - return new FlatArrowButton( direction, UIManager.getString( "Component.arrowType" ), - UIManager.getColor( "TabbedPane.foreground" ), - UIManager.getColor( "TabbedPane.disabledForeground" ), null, - UIManager.getColor( "TabbedPane.hoverColor" ) ); + return new FlatScrollableTabButton( direction ); } @Override @@ -571,6 +569,63 @@ public class FlatTabbedPaneUI return tabPane.getTabLayoutPolicy() == JTabbedPane.SCROLL_TAB_LAYOUT; } + //---- class FlatScrollableTabButton -------------------------------------- + + protected static class FlatScrollableTabButton + extends FlatArrowButton + implements MouseListener + { + private Timer autoRepeatTimer; + + protected FlatScrollableTabButton( int direction ) { + // this method is invoked before installDefaults(), so we can not use color fields here + super( direction, UIManager.getString( "Component.arrowType" ), + UIManager.getColor( "TabbedPane.foreground" ), + UIManager.getColor( "TabbedPane.disabledForeground" ), null, + UIManager.getColor( "TabbedPane.hoverColor" ) ); + + addMouseListener( this ); + } + + @Override + public void mousePressed( MouseEvent e ) { + if( SwingUtilities.isLeftMouseButton( e ) && isEnabled() ) { + if( autoRepeatTimer == null ) { + // using same delays as in BasicScrollBarUI and BasicSpinnerUI + autoRepeatTimer = new Timer( 60, e2 -> { + if( isEnabled() ) + doClick(); + } ); + autoRepeatTimer.setInitialDelay( 300 ); + } + + autoRepeatTimer.start(); + } + } + + @Override + public void mouseReleased( MouseEvent e ) { + if( autoRepeatTimer != null ) + autoRepeatTimer.stop(); + } + + @Override + public void mouseClicked( MouseEvent e ) { + } + + @Override + public void mouseEntered( MouseEvent e ) { + if( autoRepeatTimer != null && isPressed() ) + autoRepeatTimer.start(); + } + + @Override + public void mouseExited( MouseEvent e ) { + if( autoRepeatTimer != null ) + autoRepeatTimer.stop(); + } + } + //---- class FlatWheelTabScroller ----------------------------------------- protected class FlatWheelTabScroller diff --git a/flatlaf-testing/src/main/java/com/formdev/flatlaf/testing/FlatContainerTest.java b/flatlaf-testing/src/main/java/com/formdev/flatlaf/testing/FlatContainerTest.java index 4b89082a..96a0b0d1 100644 --- a/flatlaf-testing/src/main/java/com/formdev/flatlaf/testing/FlatContainerTest.java +++ b/flatlaf-testing/src/main/java/com/formdev/flatlaf/testing/FlatContainerTest.java @@ -32,6 +32,9 @@ import net.miginfocom.swing.*; public class FlatContainerTest extends FlatTestPanel { + private final int initialTabCount; + private boolean autoMoreTabs; + public static void main( String[] args ) { SwingUtilities.invokeLater( () -> { FlatTestFrame frame = FlatTestFrame.create( args, "FlatContainerTest" ); @@ -43,6 +46,7 @@ public class FlatContainerTest initComponents(); addInitialTabs( tabbedPane1, tabbedPane2, tabbedPane3, tabbedPane4 ); + initialTabCount = tabbedPane1.getTabCount(); } private void tabScrollChanged() { @@ -63,8 +67,6 @@ public class FlatContainerTest } } - private boolean autoMoreTabs; - private void showTabSeparatorsChanged() { Boolean showTabSeparators = showTabSeparatorsCheckBox.isSelected() ? true : null; putTabbedPanesClientProperty( TABBED_PANE_SHOW_TAB_SEPARATORS, showTabSeparators ); @@ -88,28 +90,30 @@ public class FlatContainerTest } private void moreTabsChanged() { - boolean moreTabs = moreTabsCheckBox.isSelected(); - addRemoveMoreTabs( tabbedPane1, moreTabs ); - addRemoveMoreTabs( tabbedPane2, moreTabs ); - addRemoveMoreTabs( tabbedPane3, moreTabs ); - addRemoveMoreTabs( tabbedPane4, moreTabs ); + moreTabsSpinnerChanged(); autoMoreTabs = false; + + moreTabsSpinner.setEnabled( moreTabsCheckBox.isSelected() ); } - private void addRemoveMoreTabs( JTabbedPane tabbedPane, boolean add ) { - if( add ) { - addTab( tabbedPane, "Tab 4", "tab content 4" ); - addTab( tabbedPane, "Tab 5", "tab content 5" ); - addTab( tabbedPane, "Tab 6", "tab content 6" ); - addTab( tabbedPane, "Tab 7", "tab content 7" ); - addTab( tabbedPane, "Tab 8", "tab content 8" ); - } else { - int tabCount = tabbedPane.getTabCount(); - if( tabCount > 4 ) { - for( int i = 0; i < 5; i++ ) - tabbedPane.removeTabAt( tabbedPane.getTabCount() - 1 ); - } + private void moreTabsSpinnerChanged() { + addRemoveMoreTabs( tabbedPane1 ); + addRemoveMoreTabs( tabbedPane2 ); + addRemoveMoreTabs( tabbedPane3 ); + addRemoveMoreTabs( tabbedPane4 ); + } + + private void addRemoveMoreTabs( JTabbedPane tabbedPane ) { + int oldTabCount = tabbedPane.getTabCount(); + int newTabCount = initialTabCount + (moreTabsCheckBox.isSelected() ? (Integer) moreTabsSpinner.getValue() : 0); + + if( newTabCount > oldTabCount ) { + for( int i = oldTabCount + 1; i <= newTabCount; i++ ) + addTab( tabbedPane, "Tab " + i, "tab content " + i ); + } else if( newTabCount < oldTabCount ) { + while( tabbedPane.getTabCount() > 4 ) + tabbedPane.removeTabAt( tabbedPane.getTabCount() - 1 ); } } @@ -162,6 +166,17 @@ public class FlatContainerTest tabbedPane.setIconAt( 1, icon ); } + private void customBorderChanged() { + Border border = customBorderCheckBox.isSelected() + ? new LineBorder( Color.magenta, 20 ) + : null; + + tabbedPane1.setBorder( border ); + tabbedPane2.setBorder( border ); + tabbedPane3.setBorder( border ); + tabbedPane4.setBorder( border ); + } + private void initComponents() { // JFormDesigner - Component initialization - DO NOT MODIFY //GEN-BEGIN:initComponents JPanel panel9 = new JPanel(); @@ -182,12 +197,14 @@ public class FlatContainerTest tabbedPane4 = new JTabbedPane(); JPanel panel14 = new JPanel(); moreTabsCheckBox = new JCheckBox(); + moreTabsSpinner = new JSpinner(); tabScrollCheckBox = new JCheckBox(); showTabSeparatorsCheckBox = new JCheckBox(); hideContentSeparatorCheckBox = new JCheckBox(); - hasFullBorderCheckBox = new JCheckBox(); tabIconsCheckBox = new JCheckBox(); tabIconSizeSpinner = new JSpinner(); + customBorderCheckBox = new JCheckBox(); + hasFullBorderCheckBox = new JCheckBox(); CellConstraints cc = new CellConstraints(); //======== this ======== @@ -294,14 +311,13 @@ public class FlatContainerTest "insets 0,hidemode 3", // columns "[]" + - "[]" + - "[]" + "[fill]" + "[]" + - "[fill]" + + "[]" + "[fill]", // rows - "[center]")); + "[center]" + + "[]")); //---- moreTabsCheckBox ---- moreTabsCheckBox.setText("More tabs"); @@ -309,37 +325,48 @@ public class FlatContainerTest moreTabsCheckBox.addActionListener(e -> moreTabsChanged()); panel14.add(moreTabsCheckBox, "cell 0 0"); + //---- moreTabsSpinner ---- + moreTabsSpinner.setModel(new SpinnerNumberModel(4, 0, null, 1)); + moreTabsSpinner.setEnabled(false); + moreTabsSpinner.addChangeListener(e -> moreTabsSpinnerChanged()); + panel14.add(moreTabsSpinner, "cell 1 0"); + //---- tabScrollCheckBox ---- tabScrollCheckBox.setText("Use scroll layout"); tabScrollCheckBox.setMnemonic('S'); tabScrollCheckBox.addActionListener(e -> tabScrollChanged()); - panel14.add(tabScrollCheckBox, "cell 1 0,alignx left,growx 0"); + panel14.add(tabScrollCheckBox, "cell 2 0,alignx left,growx 0"); //---- showTabSeparatorsCheckBox ---- showTabSeparatorsCheckBox.setText("Show tab separators"); showTabSeparatorsCheckBox.addActionListener(e -> showTabSeparatorsChanged()); - panel14.add(showTabSeparatorsCheckBox, "cell 2 0"); + panel14.add(showTabSeparatorsCheckBox, "cell 3 0"); //---- hideContentSeparatorCheckBox ---- hideContentSeparatorCheckBox.setText("Hide content separator"); hideContentSeparatorCheckBox.addActionListener(e -> hideContentSeparatorChanged()); - panel14.add(hideContentSeparatorCheckBox, "cell 3 0"); - - //---- hasFullBorderCheckBox ---- - hasFullBorderCheckBox.setText("Show content border"); - hasFullBorderCheckBox.addActionListener(e -> hasFullBorderChanged()); - panel14.add(hasFullBorderCheckBox, "cell 4 0,alignx left,growx 0"); + panel14.add(hideContentSeparatorCheckBox, "cell 4 0"); //---- tabIconsCheckBox ---- tabIconsCheckBox.setText("Tab icons"); tabIconsCheckBox.addActionListener(e -> tabIconsChanged()); - panel14.add(tabIconsCheckBox, "cell 5 0"); + panel14.add(tabIconsCheckBox, "cell 0 1"); //---- tabIconSizeSpinner ---- tabIconSizeSpinner.setModel(new SpinnerListModel(new String[] {"16", "24", "32", "48", "64"})); tabIconSizeSpinner.setEnabled(false); tabIconSizeSpinner.addChangeListener(e -> tabIconsChanged()); - panel14.add(tabIconSizeSpinner, "cell 6 0"); + panel14.add(tabIconSizeSpinner, "cell 1 1"); + + //---- customBorderCheckBox ---- + customBorderCheckBox.setText("Custom border"); + customBorderCheckBox.addActionListener(e -> customBorderChanged()); + panel14.add(customBorderCheckBox, "cell 2 1"); + + //---- hasFullBorderCheckBox ---- + hasFullBorderCheckBox.setText("Show content border"); + hasFullBorderCheckBox.addActionListener(e -> hasFullBorderChanged()); + panel14.add(hasFullBorderCheckBox, "cell 4 1,alignx left,growx 0"); } panel9.add(panel14, cc.xywh(1, 11, 3, 1)); } @@ -353,12 +380,14 @@ public class FlatContainerTest private JTabbedPane tabbedPane2; private JTabbedPane tabbedPane4; private JCheckBox moreTabsCheckBox; + private JSpinner moreTabsSpinner; private JCheckBox tabScrollCheckBox; private JCheckBox showTabSeparatorsCheckBox; private JCheckBox hideContentSeparatorCheckBox; - private JCheckBox hasFullBorderCheckBox; private JCheckBox tabIconsCheckBox; private JSpinner tabIconSizeSpinner; + private JCheckBox customBorderCheckBox; + private JCheckBox hasFullBorderCheckBox; // JFormDesigner - End of variables declaration //GEN-END:variables //---- class Tab1Panel ---------------------------------------------------- diff --git a/flatlaf-testing/src/main/java/com/formdev/flatlaf/testing/FlatContainerTest.jfd b/flatlaf-testing/src/main/java/com/formdev/flatlaf/testing/FlatContainerTest.jfd index a68eed26..aa07cade 100644 --- a/flatlaf-testing/src/main/java/com/formdev/flatlaf/testing/FlatContainerTest.jfd +++ b/flatlaf-testing/src/main/java/com/formdev/flatlaf/testing/FlatContainerTest.jfd @@ -131,8 +131,8 @@ new FormModel { } ) add( new FormContainer( "javax.swing.JPanel", new FormLayoutManager( class net.miginfocom.swing.MigLayout ) { "$layoutConstraints": "insets 0,hidemode 3" - "$columnConstraints": "[][][][fill][][fill][fill]" - "$rowConstraints": "[center]" + "$columnConstraints": "[][fill][][][fill]" + "$rowConstraints": "[center][]" } ) { name: "panel14" "opaque": false @@ -147,6 +147,20 @@ new FormModel { }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { "value": "cell 0 0" } ) + add( new FormComponent( "javax.swing.JSpinner" ) { + name: "moreTabsSpinner" + "model": new javax.swing.SpinnerNumberModel { + minimum: 0 + value: 4 + } + "enabled": false + auxiliary() { + "JavaCodeGenerator.variableLocal": false + } + addEvent( new FormEvent( "javax.swing.event.ChangeListener", "stateChanged", "moreTabsSpinnerChanged", false ) ) + }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { + "value": "cell 1 0" + } ) add( new FormComponent( "javax.swing.JCheckBox" ) { name: "tabScrollCheckBox" "text": "Use scroll layout" @@ -156,7 +170,7 @@ new FormModel { } addEvent( new FormEvent( "java.awt.event.ActionListener", "actionPerformed", "tabScrollChanged", false ) ) }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { - "value": "cell 1 0,alignx left,growx 0" + "value": "cell 2 0,alignx left,growx 0" } ) add( new FormComponent( "javax.swing.JCheckBox" ) { name: "showTabSeparatorsCheckBox" @@ -166,7 +180,7 @@ new FormModel { } addEvent( new FormEvent( "java.awt.event.ActionListener", "actionPerformed", "showTabSeparatorsChanged", false ) ) }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { - "value": "cell 2 0" + "value": "cell 3 0" } ) add( new FormComponent( "javax.swing.JCheckBox" ) { name: "hideContentSeparatorCheckBox" @@ -176,17 +190,7 @@ new FormModel { } addEvent( new FormEvent( "java.awt.event.ActionListener", "actionPerformed", "hideContentSeparatorChanged", false ) ) }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { - "value": "cell 3 0" - } ) - add( new FormComponent( "javax.swing.JCheckBox" ) { - name: "hasFullBorderCheckBox" - "text": "Show content border" - auxiliary() { - "JavaCodeGenerator.variableLocal": false - } - addEvent( new FormEvent( "java.awt.event.ActionListener", "actionPerformed", "hasFullBorderChanged", false ) ) - }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { - "value": "cell 4 0,alignx left,growx 0" + "value": "cell 4 0" } ) add( new FormComponent( "javax.swing.JCheckBox" ) { name: "tabIconsCheckBox" @@ -196,7 +200,7 @@ new FormModel { } addEvent( new FormEvent( "java.awt.event.ActionListener", "actionPerformed", "tabIconsChanged", false ) ) }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { - "value": "cell 5 0" + "value": "cell 0 1" } ) add( new FormComponent( "javax.swing.JSpinner" ) { name: "tabIconSizeSpinner" @@ -215,7 +219,27 @@ new FormModel { } addEvent( new FormEvent( "javax.swing.event.ChangeListener", "stateChanged", "tabIconsChanged", false ) ) }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { - "value": "cell 6 0" + "value": "cell 1 1" + } ) + add( new FormComponent( "javax.swing.JCheckBox" ) { + name: "customBorderCheckBox" + "text": "Custom border" + auxiliary() { + "JavaCodeGenerator.variableLocal": false + } + addEvent( new FormEvent( "java.awt.event.ActionListener", "actionPerformed", "customBorderChanged", false ) ) + }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { + "value": "cell 2 1" + } ) + add( new FormComponent( "javax.swing.JCheckBox" ) { + name: "hasFullBorderCheckBox" + "text": "Show content border" + auxiliary() { + "JavaCodeGenerator.variableLocal": false + } + addEvent( new FormEvent( "java.awt.event.ActionListener", "actionPerformed", "hasFullBorderChanged", false ) ) + }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { + "value": "cell 4 1,alignx left,growx 0" } ) }, new FormLayoutConstraints( class com.jgoodies.forms.layout.CellConstraints ) { "gridY": 11 From a7c906091c40f9000a27af08eebeff043bced365 Mon Sep 17 00:00:00 2001 From: Karl Tauber Date: Wed, 7 Oct 2020 17:54:12 +0200 Subject: [PATCH 04/10] TabbedPane: use animation for scrolling tabs with mouse wheel (issue #40) --- .../formdev/flatlaf/FlatSystemProperties.java | 8 ++ .../formdev/flatlaf/ui/FlatTabbedPaneUI.java | 85 ++++++++++++++++--- .../com/formdev/flatlaf/util/Animator.java | 10 +++ 3 files changed, 93 insertions(+), 10 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 3144dab1..eb52cf12 100644 --- a/flatlaf-core/src/main/java/com/formdev/flatlaf/FlatSystemProperties.java +++ b/flatlaf-core/src/main/java/com/formdev/flatlaf/FlatSystemProperties.java @@ -92,6 +92,14 @@ public interface FlatSystemProperties */ String MENUBAR_EMBEDDED = "flatlaf.menuBarEmbedded"; + /** + * Specifies whether animations are enabled. + *

+ * Allowed Values {@code false} and {@code true}
+ * Default {@code true} + */ + String ANIMATION = "flatlaf.animation"; + /** * 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/FlatTabbedPaneUI.java b/flatlaf-core/src/main/java/com/formdev/flatlaf/ui/FlatTabbedPaneUI.java index 52cb8836..3fb72467 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 @@ -55,6 +55,8 @@ import javax.swing.plaf.UIResource; import javax.swing.plaf.basic.BasicTabbedPaneUI; import javax.swing.text.View; import com.formdev.flatlaf.FlatLaf; +import com.formdev.flatlaf.util.Animator; +import com.formdev.flatlaf.util.CubicBezierEasing; import com.formdev.flatlaf.util.UIScale; /** @@ -635,20 +637,33 @@ public class FlatTabbedPaneUI private boolean scrolled; private Timer timer; + private Animator animator; + private Point startViewPosition; + private Point targetViewPosition; + private int lastMouseX; + private int lastMouseY; + protected void uninstall() { if( timer != null ) timer.stop(); + if( animator != null ) + animator.cancel(); } @Override public void mouseWheelMoved( MouseWheelEvent e ) { // because this listener receives mouse events for the whole tabbed pane, // we have to check whether the mouse is located over the viewport - if( !isInViewport( e ) ) + if( !isInViewport( e.getX(), e.getY() ) ) return; + lastMouseX = e.getX(); + lastMouseY = e.getY(); + // compute new view position - Point viewPosition = tabViewport.getViewPosition(); + Point viewPosition = (targetViewPosition != null) + ? targetViewPosition + : tabViewport.getViewPosition(); Dimension viewSize = tabViewport.getViewSize(); int x = viewPosition.x; int y = viewPosition.y; @@ -663,18 +678,68 @@ public class FlatTabbedPaneUI // update view position Point newViewPosition = new Point( x, y ); - tabViewport.setViewPosition( newViewPosition ); + setViewPositionAnimated( newViewPosition ); if( !newViewPosition.equals( viewPosition )) scrolled = true; + } - updateHover( e ); + protected void setViewPositionAnimated( Point viewPosition ) { + // check whether position is equal to current position + if( viewPosition.equals( tabViewport.getViewPosition() ) ) + return; + + // do not use animation if disabled + if( !Animator.useAnimation() ) { + tabViewport.setViewPosition( viewPosition ); + updateHover( lastMouseX, lastMouseY ); + return; + } + + // remember start and target view positions + if( startViewPosition == null ) + startViewPosition = tabViewport.getViewPosition(); + targetViewPosition = viewPosition; + + // create animator + if( animator == null ) { + // using same delays as in FlatScrollBarUI + int duration = 200; + int resolution = 10; + + animator = new Animator( duration, fraction -> { + if( tabViewport == null || !tabViewport.isShowing() ) { + animator.stop(); + return; + } + + // update view position + int x = startViewPosition.x + Math.round( (targetViewPosition.x - startViewPosition.x) * fraction ); + int y = startViewPosition.y + Math.round( (targetViewPosition.y - startViewPosition.y) * fraction ); + tabViewport.setViewPosition( new Point( x, y ) ); + }, () -> { + startViewPosition = targetViewPosition = null; + + if( tabPane != null ) + updateHover( lastMouseX, lastMouseY ); + } ); + + animator.setResolution( resolution ); + animator.setInterpolator( new CubicBezierEasing( 0.5f, 0.5f, 0.5f, 1 ) ); + } + + // restart animator + animator.cancel(); + animator.start(); } @Override public void mouseMoved( MouseEvent e ) { + lastMouseX = e.getX(); + lastMouseY = e.getY(); + boolean wasInViewport = inViewport; - inViewport = isInViewport( e ); + inViewport = isInViewport( e.getX(), e.getY() ); if( inViewport != wasInViewport ) { if( !inViewport ) @@ -693,15 +758,15 @@ public class FlatTabbedPaneUI @Override public void mousePressed( MouseEvent e ) { // for the case that the tab was only partly visible before the user clicked it - updateHover( e ); + updateHover( e.getX(), e.getY() ); } - protected boolean isInViewport( MouseEvent e ) { - return (tabViewport != null && tabViewport.getBounds().contains( e.getX(), e.getY() ) ); + protected boolean isInViewport( int x, int y ) { + return (tabViewport != null && tabViewport.getBounds().contains( x, y ) ); } - protected void updateHover( MouseEvent e ) { - setRolloverTab( tabForCoordinate( tabPane, e.getX(), e.getY() ) ); + protected void updateHover( int x, int y ) { + setRolloverTab( tabForCoordinate( tabPane, x, y ) ); } protected void viewportExited() { diff --git a/flatlaf-core/src/main/java/com/formdev/flatlaf/util/Animator.java b/flatlaf-core/src/main/java/com/formdev/flatlaf/util/Animator.java index 1bb72bdb..abf00b18 100644 --- a/flatlaf-core/src/main/java/com/formdev/flatlaf/util/Animator.java +++ b/flatlaf-core/src/main/java/com/formdev/flatlaf/util/Animator.java @@ -18,6 +18,7 @@ package com.formdev.flatlaf.util; import java.util.ArrayList; import javax.swing.Timer; +import com.formdev.flatlaf.FlatSystemProperties; /** * Simple animator based on ideas and concepts from "Filthy Rich Clients" book @@ -39,6 +40,15 @@ public class Animator private long startTime; private Timer timer; + /** + * Checks whether animations are enabled (the default) or disabled via + * system property {@code flatlaf.animation} set to {@code false}. + * This allows disabling all animations at command line with {@code -Dflatlaf.animation=false}. + */ + public static boolean useAnimation() { + return FlatSystemProperties.getBoolean( FlatSystemProperties.ANIMATION, true ); + } + /** * Creates an animation that runs duration milliseconds. * Use {@link #addTarget(TimingTarget)} to receive timing events From 15ac77107ffa854b4dd094bca565bf65935bfe43 Mon Sep 17 00:00:00 2001 From: Karl Tauber Date: Wed, 7 Oct 2020 19:09:19 +0200 Subject: [PATCH 05/10] TabbedPane: increased size of scroll arrow buttons (issue #40) --- .../java/com/formdev/flatlaf/ui/FlatTabbedPaneUI.java | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) 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 3fb72467..7740995b 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 @@ -573,7 +573,7 @@ public class FlatTabbedPaneUI //---- class FlatScrollableTabButton -------------------------------------- - protected static class FlatScrollableTabButton + protected class FlatScrollableTabButton extends FlatArrowButton implements MouseListener { @@ -589,6 +589,15 @@ public class FlatTabbedPaneUI addMouseListener( this ); } + @Override + public Dimension getPreferredSize() { + Dimension size = super.getPreferredSize(); + if( direction == WEST || direction == EAST ) + return new Dimension( size.width, Math.max( size.height, maxTabHeight ) ); + else + return new Dimension( Math.max( size.width, maxTabWidth ), size.height ); + } + @Override public void mousePressed( MouseEvent e ) { if( SwingUtilities.isLeftMouseButton( e ) && isEnabled() ) { From 5b0c96cd6dc331aea48c01d255972486b99b26d6 Mon Sep 17 00:00:00 2001 From: Karl Tauber Date: Thu, 8 Oct 2020 23:46:43 +0200 Subject: [PATCH 06/10] TabbedPane: avoid scrolling selected tab back into visible area (after wheel scrolling) if the mouse is over a custom tab component that handles mouse events (e.g. a close button) --- .../formdev/flatlaf/ui/FlatTabbedPaneUI.java | 33 ++++++++------ .../flatlaf/testing/FlatContainerTest.java | 45 +++++++++++++++++++ .../flatlaf/testing/FlatContainerTest.jfd | 10 +++++ 3 files changed, 74 insertions(+), 14 deletions(-) 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 7740995b..051334cd 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 @@ -744,24 +744,14 @@ public class FlatTabbedPaneUI @Override public void mouseMoved( MouseEvent e ) { - lastMouseX = e.getX(); - lastMouseY = e.getY(); - - boolean wasInViewport = inViewport; - inViewport = isInViewport( e.getX(), e.getY() ); - - if( inViewport != wasInViewport ) { - if( !inViewport ) - viewportExited(); - else if( timer != null ) - timer.stop(); - } + checkViewportExited( e.getX(), e.getY() ); } @Override public void mouseExited( MouseEvent e ) { - inViewport = false; - viewportExited(); + // this event occurs also if mouse is moved to a custom tab component + // that handles mouse events (e.g. a close button) + checkViewportExited( e.getX(), e.getY() ); } @Override @@ -778,6 +768,21 @@ public class FlatTabbedPaneUI setRolloverTab( tabForCoordinate( tabPane, x, y ) ); } + protected void checkViewportExited( int x, int y ) { + lastMouseX = x; + lastMouseY = y; + + boolean wasInViewport = inViewport; + inViewport = isInViewport( x, y ); + + if( inViewport != wasInViewport ) { + if( !inViewport ) + viewportExited(); + else if( timer != null ) + timer.stop(); + } + } + protected void viewportExited() { if( !scrolled ) return; diff --git a/flatlaf-testing/src/main/java/com/formdev/flatlaf/testing/FlatContainerTest.java b/flatlaf-testing/src/main/java/com/formdev/flatlaf/testing/FlatContainerTest.java index 96a0b0d1..eed8036a 100644 --- a/flatlaf-testing/src/main/java/com/formdev/flatlaf/testing/FlatContainerTest.java +++ b/flatlaf-testing/src/main/java/com/formdev/flatlaf/testing/FlatContainerTest.java @@ -22,6 +22,8 @@ import static com.formdev.flatlaf.FlatClientProperties.TABBED_PANE_SHOW_TAB_SEPA import java.awt.*; import javax.swing.*; import javax.swing.border.*; +import com.formdev.flatlaf.FlatLaf; +import com.formdev.flatlaf.icons.FlatInternalFrameCloseIcon; import com.formdev.flatlaf.util.ScaledImageIcon; import com.jgoodies.forms.layout.*; import net.miginfocom.swing.*; @@ -47,6 +49,9 @@ public class FlatContainerTest addInitialTabs( tabbedPane1, tabbedPane2, tabbedPane3, tabbedPane4 ); initialTabCount = tabbedPane1.getTabCount(); + + tabScrollCheckBox.setSelected( true ); + tabScrollChanged(); } private void tabScrollChanged() { @@ -177,6 +182,39 @@ public class FlatContainerTest tabbedPane4.setBorder( border ); } + private void customTabsChanged() { + customTabsChanged( tabbedPane1 ); + customTabsChanged( tabbedPane2 ); + customTabsChanged( tabbedPane3 ); + customTabsChanged( tabbedPane4 ); + } + + private void customTabsChanged( JTabbedPane tabbedPane ) { + if( customTabsCheckBox.isSelected() ) { + tabbedPane.setTabComponentAt( 1, new JButton( tabbedPane1.getTitleAt( 1 ) ) ); + tabbedPane.setTabComponentAt( 3, createCustomTab( tabbedPane1.getTitleAt( 3 ) ) ); + } else { + tabbedPane.setTabComponentAt( 1, null ); + tabbedPane.setTabComponentAt( 3, null ); + } + } + + private Component createCustomTab( String tabTitle ) { + JButton closeButton; + if( UIManager.getLookAndFeel() instanceof FlatLaf ) { + closeButton = new JButton( new FlatInternalFrameCloseIcon() ); + closeButton.setContentAreaFilled( false ); + closeButton.setBorder( null ); + } else + closeButton = new JButton( "x" ); + + JPanel tab = new JPanel( new BorderLayout( 5, 0 ) ); + tab.setOpaque( false ); + tab.add( closeButton, BorderLayout.EAST ); + tab.add( new JLabel( tabTitle ), BorderLayout.CENTER ); + return tab; + } + private void initComponents() { // JFormDesigner - Component initialization - DO NOT MODIFY //GEN-BEGIN:initComponents JPanel panel9 = new JPanel(); @@ -204,6 +242,7 @@ public class FlatContainerTest tabIconsCheckBox = new JCheckBox(); tabIconSizeSpinner = new JSpinner(); customBorderCheckBox = new JCheckBox(); + customTabsCheckBox = new JCheckBox(); hasFullBorderCheckBox = new JCheckBox(); CellConstraints cc = new CellConstraints(); @@ -363,6 +402,11 @@ public class FlatContainerTest customBorderCheckBox.addActionListener(e -> customBorderChanged()); panel14.add(customBorderCheckBox, "cell 2 1"); + //---- customTabsCheckBox ---- + customTabsCheckBox.setText("Custom tabs"); + customTabsCheckBox.addActionListener(e -> customTabsChanged()); + panel14.add(customTabsCheckBox, "cell 3 1"); + //---- hasFullBorderCheckBox ---- hasFullBorderCheckBox.setText("Show content border"); hasFullBorderCheckBox.addActionListener(e -> hasFullBorderChanged()); @@ -387,6 +431,7 @@ public class FlatContainerTest private JCheckBox tabIconsCheckBox; private JSpinner tabIconSizeSpinner; private JCheckBox customBorderCheckBox; + private JCheckBox customTabsCheckBox; private JCheckBox hasFullBorderCheckBox; // JFormDesigner - End of variables declaration //GEN-END:variables diff --git a/flatlaf-testing/src/main/java/com/formdev/flatlaf/testing/FlatContainerTest.jfd b/flatlaf-testing/src/main/java/com/formdev/flatlaf/testing/FlatContainerTest.jfd index aa07cade..ce4551bc 100644 --- a/flatlaf-testing/src/main/java/com/formdev/flatlaf/testing/FlatContainerTest.jfd +++ b/flatlaf-testing/src/main/java/com/formdev/flatlaf/testing/FlatContainerTest.jfd @@ -231,6 +231,16 @@ new FormModel { }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { "value": "cell 2 1" } ) + add( new FormComponent( "javax.swing.JCheckBox" ) { + name: "customTabsCheckBox" + "text": "Custom tabs" + auxiliary() { + "JavaCodeGenerator.variableLocal": false + } + addEvent( new FormEvent( "java.awt.event.ActionListener", "actionPerformed", "customTabsChanged", false ) ) + }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { + "value": "cell 3 1" + } ) add( new FormComponent( "javax.swing.JCheckBox" ) { name: "hasFullBorderCheckBox" "text": "Show content border" From eddb9eee46b5838286c160988f4cc1116dac8ebd Mon Sep 17 00:00:00 2001 From: Karl Tauber Date: Fri, 9 Oct 2020 10:19:17 +0200 Subject: [PATCH 07/10] TabbedPane: make sure that tab stays hover highlighted when mouse is moved to custom tab component that handles mouse events (e.g. a close button) refactored PropertyChangeListener to class Handler --- .../formdev/flatlaf/ui/FlatTabbedPaneUI.java | 80 +++++++++++++------ 1 file changed, 57 insertions(+), 23 deletions(-) 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 051334cd..2911f304 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 @@ -130,6 +130,8 @@ public class FlatTabbedPaneUI protected JViewport tabViewport; protected FlatWheelTabScroller wheelTabScroller; + private Handler handler; + public static ComponentUI createUI( JComponent c ) { return new FlatTabbedPaneUI(); } @@ -220,6 +222,8 @@ public class FlatTabbedPaneUI protected void installListeners() { super.installListeners(); + tabPane.addMouseListener( getHandler() ); + if( tabViewport != null && (wheelTabScroller = createWheelTabScroller()) != null ) { // ideally we would add the mouse listeners to the viewport, but then the // mouse listener of the tabbed pane would not receive events while @@ -234,6 +238,11 @@ public class FlatTabbedPaneUI protected void uninstallListeners() { super.uninstallListeners(); + if( handler != null ) { + tabPane.removeMouseListener( handler ); + handler = null; + } + if( wheelTabScroller != null ) { wheelTabScroller.uninstall(); @@ -244,28 +253,21 @@ public class FlatTabbedPaneUI } } + private Handler getHandler() { + if( handler == null ) + handler = new Handler(); + return handler; + } + protected FlatWheelTabScroller createWheelTabScroller() { return new FlatWheelTabScroller(); } @Override protected PropertyChangeListener createPropertyChangeListener() { - return new BasicTabbedPaneUI.PropertyChangeHandler() { - @Override - public void propertyChange( PropertyChangeEvent e ) { - super.propertyChange( e ); - - switch( e.getPropertyName() ) { - case TABBED_PANE_SHOW_TAB_SEPARATORS: - case TABBED_PANE_SHOW_CONTENT_SEPARATOR: - case TABBED_PANE_HAS_FULL_BORDER: - case TABBED_PANE_TAB_HEIGHT: - tabPane.revalidate(); - tabPane.repaint(); - break; - } - } - }; + Handler handler = getHandler(); + handler.propertyChangeDelegate = super.createPropertyChangeListener(); + return handler; } @Override @@ -273,6 +275,10 @@ public class FlatTabbedPaneUI return new FlatScrollableTabButton( direction ); } + protected void setRolloverTab( int x, int y ) { + setRolloverTab( tabForCoordinate( tabPane, x, y ) ); + } + @Override protected void setRolloverTab( int index ) { int oldIndex = getRolloverTab(); @@ -701,7 +707,7 @@ public class FlatTabbedPaneUI // do not use animation if disabled if( !Animator.useAnimation() ) { tabViewport.setViewPosition( viewPosition ); - updateHover( lastMouseX, lastMouseY ); + setRolloverTab( lastMouseX, lastMouseY ); return; } @@ -730,7 +736,7 @@ public class FlatTabbedPaneUI startViewPosition = targetViewPosition = null; if( tabPane != null ) - updateHover( lastMouseX, lastMouseY ); + setRolloverTab( lastMouseX, lastMouseY ); } ); animator.setResolution( resolution ); @@ -757,17 +763,13 @@ public class FlatTabbedPaneUI @Override public void mousePressed( MouseEvent e ) { // for the case that the tab was only partly visible before the user clicked it - updateHover( e.getX(), e.getY() ); + setRolloverTab( e.getX(), e.getY() ); } protected boolean isInViewport( int x, int y ) { return (tabViewport != null && tabViewport.getBounds().contains( x, y ) ); } - protected void updateHover( int x, int y ) { - setRolloverTab( tabForCoordinate( tabPane, x, y ) ); - } - protected void checkViewportExited( int x, int y ) { lastMouseX = x; lastMouseY = y; @@ -811,4 +813,36 @@ public class FlatTabbedPaneUI } } } + + //---- class Handler ------------------------------------------------------ + + private class Handler + extends MouseAdapter + implements PropertyChangeListener + { + PropertyChangeListener propertyChangeDelegate; + + @Override + public void mouseExited( MouseEvent e ) { + // this event occurs also if mouse is moved to a custom tab component + // that handles mouse events (e.g. a close button) + // --> make sure that the tab stays highlighted + setRolloverTab( e.getX(), e.getY() ); + } + + @Override + public void propertyChange( PropertyChangeEvent e ) { + propertyChangeDelegate.propertyChange( e ); + + switch( e.getPropertyName() ) { + case TABBED_PANE_SHOW_TAB_SEPARATORS: + case TABBED_PANE_SHOW_CONTENT_SEPARATOR: + case TABBED_PANE_HAS_FULL_BORDER: + case TABBED_PANE_TAB_HEIGHT: + tabPane.revalidate(); + tabPane.repaint(); + break; + } + } + } } From 3de489f693d51eb4f5ee4f20e5c2573ac6c89169 Mon Sep 17 00:00:00 2001 From: Karl Tauber Date: Fri, 9 Oct 2020 16:27:52 +0200 Subject: [PATCH 08/10] TabbedPane: - fixed jittery animated scrolling tabs - support disabling animated scrolling with "ScrollPane.smoothScrolling=false" --- .../com/formdev/flatlaf/ui/FlatTabbedPaneUI.java | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) 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 2911f304..5f408f53 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 @@ -100,6 +100,7 @@ import com.formdev.flatlaf.util.UIScale; * @uiDefault TabbedPane.showTabSeparators boolean * @uiDefault TabbedPane.tabSeparatorsFullHeight boolean * @uiDefault TabbedPane.hasFullBorder boolean + * @uiDefault ScrollPane.smoothScrolling boolean * * @author Karl Tauber */ @@ -577,6 +578,16 @@ public class FlatTabbedPaneUI return tabPane.getTabLayoutPolicy() == JTabbedPane.SCROLL_TAB_LAYOUT; } + protected boolean isSmoothScrollingEnabled() { + if( !Animator.useAnimation() ) + return false; + + // 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" ); + } + //---- class FlatScrollableTabButton -------------------------------------- protected class FlatScrollableTabButton @@ -705,15 +716,14 @@ public class FlatTabbedPaneUI return; // do not use animation if disabled - if( !Animator.useAnimation() ) { + if( !isSmoothScrollingEnabled() ) { tabViewport.setViewPosition( viewPosition ); setRolloverTab( lastMouseX, lastMouseY ); return; } // remember start and target view positions - if( startViewPosition == null ) - startViewPosition = tabViewport.getViewPosition(); + startViewPosition = tabViewport.getViewPosition(); targetViewPosition = viewPosition; // create animator From a46bdef0792a17907c9472f7d1d7f5dc8ed63356 Mon Sep 17 00:00:00 2001 From: Karl Tauber Date: Sun, 11 Oct 2020 22:51:33 +0200 Subject: [PATCH 09/10] Animator: reuse timer instance (cherry picked from commit 0888fd8fb5d18c36886bf958ac5a5e44bf75618d) --- .../com/formdev/flatlaf/util/Animator.java | 21 ++++++++++--------- 1 file changed, 11 insertions(+), 10 deletions(-) diff --git a/flatlaf-core/src/main/java/com/formdev/flatlaf/util/Animator.java b/flatlaf-core/src/main/java/com/formdev/flatlaf/util/Animator.java index abf00b18..66e19040 100644 --- a/flatlaf-core/src/main/java/com/formdev/flatlaf/util/Animator.java +++ b/flatlaf-core/src/main/java/com/formdev/flatlaf/util/Animator.java @@ -184,14 +184,17 @@ public class Animator timeToStop = false; startTime = System.nanoTime() / 1000000; - timer = new Timer( resolution, e -> { - if( !hasBegun ) { - begin(); - hasBegun = true; - } + if( timer == null ) { + timer = new Timer( resolution, e -> { + if( !hasBegun ) { + begin(); + hasBegun = true; + } - timingEvent( getTimingFraction() ); - } ); + timingEvent( getTimingFraction() ); + } ); + } else + timer.setDelay( resolution ); timer.setInitialDelay( 0 ); timer.start(); } @@ -213,10 +216,8 @@ public class Animator } private void stop( boolean cancel ) { - if( timer != null ) { + if( timer != null ) timer.stop(); - timer = null; - } if( !cancel ) end(); From 3fc85cd7b20550f6ca415d649e5cd293378fbe49 Mon Sep 17 00:00:00 2001 From: Karl Tauber Date: Mon, 12 Oct 2020 00:33:23 +0200 Subject: [PATCH 10/10] TabbedPane: support precise scrolling tabs with trackpad (issue #40) --- .../formdev/flatlaf/ui/FlatTabbedPaneUI.java | 95 +++++++++++++++---- .../com/formdev/flatlaf/util/Animator.java | 12 +++ 2 files changed, 87 insertions(+), 20 deletions(-) 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 5f408f53..983d458f 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 @@ -132,6 +132,7 @@ public class FlatTabbedPaneUI protected FlatWheelTabScroller wheelTabScroller; private Handler handler; + private boolean blockRollover; public static ComponentUI createUI( JComponent c ) { return new FlatTabbedPaneUI(); @@ -282,6 +283,9 @@ public class FlatTabbedPaneUI @Override protected void setRolloverTab( int index ) { + if( blockRollover ) + return; + int oldIndex = getRolloverTab(); super.setRolloverTab( index ); @@ -659,19 +663,23 @@ public class FlatTabbedPaneUI protected class FlatWheelTabScroller extends MouseAdapter { + private int lastMouseX; + private int lastMouseY; + private boolean inViewport; private boolean scrolled; - private Timer timer; + private Timer rolloverTimer; + private Timer exitedTimer; private Animator animator; private Point startViewPosition; private Point targetViewPosition; - private int lastMouseX; - private int lastMouseY; protected void uninstall() { - if( timer != null ) - timer.stop(); + if( rolloverTimer != null ) + rolloverTimer.stop(); + if( exitedTimer != null ) + exitedTimer.stop(); if( animator != null ) animator.cancel(); } @@ -686,6 +694,8 @@ public class FlatTabbedPaneUI lastMouseX = e.getX(); lastMouseY = e.getY(); + double preciseWheelRotation = e.getPreciseWheelRotation(); + // compute new view position Point viewPosition = (targetViewPosition != null) ? targetViewPosition @@ -695,19 +705,34 @@ public class FlatTabbedPaneUI int y = viewPosition.y; int tabPlacement = tabPane.getTabPlacement(); if( tabPlacement == TOP || tabPlacement == BOTTOM ) { - x += maxTabHeight * e.getWheelRotation(); + x += maxTabHeight * preciseWheelRotation; x = Math.min( Math.max( x, 0 ), viewSize.width - tabViewport.getWidth() ); } else { - y += maxTabHeight * e.getWheelRotation(); + y += maxTabHeight * preciseWheelRotation; y = Math.min( Math.max( y, 0 ), viewSize.height - tabViewport.getHeight() ); } - // update view position + // check whether view position has changed Point newViewPosition = new Point( x, y ); - setViewPositionAnimated( newViewPosition ); + if( newViewPosition.equals( viewPosition ) ) + return; - if( !newViewPosition.equals( viewPosition )) - scrolled = true; + // update view position + if( preciseWheelRotation != 0 && + preciseWheelRotation != e.getWheelRotation() ) + { + // do not use animation for precise scrolling (e.g. with trackpad) + + // stop running animation (if any) + if( animator != null ) + animator.stop(); + + tabViewport.setViewPosition( newViewPosition ); + updateRolloverDelayed(); + } else + setViewPositionAnimated( newViewPosition ); + + scrolled = true; } protected void setViewPositionAnimated( Point viewPosition ) { @@ -718,7 +743,7 @@ public class FlatTabbedPaneUI // do not use animation if disabled if( !isSmoothScrollingEnabled() ) { tabViewport.setViewPosition( viewPosition ); - setRolloverTab( lastMouseX, lastMouseY ); + updateRolloverDelayed(); return; } @@ -754,8 +779,38 @@ public class FlatTabbedPaneUI } // restart animator - animator.cancel(); - animator.start(); + animator.restart(); + } + + protected void updateRolloverDelayed() { + blockRollover = true; + + // keep rollover on last tab until it would move to another tab, then clear it + int oldIndex = getRolloverTab(); + if( oldIndex >= 0 ) { + int index = tabForCoordinate( tabPane, lastMouseX, lastMouseY ); + if( index >= 0 && index != oldIndex ) { + // clear if moved to another tab + blockRollover = false; + setRolloverTab( -1 ); + blockRollover = true; + } + } + + // create timer + if( rolloverTimer == null ) { + rolloverTimer = new Timer( 150, e -> { + blockRollover = false; + + // highlight tab at mouse location + if( tabPane != null ) + setRolloverTab( lastMouseX, lastMouseY ); + } ); + rolloverTimer.setRepeats( false ); + } + + // restart timer + rolloverTimer.restart(); } @Override @@ -790,8 +845,8 @@ public class FlatTabbedPaneUI if( inViewport != wasInViewport ) { if( !inViewport ) viewportExited(); - else if( timer != null ) - timer.stop(); + else if( exitedTimer != null ) + exitedTimer.stop(); } } @@ -799,12 +854,12 @@ public class FlatTabbedPaneUI if( !scrolled ) return; - if( timer == null ) { - timer = new Timer( 500, e -> ensureSelectedTabVisible() ); - timer.setRepeats( false ); + if( exitedTimer == null ) { + exitedTimer = new Timer( 500, e -> ensureSelectedTabVisible() ); + exitedTimer.setRepeats( false ); } - timer.start(); + exitedTimer.start(); } protected void ensureSelectedTabVisible() { diff --git a/flatlaf-core/src/main/java/com/formdev/flatlaf/util/Animator.java b/flatlaf-core/src/main/java/com/formdev/flatlaf/util/Animator.java index 66e19040..ce980c98 100644 --- a/flatlaf-core/src/main/java/com/formdev/flatlaf/util/Animator.java +++ b/flatlaf-core/src/main/java/com/formdev/flatlaf/util/Animator.java @@ -216,6 +216,9 @@ public class Animator } private void stop( boolean cancel ) { + if( !running ) + return; + if( timer != null ) timer.stop(); @@ -226,6 +229,15 @@ public class Animator timeToStop = false; } + /** + * Restarts the animation. + * Invokes {@link #cancel()} and {@link #start()}. + */ + public void restart() { + cancel(); + start(); + } + /** * Returns whether this animation is running. */