diff --git a/CHANGELOG.md b/CHANGELOG.md index 39344c38..0a726d1b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,9 @@ FlatLaf Change Log #### New features and improvements +- 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) - Support painting separator line between window title and content (use UI value `TitlePane.borderColor`). (issue #184) 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 ed319855..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 @@ -20,17 +20,22 @@ 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.MouseListener; +import java.awt.event.MouseWheelEvent; import java.awt.geom.Path2D; import java.awt.geom.Rectangle2D; import java.beans.PropertyChangeEvent; @@ -40,13 +45,18 @@ 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.SwingUtilities; +import javax.swing.Timer; import javax.swing.UIManager; import javax.swing.plaf.ComponentUI; 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; /** @@ -90,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 */ @@ -117,6 +128,12 @@ public class FlatTabbedPaneUI protected boolean hasFullBorder; protected boolean tabsOverlapBorder; + protected JViewport tabViewport; + protected FlatWheelTabScroller wheelTabScroller; + + private Handler handler; + private boolean blockRollover; + public static ComponentUI createUI( JComponent c ) { return new FlatTabbedPaneUI(); } @@ -188,36 +205,87 @@ public class FlatTabbedPaneUI } @Override - protected PropertyChangeListener createPropertyChangeListener() { - return new BasicTabbedPaneUI.PropertyChangeHandler() { - @Override - public void propertyChange( PropertyChangeEvent e ) { - super.propertyChange( e ); + protected void installComponents() { + super.installComponents(); - 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; + // 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(); + + 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 + // the mouse pointer is over the viewport + tabPane.addMouseWheelListener( wheelTabScroller ); + tabPane.addMouseMotionListener( wheelTabScroller ); + tabPane.addMouseListener( wheelTabScroller ); + } + } + + @Override + protected void uninstallListeners() { + super.uninstallListeners(); + + if( handler != null ) { + tabPane.removeMouseListener( handler ); + handler = null; + } + + if( wheelTabScroller != null ) { + wheelTabScroller.uninstall(); + + tabPane.removeMouseWheelListener( wheelTabScroller ); + tabPane.removeMouseMotionListener( wheelTabScroller ); + tabPane.removeMouseListener( wheelTabScroller ); + wheelTabScroller = null; + } + } + + private Handler getHandler() { + if( handler == null ) + handler = new Handler(); + return handler; + } + + protected FlatWheelTabScroller createWheelTabScroller() { + return new FlatWheelTabScroller(); + } + + @Override + protected PropertyChangeListener createPropertyChangeListener() { + Handler handler = getHandler(); + handler.propertyChangeDelegate = super.createPropertyChangeListener(); + return handler; } @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 ); + } + + protected void setRolloverTab( int x, int y ) { + setRolloverTab( tabForCoordinate( tabPane, x, y ) ); } @Override protected void setRolloverTab( int index ) { + if( blockRollover ) + return; + int oldIndex = getRolloverTab(); super.setRolloverTab( index ); @@ -489,17 +557,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 +582,332 @@ 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; + protected boolean isSmoothScrollingEnabled() { + if( !Animator.useAnimation() ) + return false; - if( child instanceof Container ) { - Component c2 = findComponentByClassName( (Container) child, className ); - if( c2 != null ) - return c2; + // 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 + 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 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() ) { + 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 + extends MouseAdapter + { + private int lastMouseX; + private int lastMouseY; + + private boolean inViewport; + private boolean scrolled; + private Timer rolloverTimer; + private Timer exitedTimer; + + private Animator animator; + private Point startViewPosition; + private Point targetViewPosition; + + protected void uninstall() { + if( rolloverTimer != null ) + rolloverTimer.stop(); + if( exitedTimer != null ) + exitedTimer.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.getX(), e.getY() ) ) + return; + + lastMouseX = e.getX(); + lastMouseY = e.getY(); + + double preciseWheelRotation = e.getPreciseWheelRotation(); + + // compute new view position + Point viewPosition = (targetViewPosition != null) + ? targetViewPosition + : 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 * preciseWheelRotation; + x = Math.min( Math.max( x, 0 ), viewSize.width - tabViewport.getWidth() ); + } else { + y += maxTabHeight * preciseWheelRotation; + y = Math.min( Math.max( y, 0 ), viewSize.height - tabViewport.getHeight() ); + } + + // check whether view position has changed + Point newViewPosition = new Point( x, y ); + if( newViewPosition.equals( viewPosition ) ) + return; + + // 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 ) { + // check whether position is equal to current position + if( viewPosition.equals( tabViewport.getViewPosition() ) ) + return; + + // do not use animation if disabled + if( !isSmoothScrollingEnabled() ) { + tabViewport.setViewPosition( viewPosition ); + updateRolloverDelayed(); + return; + } + + // remember start and target view positions + 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 ) + setRolloverTab( lastMouseX, lastMouseY ); + } ); + + animator.setResolution( resolution ); + animator.setInterpolator( new CubicBezierEasing( 0.5f, 0.5f, 0.5f, 1 ) ); + } + + // restart animator + 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 + public void mouseMoved( MouseEvent e ) { + checkViewportExited( e.getX(), e.getY() ); + } + + @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) + checkViewportExited( e.getX(), e.getY() ); + } + + @Override + public void mousePressed( MouseEvent e ) { + // for the case that the tab was only partly visible before the user clicked it + setRolloverTab( e.getX(), e.getY() ); + } + + protected boolean isInViewport( int x, int y ) { + return (tabViewport != null && tabViewport.getBounds().contains( 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( exitedTimer != null ) + exitedTimer.stop(); + } + } + + protected void viewportExited() { + if( !scrolled ) + return; + + if( exitedTimer == null ) { + exitedTimer = new Timer( 500, e -> ensureSelectedTabVisible() ); + exitedTimer.setRepeats( false ); + } + + exitedTimer.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 ); + } + } + } + + //---- 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; } } - return null; } } 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..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 @@ -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 @@ -174,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(); } @@ -203,10 +216,11 @@ public class Animator } private void stop( boolean cancel ) { - if( timer != null ) { + if( !running ) + return; + + if( timer != null ) timer.stop(); - timer = null; - } if( !cancel ) end(); @@ -215,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. */ 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..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.*; @@ -32,6 +34,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 +48,10 @@ public class FlatContainerTest initComponents(); addInitialTabs( tabbedPane1, tabbedPane2, tabbedPane3, tabbedPane4 ); + initialTabCount = tabbedPane1.getTabCount(); + + tabScrollCheckBox.setSelected( true ); + tabScrollChanged(); } private void tabScrollChanged() { @@ -63,8 +72,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 +95,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 +171,50 @@ 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 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(); @@ -182,12 +235,15 @@ 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(); + customTabsCheckBox = new JCheckBox(); + hasFullBorderCheckBox = new JCheckBox(); CellConstraints cc = new CellConstraints(); //======== this ======== @@ -294,14 +350,13 @@ public class FlatContainerTest "insets 0,hidemode 3", // columns "[]" + - "[]" + - "[]" + "[fill]" + "[]" + - "[fill]" + + "[]" + "[fill]", // rows - "[center]")); + "[center]" + + "[]")); //---- moreTabsCheckBox ---- moreTabsCheckBox.setText("More tabs"); @@ -309,37 +364,53 @@ 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"); + + //---- 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()); + panel14.add(hasFullBorderCheckBox, "cell 4 1,alignx left,growx 0"); } panel9.add(panel14, cc.xywh(1, 11, 3, 1)); } @@ -353,12 +424,15 @@ 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 customTabsCheckBox; + 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..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 @@ -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,37 @@ 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: "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" + 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