From c58f5a6ca7c4b5dc5aacd5e25ad6cc6d38c891ef Mon Sep 17 00:00:00 2001 From: Karl Tauber Date: Thu, 15 Oct 2020 00:10:07 +0200 Subject: [PATCH 1/7] TabbedPane: replaced forward/backward scrolling arrow buttons with "Show Hidden Tabs" button (issue #40) --- CHANGELOG.md | 5 + .../formdev/flatlaf/ui/FlatTabbedPaneUI.java | 502 +++++++++++++++++- .../com/formdev/flatlaf/FlatLaf.properties | 3 +- .../flatlaf/resources/Bundle.properties | 5 + .../flatlaf/resources/Bundle_de.properties | 5 + .../uidefaults/FlatDarkLaf_1.8.0_202.txt | 3 +- .../uidefaults/FlatLightLaf_1.8.0_202.txt | 3 +- .../uidefaults/FlatTestLaf_1.8.0_202.txt | 3 +- .../flatlaf/testing/FlatContainerTest.java | 47 ++ .../flatlaf/testing/FlatContainerTest.jfd | 38 +- .../flatlaf/themeeditor/FlatLafUIKeys.txt | 1 + 11 files changed, 596 insertions(+), 19 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0a726d1b..56a767de 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,11 @@ FlatLaf Change Log #### New features and improvements +- TabbedPane: Replaced forward/backward scrolling arrow buttons with "Show + Hidden Tabs" button. If pressed, it shows a popup menu that contains (partly) + hidden tabs and selecting one activates that tab. If you prefer + forward/backward buttons, use `UIManager.put( + "TabbedPane.hiddenTabsNavigation", "arrowButtons" )`. (issue #40) - 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) 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 983d458f..20f7579b 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,16 +20,23 @@ 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.EventQueue; 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.LayoutManager; import java.awt.Point; import java.awt.Rectangle; import java.awt.Shape; +import java.awt.event.ActionEvent; +import java.awt.event.ActionListener; +import java.awt.event.ComponentEvent; +import java.awt.event.ComponentListener; import java.awt.event.InputEvent; import java.awt.event.KeyEvent; import java.awt.event.MouseAdapter; @@ -41,15 +48,22 @@ import java.awt.geom.Rectangle2D; import java.beans.PropertyChangeEvent; import java.beans.PropertyChangeListener; import java.util.Collections; +import java.util.Locale; import java.util.Set; import javax.swing.JButton; import javax.swing.JComponent; +import javax.swing.JMenuItem; +import javax.swing.JPopupMenu; 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.event.ChangeEvent; +import javax.swing.event.ChangeListener; +import javax.swing.event.PopupMenuEvent; +import javax.swing.event.PopupMenuListener; import javax.swing.plaf.ComponentUI; import javax.swing.plaf.UIResource; import javax.swing.plaf.basic.BasicTabbedPaneUI; @@ -100,6 +114,7 @@ import com.formdev.flatlaf.util.UIScale; * @uiDefault TabbedPane.showTabSeparators boolean * @uiDefault TabbedPane.tabSeparatorsFullHeight boolean * @uiDefault TabbedPane.hasFullBorder boolean + * @uiDefault TabbedPane.hiddenTabsNavigation String moreTabsButton (default) or arrowButtons * @uiDefault ScrollPane.smoothScrolling boolean * * @author Karl Tauber @@ -107,6 +122,10 @@ import com.formdev.flatlaf.util.UIScale; public class FlatTabbedPaneUI extends BasicTabbedPaneUI { + // hidden tabs navigation types + protected static final int MORE_TABS_BUTTON = 0; + protected static final int ARROW_BUTTONS = 1; + private static Set focusForwardTraversalKeys; private static Set focusBackwardTraversalKeys; @@ -128,9 +147,15 @@ public class FlatTabbedPaneUI protected boolean hasFullBorder; protected boolean tabsOverlapBorder; + protected int hiddenTabsNavigation = MORE_TABS_BUTTON; + + protected String moreTabsButtonToolTipText; + protected JViewport tabViewport; protected FlatWheelTabScroller wheelTabScroller; + private JButton moreTabsButton; + private Handler handler; private boolean blockRollover; @@ -140,7 +165,28 @@ public class FlatTabbedPaneUI @Override protected void installDefaults() { - super.installDefaults(); + if( UIManager.getBoolean( "TabbedPane.tabsOverlapBorder" ) ) { + // Force BasicTabbedPaneUI.tabsOverlapBorder to false, + // which is necessary for "more tabs" button to work correctly. + // + // If it would be true, class TabbedPaneScrollLayout would invoke TabbedPaneLayout.padSelectedTab(), + // which would modify rectangle of selected tab in a wrong way (for wrap tab layout policy). + // This would cause tab painting issues when scrolled and + // missing "more tabs" button if last tab is selected. + // + // All methods of BasicTabbedPaneUI that use tabsOverlapBorder (except + // the one method mentioned above) are overridden. + // + // This is normally not invoked because the default value for + // TabbedPane.tabsOverlapBorder is false in all FlatLaf themes. + // Anyway, 3rd party themes may have changed it. + // So make sure that it works anyway to avoid issues. + Object oldValue = UIManager.put( "TabbedPane.tabsOverlapBorder", false ); + super.installDefaults(); + UIManager.put( "TabbedPane.tabsOverlapBorder", oldValue ); + } else + super.installDefaults(); + disabledForeground = UIManager.getColor( "TabbedPane.disabledForeground" ); selectedBackground = UIManager.getColor( "TabbedPane.selectedBackground" ); @@ -160,6 +206,9 @@ public class FlatTabbedPaneUI hasFullBorder = UIManager.getBoolean( "TabbedPane.hasFullBorder" ); tabsOverlapBorder = UIManager.getBoolean( "TabbedPane.tabsOverlapBorder" ); + Locale l = tabPane.getLocale(); + moreTabsButtonToolTipText = UIManager.getString( "TabbedPane.moreTabsButtonToolTipText", l ); + // scale textIconGap = scale( textIconGap ); tabInsets = scale( tabInsets ); @@ -218,6 +267,47 @@ public class FlatTabbedPaneUI } } } + + // initialize here because used in installHiddenTabsNavigation() before installDefaults() was invoked + hiddenTabsNavigation = parseHiddenTabsNavigation( UIManager.getString( "TabbedPane.hiddenTabsNavigation" ) ); + + installHiddenTabsNavigation(); + } + + private void installHiddenTabsNavigation() { + if( hiddenTabsNavigation != MORE_TABS_BUTTON || + !isScrollTabLayout() || + tabViewport == null ) + return; + + // At this point, BasicTabbedPaneUI already has installed + // TabbedPaneScrollLayout (in super.createLayoutManager()) and + // ScrollableTabSupport, ScrollableTabViewport, ScrollableTabPanel, etc + // (in super.installComponents()). + + // install own layout manager that delegates to original layout manager + tabPane.setLayout( createScrollLayoutManager( (TabbedPaneLayout) tabPane.getLayout() ) ); + + // create and add "more tabs" button + moreTabsButton = createMoreTabsButton(); + tabPane.add( moreTabsButton ); + } + + @Override + protected void uninstallComponents() { + // restore layout manager before invoking super.uninstallComponents() for + // correct uninstallation of BasicTabbedPaneUI tab scroller support + if( tabPane.getLayout() instanceof FlatTabbedPaneScrollLayout ) + tabPane.setLayout( ((FlatTabbedPaneScrollLayout)tabPane.getLayout()).delegate ); + + super.uninstallComponents(); + + if( moreTabsButton != null ) { + tabPane.remove( moreTabsButton ); + moreTabsButton = null; + } + + tabViewport = null; } @Override @@ -225,6 +315,8 @@ public class FlatTabbedPaneUI super.installListeners(); tabPane.addMouseListener( getHandler() ); + tabPane.addMouseMotionListener( getHandler() ); + tabPane.addComponentListener( getHandler() ); if( tabViewport != null && (wheelTabScroller = createWheelTabScroller()) != null ) { // ideally we would add the mouse listeners to the viewport, but then the @@ -242,6 +334,8 @@ public class FlatTabbedPaneUI if( handler != null ) { tabPane.removeMouseListener( handler ); + tabPane.removeMouseMotionListener( handler ); + tabPane.removeComponentListener( handler ); handler = null; } @@ -272,6 +366,21 @@ public class FlatTabbedPaneUI return handler; } + @Override + protected ChangeListener createChangeListener() { + Handler handler = getHandler(); + handler.changeDelegate = super.createChangeListener(); + return handler; + } + + protected LayoutManager createScrollLayoutManager( TabbedPaneLayout delegate ) { + return new FlatTabbedPaneScrollLayout( delegate ); + } + + protected JButton createMoreTabsButton() { + return new FlatMoreTabsButton(); + } + @Override protected JButton createScrollButton( int direction ) { return new FlatScrollableTabButton( direction ); @@ -353,6 +462,19 @@ public class FlatTabbedPaneUI super.update( g, c ); } + @Override + public void paint( Graphics g, JComponent c ) { + ensureCurrentLayout(); + + int tabPlacement = tabPane.getTabPlacement(); + int selectedIndex = tabPane.getSelectedIndex(); + + paintContentBorder( g, tabPlacement, selectedIndex ); + + if( !isScrollTabLayout() ) + paintTabArea( g, tabPlacement, selectedIndex ); + } + @Override protected void paintText( Graphics g, int tabPlacement, Font font, FontMetrics metrics, int tabIndex, String title, Rectangle textRect, boolean isSelected ) @@ -573,6 +695,43 @@ public class FlatTabbedPaneUI { } + @Override + public int tabForCoordinate( JTabbedPane pane, int x, int y ) { + if( moreTabsButton != null ) { + // convert x,y from JTabbedPane coordinate space to ScrollableTabPanel coordinate space + Point viewPosition = tabViewport.getViewPosition(); + x = x - tabViewport.getX() + viewPosition.x; + y = y - tabViewport.getY() + viewPosition.y; + + // check whether point is within viewport + if( !tabViewport.getViewRect().contains( x, y ) ) + return -1; + } + + return super.tabForCoordinate( pane, x, y ); + } + + @Override + protected Rectangle getTabBounds( int tabIndex, Rectangle dest ) { + if( moreTabsButton != null ) { + // copy tab bounds to dest + dest.setBounds( rects[tabIndex] ); + + // convert tab bounds to coordinate space of JTabbedPane + Point viewPosition = tabViewport.getViewPosition(); + dest.x = dest.x + tabViewport.getX() - viewPosition.x; + dest.y = dest.y + tabViewport.getY() - viewPosition.y; + return dest; + } else + return super.getTabBounds( tabIndex, dest ); + } + + protected void ensureCurrentLayout() { + // since super.ensureCurrentLayout() is private, + // use super.getTabRunCount() as workaround + super.getTabRunCount( tabPane ); + } + private boolean isLastInRun( int tabIndex ) { int run = getRunForTab( tabPane.getTabCount(), tabIndex ); return lastTabInRun( tabPane.getTabCount(), run ) == tabIndex; @@ -592,6 +751,170 @@ public class FlatTabbedPaneUI return UIManager.getBoolean( "ScrollPane.smoothScrolling" ); } + protected static int parseHiddenTabsNavigation( String str ) { + if( str == null ) + return MORE_TABS_BUTTON; + + switch( str ) { + default: + case "moreTabsButton": return MORE_TABS_BUTTON; + case "arrowButtons": return ARROW_BUTTONS; + } + } + + private void runWithOriginalLayoutManager( Runnable runnable ) { + LayoutManager layout = tabPane.getLayout(); + if( layout instanceof FlatTabbedPaneScrollLayout ) { + // temporary change layout manager because the runnable may use + // BasicTabbedPaneUI.scrollableTabLayoutEnabled() + tabPane.setLayout( ((FlatTabbedPaneScrollLayout)layout).delegate ); + runnable.run(); + tabPane.setLayout( layout ); + } else + runnable.run(); + } + + protected void ensureSelectedTabIsVisible() { + if( tabPane == null || tabViewport == null ) + return; + + int selectedIndex = tabPane.getSelectedIndex(); + if( selectedIndex < 0 ) + return; + + Rectangle tabBounds = getTabBounds( tabPane, selectedIndex ); + tabViewport.scrollRectToVisible( tabBounds ); + } + + //---- class FlatMoreTabsButton ------------------------------------------- + + protected class FlatMoreTabsButton + extends FlatArrowButton + implements ActionListener, PopupMenuListener + { + private boolean popupVisible; + + public FlatMoreTabsButton() { + // this method is invoked before installDefaults(), so we can not use color fields here + super( SOUTH, UIManager.getString( "Component.arrowType" ), + UIManager.getColor( "TabbedPane.foreground" ), + UIManager.getColor( "TabbedPane.disabledForeground" ), null, + UIManager.getColor( "TabbedPane.hoverColor" ) ); + + updateDirection(); + setToolTipText( moreTabsButtonToolTipText ); + addActionListener( this ); + } + + protected void updateDirection() { + int direction; + switch( tabPane.getTabPlacement() ) { + default: + case TOP: direction = SOUTH; break; + case BOTTOM: direction = NORTH; break; + case LEFT: direction = EAST; break; + case RIGHT: direction = WEST; break; + + } + setDirection( direction ); + } + + @Override + public void paint( Graphics g ) { + // paint arrow button near separator line + if( direction == EAST || direction == WEST ) { + int xoffset = (getWidth() / 2) - getHeight(); + setXOffset( (direction == EAST) ? xoffset : -xoffset ); + } + + super.paint( g ); + } + + @Override + protected boolean isHover() { + return super.isHover() || popupVisible; + } + + @Override + public void actionPerformed( ActionEvent e ) { + if( tabViewport == null ) + return; + + // detect (partly) hidden tabs and build popup menu + JPopupMenu popupMenu = new JPopupMenu(); + popupMenu.addPopupMenuListener( this ); + Rectangle viewRect = tabViewport.getViewRect(); + int lastIndex = -1; + for( int i = 0; i < rects.length; i++ ) { + if( !viewRect.contains( rects[i] ) ) { + // add separator between leading and trailing tabs + if( lastIndex >= 0 && lastIndex + 1 != i ) + popupMenu.addSeparator(); + lastIndex = i; + + // create menu item for tab + popupMenu.add( createMenuItem( i ) ); + } + } + + // show popup menu + int buttonWidth = getWidth(); + int buttonHeight = getHeight(); + int x = 0; + int y = 0; + Dimension popupSize = popupMenu.getPreferredSize(); + switch( tabPane.getTabPlacement() ) { + default: + case TOP: x = buttonWidth - popupSize.width; y = buttonHeight; break; + case BOTTOM: x = buttonWidth - popupSize.width; y = -popupSize.height; break; + case LEFT: x = buttonWidth; y = buttonHeight - popupSize.height; break; + case RIGHT: x = -popupSize.width; y = buttonHeight - popupSize.height; break; + + } + popupMenu.show( this, x, y ); + } + + protected JMenuItem createMenuItem( int index ) { + JMenuItem menuItem = new JMenuItem( tabPane.getTitleAt( index ), tabPane.getIconAt( index ) ); + menuItem.setDisabledIcon( tabPane.getDisabledIconAt( index ) ); + menuItem.setToolTipText( tabPane.getToolTipTextAt( index ) ); + + Color foregroundAt = tabPane.getForegroundAt( index ); + if( foregroundAt != tabPane.getForeground() ) + menuItem.setForeground( foregroundAt ); + + Color backgroundAt = tabPane.getBackgroundAt( index ); + if( backgroundAt != tabPane.getBackground() ) { + menuItem.setBackground( backgroundAt ); + menuItem.setOpaque( true ); + } + + if( !tabPane.isEnabledAt( index ) ) + menuItem.setEnabled( false ); + + menuItem.addActionListener( e -> tabPane.setSelectedIndex( index ) ); + return menuItem; + } + + @Override + public void popupMenuWillBecomeVisible( PopupMenuEvent e ) { + popupVisible = true; + repaint(); + } + + @Override + public void popupMenuWillBecomeInvisible( PopupMenuEvent e ) { + popupVisible = false; + repaint(); + } + + @Override + public void popupMenuCanceled( PopupMenuEvent e ) { + popupVisible = false; + repaint(); + } + } + //---- class FlatScrollableTabButton -------------------------------------- protected class FlatScrollableTabButton @@ -612,11 +935,21 @@ public class FlatTabbedPaneUI @Override public Dimension getPreferredSize() { + // Use half width/height if "more tabs" button is used, because size of + // "more tabs" button is the union of the backward and forward scroll buttons. + // With this "trick", viewport gets correct size. + boolean halfSize = (hiddenTabsNavigation == MORE_TABS_BUTTON); + 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 ); + if( direction == WEST || direction == EAST ) { + return new Dimension( + halfSize ? ((size.width / 2) + scale( 4 )) : size.width, + Math.max( size.height, maxTabHeight ) ); + } else { + return new Dimension( + Math.max( size.width, maxTabWidth ), + halfSize ? ((size.height / 2) + scale( 4 )) : size.height ); + } } @Override @@ -871,11 +1204,8 @@ public class FlatTabbedPaneUI return; scrolled = false; - int selectedIndex = tabPane.getSelectedIndex(); - if( selectedIndex >= 0 ) { - Rectangle tabBounds = getTabBounds( tabPane, selectedIndex ); - tabViewport.scrollRectToVisible( tabBounds ); - } + // scroll selected tab into visible area + ensureSelectedTabIsVisible(); } } @@ -883,9 +1213,18 @@ public class FlatTabbedPaneUI private class Handler extends MouseAdapter - implements PropertyChangeListener + implements PropertyChangeListener, ChangeListener, ComponentListener { PropertyChangeListener propertyChangeDelegate; + ChangeListener changeDelegate; + + //---- interface MouseListener ---- + + @Override + public void mouseEntered( MouseEvent e ) { + // this is necessary for "more tabs" button + setRolloverTab( e.getX(), e.getY() ); + } @Override public void mouseExited( MouseEvent e ) { @@ -895,11 +1234,41 @@ public class FlatTabbedPaneUI setRolloverTab( e.getX(), e.getY() ); } + //---- interface MouseMotionListener ---- + + @Override + public void mouseMoved( MouseEvent e ) { + // this is necessary for "more tabs" button + setRolloverTab( e.getX(), e.getY() ); + } + + //---- interface PropertyChangeListener ---- + @Override public void propertyChange( PropertyChangeEvent e ) { - propertyChangeDelegate.propertyChange( e ); - + // invoke delegate listener switch( e.getPropertyName() ) { + case "tabPlacement": + case "opaque": + case "background": + case "indexForTabComponent": + runWithOriginalLayoutManager( () -> { + propertyChangeDelegate.propertyChange( e ); + } ); + break; + + default: + propertyChangeDelegate.propertyChange( e ); + break; + } + + // handle event + switch( e.getPropertyName() ) { + case "tabPlacement": + if( moreTabsButton instanceof FlatMoreTabsButton ) + ((FlatMoreTabsButton)moreTabsButton).updateDirection(); + break; + case TABBED_PANE_SHOW_TAB_SEPARATORS: case TABBED_PANE_SHOW_CONTENT_SEPARATOR: case TABBED_PANE_HAS_FULL_BORDER: @@ -909,5 +1278,112 @@ public class FlatTabbedPaneUI break; } } + + //---- interface ChangeListener ---- + + @Override + public void stateChanged( ChangeEvent e ) { + changeDelegate.stateChanged( e ); + + // scroll selected tab into visible area + if( moreTabsButton != null ) + ensureSelectedTabIsVisible(); + } + + //---- interface ComponentListener ---- + + @Override + public void componentResized( ComponentEvent e ) { + // make sure that selected tab stays visible when component size changed + EventQueue.invokeLater( () -> { + ensureSelectedTabIsVisible(); + } ); + } + + @Override public void componentMoved( ComponentEvent e ) {} + @Override public void componentShown( ComponentEvent e ) {} + @Override public void componentHidden( ComponentEvent e ) {} + } + + //---- class FlatTabbedPaneScrollLayout ----------------------------------- + + /** + * Layout manager used if "TabbedPane.hiddenTabsNavigation" is "moreTabsButton". + *

+ * Although this class delegates all methods to the original layout manager + * {@link BasicTabbedPaneUI.TabbedPaneScrollLayout}, which extends + * {@link BasicTabbedPaneUI.TabbedPaneLayout}, it is necessary that this class + * also extends {@link TabbedPaneLayout} to avoid a {@code ClassCastException} + * in {@link BasicTabbedPaneUI}.ensureCurrentLayout(). + */ + protected class FlatTabbedPaneScrollLayout + extends TabbedPaneLayout + implements LayoutManager + { + private final TabbedPaneLayout delegate; + + protected FlatTabbedPaneScrollLayout( TabbedPaneLayout delegate ) { + this.delegate = delegate; + } + + @Override + public void calculateLayoutInfo() { + delegate.calculateLayoutInfo(); + } + + //---- interface LayoutManager ---- + + @Override + public void addLayoutComponent( String name, Component comp ) { + delegate.addLayoutComponent( name, comp ); + } + + @Override + public void removeLayoutComponent( Component comp ) { + delegate.removeLayoutComponent( comp ); + } + + @Override + public Dimension preferredLayoutSize( Container parent ) { + return delegate.preferredLayoutSize( parent ); + } + + @Override + public Dimension minimumLayoutSize( Container parent ) { + return delegate.minimumLayoutSize( parent ); + } + + @Override + public void layoutContainer( Container parent ) { + // delegate to original layout manager and let it layout tabs and buttons + // + // runWithOriginalLayoutManager() is necessary for correct locations + // of tab components layed out in TabbedPaneLayout.layoutTabComponents() + runWithOriginalLayoutManager( () -> { + delegate.layoutContainer( parent ); + } ); + + // check whether scroll buttons are visible, which is changed by original + // layout manager depending on whether there is enough room for all tabs + boolean scrollButtonsVisible = false; + Rectangle buttonsBounds = null; + for( Component c : tabPane.getComponents() ) { + if( c instanceof FlatScrollableTabButton && c.isVisible() ) { + scrollButtonsVisible = true; + + // compute union bounds of all scroll buttons + Rectangle r = c.getBounds(); + buttonsBounds = (buttonsBounds != null) ? buttonsBounds.union( r ) : r; + + // hide scroll button + c.setVisible( false ); + } + } + + // show/hide "more tabs" button and layout it + moreTabsButton.setVisible( scrollButtonsVisible ); + if( buttonsBounds != null ) + moreTabsButton.setBounds( buttonsBounds ); + } } } diff --git a/flatlaf-core/src/main/resources/com/formdev/flatlaf/FlatLaf.properties b/flatlaf-core/src/main/resources/com/formdev/flatlaf/FlatLaf.properties index 6a9583c8..8559cc12 100644 --- a/flatlaf-core/src/main/resources/com/formdev/flatlaf/FlatLaf.properties +++ b/flatlaf-core/src/main/resources/com/formdev/flatlaf/FlatLaf.properties @@ -546,10 +546,11 @@ TabbedPane.tabInsets=4,12,4,12 TabbedPane.tabAreaInsets=0,0,0,0 TabbedPane.selectedTabPadInsets=0,0,0,0 TabbedPane.tabRunOverlay=0 -TabbedPane.tabsOverlapBorder=true +TabbedPane.tabsOverlapBorder=false TabbedPane.disabledForeground=@disabledText TabbedPane.shadow=@background TabbedPane.contentBorderInsets=null +TabbedPane.hiddenTabsNavigation=moreTabsButton #---- Table ---- diff --git a/flatlaf-core/src/main/resources/com/formdev/flatlaf/resources/Bundle.properties b/flatlaf-core/src/main/resources/com/formdev/flatlaf/resources/Bundle.properties index 9ee3fb8f..fa1d20fc 100644 --- a/flatlaf-core/src/main/resources/com/formdev/flatlaf/resources/Bundle.properties +++ b/flatlaf-core/src/main/resources/com/formdev/flatlaf/resources/Bundle.properties @@ -46,3 +46,8 @@ FileChooser.refreshActionLabelText=Refresh FileChooser.newFolderActionLabelText=New Folder FileChooser.listViewActionLabelText=List FileChooser.detailsViewActionLabelText=Details + + +#---- TabbedPane ---- + +TabbedPane.moreTabsButtonToolTipText=Show Hidden Tabs diff --git a/flatlaf-core/src/main/resources/com/formdev/flatlaf/resources/Bundle_de.properties b/flatlaf-core/src/main/resources/com/formdev/flatlaf/resources/Bundle_de.properties index f9034e76..5f30c8dd 100644 --- a/flatlaf-core/src/main/resources/com/formdev/flatlaf/resources/Bundle_de.properties +++ b/flatlaf-core/src/main/resources/com/formdev/flatlaf/resources/Bundle_de.properties @@ -46,3 +46,8 @@ FileChooser.refreshActionLabelText=Aktualisieren FileChooser.newFolderActionLabelText=Neuer Ordner FileChooser.listViewActionLabelText=Liste FileChooser.detailsViewActionLabelText=Details + + +#---- TabbedPane ---- + +TabbedPane.moreTabsButtonToolTipText=Verdeckte Tabs anzeigen diff --git a/flatlaf-testing/dumps/uidefaults/FlatDarkLaf_1.8.0_202.txt b/flatlaf-testing/dumps/uidefaults/FlatDarkLaf_1.8.0_202.txt index 18f3bfc9..cd66c85b 100644 --- a/flatlaf-testing/dumps/uidefaults/FlatDarkLaf_1.8.0_202.txt +++ b/flatlaf-testing/dumps/uidefaults/FlatDarkLaf_1.8.0_202.txt @@ -916,6 +916,7 @@ TabbedPane.focusColor #3d4b5c javax.swing.plaf.ColorUIResource [UI] TabbedPane.font [active] $defaultFont [UI] TabbedPane.foreground #bbbbbb javax.swing.plaf.ColorUIResource [UI] TabbedPane.hasFullBorder false +TabbedPane.hiddenTabsNavigation moreTabsButton TabbedPane.highlight #242424 javax.swing.plaf.ColorUIResource [UI] TabbedPane.hoverColor #2e3133 javax.swing.plaf.ColorUIResource [UI] TabbedPane.labelShift 1 @@ -932,7 +933,7 @@ TabbedPane.tabRunOverlay 0 TabbedPane.tabSelectionHeight 3 TabbedPane.tabSeparatorsFullHeight false TabbedPane.tabsOpaque true -TabbedPane.tabsOverlapBorder true +TabbedPane.tabsOverlapBorder false TabbedPane.textIconGap 4 TabbedPane.underlineColor #4a88c7 javax.swing.plaf.ColorUIResource [UI] TabbedPaneUI com.formdev.flatlaf.ui.FlatTabbedPaneUI diff --git a/flatlaf-testing/dumps/uidefaults/FlatLightLaf_1.8.0_202.txt b/flatlaf-testing/dumps/uidefaults/FlatLightLaf_1.8.0_202.txt index c749f999..e179a14e 100644 --- a/flatlaf-testing/dumps/uidefaults/FlatLightLaf_1.8.0_202.txt +++ b/flatlaf-testing/dumps/uidefaults/FlatLightLaf_1.8.0_202.txt @@ -921,6 +921,7 @@ TabbedPane.focusColor #dae4ed javax.swing.plaf.ColorUIResource [UI] TabbedPane.font [active] $defaultFont [UI] TabbedPane.foreground #000000 javax.swing.plaf.ColorUIResource [UI] TabbedPane.hasFullBorder false +TabbedPane.hiddenTabsNavigation moreTabsButton TabbedPane.highlight #ffffff javax.swing.plaf.ColorUIResource [UI] TabbedPane.hoverColor #d9d9d9 javax.swing.plaf.ColorUIResource [UI] TabbedPane.labelShift 1 @@ -937,7 +938,7 @@ TabbedPane.tabRunOverlay 0 TabbedPane.tabSelectionHeight 3 TabbedPane.tabSeparatorsFullHeight false TabbedPane.tabsOpaque true -TabbedPane.tabsOverlapBorder true +TabbedPane.tabsOverlapBorder false TabbedPane.textIconGap 4 TabbedPane.underlineColor #4083c9 javax.swing.plaf.ColorUIResource [UI] TabbedPaneUI com.formdev.flatlaf.ui.FlatTabbedPaneUI diff --git a/flatlaf-testing/dumps/uidefaults/FlatTestLaf_1.8.0_202.txt b/flatlaf-testing/dumps/uidefaults/FlatTestLaf_1.8.0_202.txt index 86f70157..d49664a1 100644 --- a/flatlaf-testing/dumps/uidefaults/FlatTestLaf_1.8.0_202.txt +++ b/flatlaf-testing/dumps/uidefaults/FlatTestLaf_1.8.0_202.txt @@ -909,6 +909,7 @@ TabbedPane.focusColor #dddddd javax.swing.plaf.ColorUIResource [UI] TabbedPane.font [active] $defaultFont [UI] TabbedPane.foreground #ff0000 javax.swing.plaf.ColorUIResource [UI] TabbedPane.hasFullBorder false +TabbedPane.hiddenTabsNavigation moreTabsButton TabbedPane.highlight #ffffff javax.swing.plaf.ColorUIResource [UI] TabbedPane.hoverColor #eeeeee javax.swing.plaf.ColorUIResource [UI] TabbedPane.labelShift 1 @@ -928,7 +929,7 @@ TabbedPane.tabSelectionHeight 3 TabbedPane.tabSeparatorColor #0000ff javax.swing.plaf.ColorUIResource [UI] TabbedPane.tabSeparatorsFullHeight false TabbedPane.tabsOpaque true -TabbedPane.tabsOverlapBorder true +TabbedPane.tabsOverlapBorder false TabbedPane.textIconGap 4 TabbedPane.underlineColor #4a88c7 javax.swing.plaf.ColorUIResource [UI] TabbedPaneUI com.formdev.flatlaf.ui.FlatTabbedPaneUI 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 eed8036a..ad631094 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 @@ -215,6 +215,27 @@ public class FlatContainerTest return tab; } + private void tabPlacementChanged() { + int tabPlacement = -1; + switch( (String) tabPlacementField.getSelectedItem() ) { + case "top": tabPlacement = SwingConstants.TOP; break; + case "bottom": tabPlacement = SwingConstants.BOTTOM; break; + case "left": tabPlacement = SwingConstants.LEFT; break; + case "right": tabPlacement = SwingConstants.RIGHT; break; + } + + tabbedPane1.setTabPlacement( (tabPlacement >= 0) ? tabPlacement : SwingConstants.TOP ); + tabbedPane2.setTabPlacement( (tabPlacement >= 0) ? tabPlacement : SwingConstants.BOTTOM ); + tabbedPane3.setTabPlacement( (tabPlacement >= 0) ? tabPlacement : SwingConstants.LEFT ); + tabbedPane4.setTabPlacement( (tabPlacement >= 0) ? tabPlacement : SwingConstants.RIGHT ); + } + + private void tabBackForegroundChanged() { + boolean enabled = tabBackForegroundCheckBox.isSelected(); + tabbedPane1.setBackgroundAt( 0, enabled ? Color.red : null ); + tabbedPane1.setForegroundAt( 1, enabled ? Color.red : null ); + } + private void initComponents() { // JFormDesigner - Component initialization - DO NOT MODIFY //GEN-BEGIN:initComponents JPanel panel9 = new JPanel(); @@ -244,6 +265,9 @@ public class FlatContainerTest customBorderCheckBox = new JCheckBox(); customTabsCheckBox = new JCheckBox(); hasFullBorderCheckBox = new JCheckBox(); + JLabel tabPlacementLabel = new JLabel(); + tabPlacementField = new JComboBox<>(); + tabBackForegroundCheckBox = new JCheckBox(); CellConstraints cc = new CellConstraints(); //======== this ======== @@ -356,6 +380,7 @@ public class FlatContainerTest "[fill]", // rows "[center]" + + "[]" + "[]")); //---- moreTabsCheckBox ---- @@ -411,6 +436,26 @@ public class FlatContainerTest hasFullBorderCheckBox.setText("Show content border"); hasFullBorderCheckBox.addActionListener(e -> hasFullBorderChanged()); panel14.add(hasFullBorderCheckBox, "cell 4 1,alignx left,growx 0"); + + //---- tabPlacementLabel ---- + tabPlacementLabel.setText("Tab placement:"); + panel14.add(tabPlacementLabel, "cell 0 2"); + + //---- tabPlacementField ---- + tabPlacementField.setModel(new DefaultComboBoxModel<>(new String[] { + "default", + "top", + "bottom", + "left", + "right" + })); + tabPlacementField.addActionListener(e -> tabPlacementChanged()); + panel14.add(tabPlacementField, "cell 1 2"); + + //---- tabBackForegroundCheckBox ---- + tabBackForegroundCheckBox.setText("Tab back/foreground"); + tabBackForegroundCheckBox.addActionListener(e -> tabBackForegroundChanged()); + panel14.add(tabBackForegroundCheckBox, "cell 4 2"); } panel9.add(panel14, cc.xywh(1, 11, 3, 1)); } @@ -433,6 +478,8 @@ public class FlatContainerTest private JCheckBox customBorderCheckBox; private JCheckBox customTabsCheckBox; private JCheckBox hasFullBorderCheckBox; + private JComboBox tabPlacementField; + private JCheckBox tabBackForegroundCheckBox; // 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 ce4551bc..437bfa6f 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 @@ -132,7 +132,7 @@ new FormModel { add( new FormContainer( "javax.swing.JPanel", new FormLayoutManager( class net.miginfocom.swing.MigLayout ) { "$layoutConstraints": "insets 0,hidemode 3" "$columnConstraints": "[][fill][][][fill]" - "$rowConstraints": "[center][]" + "$rowConstraints": "[center][][]" } ) { name: "panel14" "opaque": false @@ -251,6 +251,40 @@ new FormModel { }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { "value": "cell 4 1,alignx left,growx 0" } ) + add( new FormComponent( "javax.swing.JLabel" ) { + name: "tabPlacementLabel" + "text": "Tab placement:" + }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { + "value": "cell 0 2" + } ) + add( new FormComponent( "javax.swing.JComboBox" ) { + name: "tabPlacementField" + "model": new javax.swing.DefaultComboBoxModel { + selectedItem: "default" + addElement( "default" ) + addElement( "top" ) + addElement( "bottom" ) + addElement( "left" ) + addElement( "right" ) + } + auxiliary() { + "JavaCodeGenerator.variableLocal": false + "JavaCodeGenerator.typeParameters": "String" + } + addEvent( new FormEvent( "java.awt.event.ActionListener", "actionPerformed", "tabPlacementChanged", false ) ) + }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { + "value": "cell 1 2" + } ) + add( new FormComponent( "javax.swing.JCheckBox" ) { + name: "tabBackForegroundCheckBox" + "text": "Tab back/foreground" + auxiliary() { + "JavaCodeGenerator.variableLocal": false + } + addEvent( new FormEvent( "java.awt.event.ActionListener", "actionPerformed", "tabBackForegroundChanged", false ) ) + }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { + "value": "cell 4 2" + } ) }, new FormLayoutConstraints( class com.jgoodies.forms.layout.CellConstraints ) { "gridY": 11 "gridWidth": 3 @@ -260,7 +294,7 @@ new FormModel { } ) }, new FormLayoutConstraints( null ) { "location": new java.awt.Point( 0, 0 ) - "size": new java.awt.Dimension( 810, 515 ) + "size": new java.awt.Dimension( 810, 570 ) } ) add( new FormContainer( "javax.swing.JPanel", new FormLayoutManager( class net.miginfocom.swing.MigLayout ) { "$layoutConstraints": "hidemode 3,align center center" diff --git a/flatlaf-theme-editor/src/main/resources/com/formdev/flatlaf/themeeditor/FlatLafUIKeys.txt b/flatlaf-theme-editor/src/main/resources/com/formdev/flatlaf/themeeditor/FlatLafUIKeys.txt index 431096d4..a8742276 100644 --- a/flatlaf-theme-editor/src/main/resources/com/formdev/flatlaf/themeeditor/FlatLafUIKeys.txt +++ b/flatlaf-theme-editor/src/main/resources/com/formdev/flatlaf/themeeditor/FlatLafUIKeys.txt @@ -646,6 +646,7 @@ TabbedPane.focusInputMap TabbedPane.font TabbedPane.foreground TabbedPane.hasFullBorder +TabbedPane.hiddenTabsNavigation TabbedPane.highlight TabbedPane.hoverColor TabbedPane.labelShift From c0408045efd99b27486f1c770a0174fe04bcce10 Mon Sep 17 00:00:00 2001 From: Karl Tauber Date: Thu, 15 Oct 2020 10:41:45 +0200 Subject: [PATCH 2/7] TabbedPane: support specifying hiddenTabsNavigation type per tabbedpane via client property (issue #40) --- .../formdev/flatlaf/FlatClientProperties.java | 24 +++++++++++++ .../formdev/flatlaf/ui/FlatTabbedPaneUI.java | 35 +++++++++++++------ .../flatlaf/testing/FlatContainerTest.java | 33 +++++++++++++++-- .../flatlaf/testing/FlatContainerTest.jfd | 22 ++++++++++++ 4 files changed, 101 insertions(+), 13 deletions(-) diff --git a/flatlaf-core/src/main/java/com/formdev/flatlaf/FlatClientProperties.java b/flatlaf-core/src/main/java/com/formdev/flatlaf/FlatClientProperties.java index 05183459..3ad1dd8f 100644 --- a/flatlaf-core/src/main/java/com/formdev/flatlaf/FlatClientProperties.java +++ b/flatlaf-core/src/main/java/com/formdev/flatlaf/FlatClientProperties.java @@ -240,6 +240,30 @@ public interface FlatClientProperties */ String TABBED_PANE_TAB_HEIGHT = "JTabbedPane.tabHeight"; + /** + * Specifies how to navigate to hidden tabs. + *

+ * Component {@link javax.swing.JTabbedPane}
+ * Value type {@link java.lang.String} + * Allowed Values {@link #TABBED_PANE_HIDDEN_TABS_NAVIGATION_MORE_TABS_BUTTON} + * or {@link #TABBED_PANE_HIDDEN_TABS_NAVIGATION_ARROW_BUTTONS} + */ + String TABBED_PANE_HIDDEN_TABS_NAVIGATION = "JTabbedPane.hiddenTabsNavigation"; + + /** + * Use "more tabs" button for navigation to hidden tabs. + * + * @see #TABBED_PANE_HIDDEN_TABS_NAVIGATION + */ + String TABBED_PANE_HIDDEN_TABS_NAVIGATION_MORE_TABS_BUTTON = "moreTabsButton"; + + /** + * Use forward/backward buttons for navigation to hidden tabs. + * + * @see #TABBED_PANE_HIDDEN_TABS_NAVIGATION + */ + String TABBED_PANE_HIDDEN_TABS_NAVIGATION_ARROW_BUTTONS = "arrowButtons"; + /** * Specifies whether all text is selected when the text component gains focus. *

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 20f7579b..f8df3d51 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 @@ -268,13 +268,27 @@ public class FlatTabbedPaneUI } } - // initialize here because used in installHiddenTabsNavigation() before installDefaults() was invoked - hiddenTabsNavigation = parseHiddenTabsNavigation( UIManager.getString( "TabbedPane.hiddenTabsNavigation" ) ); - installHiddenTabsNavigation(); } - private void installHiddenTabsNavigation() { + @Override + protected void uninstallComponents() { + // uninstall hidden tabs navigation before invoking super.uninstallComponents() for + // correct uninstallation of BasicTabbedPaneUI tab scroller support + uninstallHiddenTabsNavigation(); + + super.uninstallComponents(); + + tabViewport = null; + } + + protected void installHiddenTabsNavigation() { + // initialize here because used in installHiddenTabsNavigation() before installDefaults() was invoked + String hiddenTabsNavigationStr = (String) tabPane.getClientProperty( TABBED_PANE_HIDDEN_TABS_NAVIGATION ); + if( hiddenTabsNavigationStr == null ) + hiddenTabsNavigationStr = UIManager.getString( "TabbedPane.hiddenTabsNavigation" ); + hiddenTabsNavigation = parseHiddenTabsNavigation( hiddenTabsNavigationStr ); + if( hiddenTabsNavigation != MORE_TABS_BUTTON || !isScrollTabLayout() || tabViewport == null ) @@ -293,21 +307,16 @@ public class FlatTabbedPaneUI tabPane.add( moreTabsButton ); } - @Override - protected void uninstallComponents() { + protected void uninstallHiddenTabsNavigation() { // restore layout manager before invoking super.uninstallComponents() for // correct uninstallation of BasicTabbedPaneUI tab scroller support if( tabPane.getLayout() instanceof FlatTabbedPaneScrollLayout ) tabPane.setLayout( ((FlatTabbedPaneScrollLayout)tabPane.getLayout()).delegate ); - super.uninstallComponents(); - if( moreTabsButton != null ) { tabPane.remove( moreTabsButton ); moreTabsButton = null; } - - tabViewport = null; } @Override @@ -1276,6 +1285,12 @@ public class FlatTabbedPaneUI tabPane.revalidate(); tabPane.repaint(); break; + + case TABBED_PANE_HIDDEN_TABS_NAVIGATION: + uninstallHiddenTabsNavigation(); + installHiddenTabsNavigation(); + tabPane.repaint(); + break; } } 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 ad631094..6120407c 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 @@ -16,9 +16,7 @@ package com.formdev.flatlaf.testing; -import static com.formdev.flatlaf.FlatClientProperties.TABBED_PANE_HAS_FULL_BORDER; -import static com.formdev.flatlaf.FlatClientProperties.TABBED_PANE_SHOW_CONTENT_SEPARATOR; -import static com.formdev.flatlaf.FlatClientProperties.TABBED_PANE_SHOW_TAB_SEPARATORS; +import static com.formdev.flatlaf.FlatClientProperties.*; import java.awt.*; import javax.swing.*; import javax.swing.border.*; @@ -230,6 +228,19 @@ public class FlatContainerTest tabbedPane4.setTabPlacement( (tabPlacement >= 0) ? tabPlacement : SwingConstants.RIGHT ); } + private void hiddenTabsNavigationChanged() { + String value = null; + switch( (String) hiddenTabsNavigationField.getSelectedItem() ) { + case "moreTabsButton": value = TABBED_PANE_HIDDEN_TABS_NAVIGATION_MORE_TABS_BUTTON; break; + case "arrowButtons": value = TABBED_PANE_HIDDEN_TABS_NAVIGATION_ARROW_BUTTONS; break; + } + + tabbedPane1.putClientProperty( TABBED_PANE_HIDDEN_TABS_NAVIGATION, value ); + tabbedPane2.putClientProperty( TABBED_PANE_HIDDEN_TABS_NAVIGATION, value ); + tabbedPane3.putClientProperty( TABBED_PANE_HIDDEN_TABS_NAVIGATION, value ); + tabbedPane4.putClientProperty( TABBED_PANE_HIDDEN_TABS_NAVIGATION, value ); + } + private void tabBackForegroundChanged() { boolean enabled = tabBackForegroundCheckBox.isSelected(); tabbedPane1.setBackgroundAt( 0, enabled ? Color.red : null ); @@ -267,6 +278,8 @@ public class FlatContainerTest hasFullBorderCheckBox = new JCheckBox(); JLabel tabPlacementLabel = new JLabel(); tabPlacementField = new JComboBox<>(); + JLabel hiddenTabsNavigationLabel = new JLabel(); + hiddenTabsNavigationField = new JComboBox<>(); tabBackForegroundCheckBox = new JCheckBox(); CellConstraints cc = new CellConstraints(); @@ -452,6 +465,19 @@ public class FlatContainerTest tabPlacementField.addActionListener(e -> tabPlacementChanged()); panel14.add(tabPlacementField, "cell 1 2"); + //---- hiddenTabsNavigationLabel ---- + hiddenTabsNavigationLabel.setText("Hidden tabs navigation:"); + panel14.add(hiddenTabsNavigationLabel, "cell 2 2"); + + //---- hiddenTabsNavigationField ---- + hiddenTabsNavigationField.setModel(new DefaultComboBoxModel<>(new String[] { + "default", + "moreTabsButton", + "arrowButtons" + })); + hiddenTabsNavigationField.addActionListener(e -> hiddenTabsNavigationChanged()); + panel14.add(hiddenTabsNavigationField, "cell 3 2"); + //---- tabBackForegroundCheckBox ---- tabBackForegroundCheckBox.setText("Tab back/foreground"); tabBackForegroundCheckBox.addActionListener(e -> tabBackForegroundChanged()); @@ -479,6 +505,7 @@ public class FlatContainerTest private JCheckBox customTabsCheckBox; private JCheckBox hasFullBorderCheckBox; private JComboBox tabPlacementField; + private JComboBox hiddenTabsNavigationField; private JCheckBox tabBackForegroundCheckBox; // 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 437bfa6f..4e481d5b 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 @@ -275,6 +275,28 @@ new FormModel { }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { "value": "cell 1 2" } ) + add( new FormComponent( "javax.swing.JLabel" ) { + name: "hiddenTabsNavigationLabel" + "text": "Hidden tabs navigation:" + }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { + "value": "cell 2 2" + } ) + add( new FormComponent( "javax.swing.JComboBox" ) { + name: "hiddenTabsNavigationField" + "model": new javax.swing.DefaultComboBoxModel { + selectedItem: "default" + addElement( "default" ) + addElement( "moreTabsButton" ) + addElement( "arrowButtons" ) + } + auxiliary() { + "JavaCodeGenerator.variableLocal": false + "JavaCodeGenerator.typeParameters": "String" + } + addEvent( new FormEvent( "java.awt.event.ActionListener", "actionPerformed", "hiddenTabsNavigationChanged", false ) ) + }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { + "value": "cell 3 2" + } ) add( new FormComponent( "javax.swing.JCheckBox" ) { name: "tabBackForegroundCheckBox" "text": "Tab back/foreground" From 1f5e08fdc6f7cc853af97f7436af569d64847fa7 Mon Sep 17 00:00:00 2001 From: Karl Tauber Date: Thu, 15 Oct 2020 13:16:21 +0200 Subject: [PATCH 3/7] TabbedPane: fixed clipping title if "more tabs" button is used (issue #40) --- .../formdev/flatlaf/ui/FlatTabbedPaneUI.java | 15 ++++++++ .../flatlaf/util/JavaCompatibility.java | 38 ++++++++++++++++++- 2 files changed, 52 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 f8df3d51..8b376ce3 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 @@ -71,6 +71,7 @@ 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.JavaCompatibility; import com.formdev.flatlaf.util.UIScale; /** @@ -497,6 +498,20 @@ public class FlatTabbedPaneUI return; } + // clip title if "more tabs" button is used + // (normally this is done by invoker, but fails in this case) + if( hiddenTabsNavigation == MORE_TABS_BUTTON && + tabViewport != null && + (tabPlacement == TOP || tabPlacement == BOTTOM) ) + { + Rectangle viewRect = tabViewport.getViewRect(); + viewRect.width -= 4; // subtract width of cropped edge + if( !viewRect.contains( textRect ) ) { + Rectangle r = viewRect.intersection( textRect ); + title = JavaCompatibility.getClippedString( null, metrics, title, r.width ); + } + } + // plain text Color color; if( tabPane.isEnabled() && tabPane.isEnabledAt( tabIndex ) ) { diff --git a/flatlaf-core/src/main/java/com/formdev/flatlaf/util/JavaCompatibility.java b/flatlaf-core/src/main/java/com/formdev/flatlaf/util/JavaCompatibility.java index dc696af6..f4e740fc 100644 --- a/flatlaf-core/src/main/java/com/formdev/flatlaf/util/JavaCompatibility.java +++ b/flatlaf-core/src/main/java/com/formdev/flatlaf/util/JavaCompatibility.java @@ -16,6 +16,7 @@ package com.formdev.flatlaf.util; +import java.awt.FontMetrics; import java.awt.Graphics; import java.awt.Graphics2D; import java.lang.reflect.InvocationTargetException; @@ -27,7 +28,7 @@ import com.formdev.flatlaf.FlatLaf; /** * Provides Java version compatibility methods. - * + *

* WARNING: This is private API and may change. * * @author Karl Tauber @@ -35,10 +36,12 @@ import com.formdev.flatlaf.FlatLaf; public class JavaCompatibility { private static Method drawStringUnderlineCharAtMethod; + private static Method getClippedStringMethod; /** * Java 8: sun.swing.SwingUtilities2.drawStringUnderlineCharAt( JComponent c, * Graphics g, String text, int underlinedIndex, int x, int y ) + *
* Java 9: javax.swing.plaf.basic.BasicGraphicsUtils.drawStringUnderlineCharAt( JComponent c, * Graphics2D g, String string, int underlinedIndex, float x, float y ) */ @@ -71,4 +74,37 @@ public class JavaCompatibility throw new RuntimeException( ex ); } } + + /** + * Java 8: sun.swing.SwingUtilities2.clipStringIfNecessary( JComponent c, + * FontMetrics fm, String string, int availTextWidth ) + *
+ * Java 9: javax.swing.plaf.basic.BasicGraphicsUtils.getClippedString( JComponent c, + * FontMetrics fm, String string, int availTextWidth ) + */ + public static String getClippedString( JComponent c, FontMetrics fm, String string, int availTextWidth ) { + synchronized( JavaCompatibility.class ) { + if( getClippedStringMethod == null ) { + try { + Class cls = Class.forName( SystemInfo.isJava_9_orLater + ? "javax.swing.plaf.basic.BasicGraphicsUtils" + : "sun.swing.SwingUtilities2" ); + getClippedStringMethod = cls.getMethod( SystemInfo.isJava_9_orLater + ? "getClippedString" + : "clipStringIfNecessary", + new Class[] { JComponent.class, FontMetrics.class, String.class, int.class } ); + } catch( Exception ex ) { + Logger.getLogger( FlatLaf.class.getName() ).log( Level.SEVERE, null, ex ); + throw new RuntimeException( ex ); + } + } + } + + try { + return (String) getClippedStringMethod.invoke( null, c, fm, string, availTextWidth ); + } catch( IllegalAccessException | IllegalArgumentException | InvocationTargetException ex ) { + Logger.getLogger( FlatLaf.class.getName() ).log( Level.SEVERE, null, ex ); + throw new RuntimeException( ex ); + } + } } From 2c1075f4710bcc1c04bf705228eb6ccf55855c25 Mon Sep 17 00:00:00 2001 From: Karl Tauber Date: Thu, 15 Oct 2020 14:53:42 +0200 Subject: [PATCH 4/7] TabbedPane: do not clip title on left tabs when scrolled --- .../main/java/com/formdev/flatlaf/ui/FlatTabbedPaneUI.java | 4 ++-- 1 file changed, 2 insertions(+), 2 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 8b376ce3..4c056e74 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 @@ -188,7 +188,6 @@ public class FlatTabbedPaneUI } else super.installDefaults(); - disabledForeground = UIManager.getColor( "TabbedPane.disabledForeground" ); selectedBackground = UIManager.getColor( "TabbedPane.selectedBackground" ); selectedForeground = UIManager.getColor( "TabbedPane.selectedForeground" ); @@ -508,7 +507,8 @@ public class FlatTabbedPaneUI viewRect.width -= 4; // subtract width of cropped edge if( !viewRect.contains( textRect ) ) { Rectangle r = viewRect.intersection( textRect ); - title = JavaCompatibility.getClippedString( null, metrics, title, r.width ); + if( r.x > viewRect.x ) + title = JavaCompatibility.getClippedString( null, metrics, title, r.width ); } } From 201581a07c00ea316ed0a044ff22bf24854f4cd1 Mon Sep 17 00:00:00 2001 From: Karl Tauber Date: Fri, 16 Oct 2020 00:24:02 +0200 Subject: [PATCH 5/7] TabbedPane: support right-to-left if "more tabs" button is used (issue #40) --- .../formdev/flatlaf/ui/FlatTabbedPaneUI.java | 87 +++++++++++++++---- .../flatlaf/testing/FlatContainerTest.java | 5 +- 2 files changed, 73 insertions(+), 19 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 4c056e74..80cbed5d 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 @@ -802,12 +802,13 @@ public class FlatTabbedPaneUI if( tabPane == null || tabViewport == null ) return; + ensureCurrentLayout(); + int selectedIndex = tabPane.getSelectedIndex(); - if( selectedIndex < 0 ) + if( selectedIndex < 0 || selectedIndex >= rects.length ) return; - Rectangle tabBounds = getTabBounds( tabPane, selectedIndex ); - tabViewport.scrollRectToVisible( tabBounds ); + ((JComponent)tabViewport.getView()).scrollRectToVisible( (Rectangle) rects[selectedIndex].clone() ); } //---- class FlatMoreTabsButton ------------------------------------------- @@ -881,20 +882,23 @@ public class FlatTabbedPaneUI } } - // show popup menu + // compute popup menu location int buttonWidth = getWidth(); int buttonHeight = getHeight(); - int x = 0; - int y = 0; Dimension popupSize = popupMenu.getPreferredSize(); + boolean leftToRight = tabPane.getComponentOrientation().isLeftToRight(); + + int x = leftToRight ? buttonWidth - popupSize.width : 0; + int y = buttonHeight - popupSize.height; switch( tabPane.getTabPlacement() ) { default: - case TOP: x = buttonWidth - popupSize.width; y = buttonHeight; break; - case BOTTOM: x = buttonWidth - popupSize.width; y = -popupSize.height; break; - case LEFT: x = buttonWidth; y = buttonHeight - popupSize.height; break; - case RIGHT: x = -popupSize.width; y = buttonHeight - popupSize.height; break; - + case TOP: y = buttonHeight; break; + case BOTTOM: y = -popupSize.height; break; + case LEFT: x = buttonWidth; break; + case RIGHT: x = -popupSize.width; break; } + + // show popup menu popupMenu.show( this, x, y ); } @@ -916,10 +920,15 @@ public class FlatTabbedPaneUI if( !tabPane.isEnabledAt( index ) ) menuItem.setEnabled( false ); - menuItem.addActionListener( e -> tabPane.setSelectedIndex( index ) ); + menuItem.addActionListener( e -> selectTab( index ) ); return menuItem; } + protected void selectTab( int index ) { + tabPane.setSelectedIndex( index ); + ensureSelectedTabIsVisible(); + } + @Override public void popupMenuWillBecomeVisible( PopupMenuEvent e ) { popupVisible = true; @@ -1052,6 +1061,7 @@ public class FlatTabbedPaneUI lastMouseY = e.getY(); double preciseWheelRotation = e.getPreciseWheelRotation(); + int amount = (int) (maxTabHeight * preciseWheelRotation); // compute new view position Point viewPosition = (targetViewPosition != null) @@ -1062,10 +1072,11 @@ public class FlatTabbedPaneUI int y = viewPosition.y; int tabPlacement = tabPane.getTabPlacement(); if( tabPlacement == TOP || tabPlacement == BOTTOM ) { - x += maxTabHeight * preciseWheelRotation; + boolean leftToRight = tabPane.getComponentOrientation().isLeftToRight(); + x += leftToRight ? amount : -amount; x = Math.min( Math.max( x, 0 ), viewSize.width - tabViewport.getWidth() ); } else { - y += maxTabHeight * preciseWheelRotation; + y += amount; y = Math.min( Math.max( y, 0 ), viewSize.height - tabViewport.getHeight() ); } @@ -1395,11 +1406,11 @@ public class FlatTabbedPaneUI // check whether scroll buttons are visible, which is changed by original // layout manager depending on whether there is enough room for all tabs - boolean scrollButtonsVisible = false; + boolean moreTabsButtonVisible = false; Rectangle buttonsBounds = null; for( Component c : tabPane.getComponents() ) { if( c instanceof FlatScrollableTabButton && c.isVisible() ) { - scrollButtonsVisible = true; + moreTabsButtonVisible = true; // compute union bounds of all scroll buttons Rectangle r = c.getBounds(); @@ -1410,8 +1421,50 @@ public class FlatTabbedPaneUI } } + // fixes for bugs in TabbedPaneScrollLayout + if( tabPane.getTabPlacement() == TOP || tabPane.getTabPlacement() == BOTTOM ) { + Insets insets = tabPane.getInsets(); + if( !tabPane.getComponentOrientation().isLeftToRight() ) { + // fixes for right-to-left, which is faulty in TabbedPaneScrollLayout.calculateTabRects() + + // if tabbed pane width is smaller than total tabs width, + // the x locations are not zero based + int xLastTab = rects[rects.length - 1].x; + int offset = (xLastTab < 0) ? xLastTab : 0; + if( offset != 0 ) { + for( int i = 0; i < rects.length; i++ ) { + // fix x location in rects + rects[i].x -= offset; + + // fix tab component location + Component c = tabPane.getTabComponentAt( i ); + if( c != null ) + c.setLocation( c.getX() - offset, c.getY() ); + } + + moreTabsButtonVisible = true; + + Insets tabAreaInsets = getTabAreaInsets( tabPane.getTabPlacement() ); + Rectangle bounds = tabViewport.getBounds(); + + // compute "more tabs" button bounds + int buttonWidth = moreTabsButton.getPreferredSize().width + UIScale.scale( 8 ); + int buttonHeight = bounds.height - tabAreaInsets.top - tabAreaInsets.bottom; + buttonsBounds = new Rectangle( bounds.x, bounds.y + bounds.height - buttonHeight, buttonWidth, buttonHeight ); + + // make viewport smaller on left side so that there is room for the button + tabViewport.setBounds( bounds.x + buttonWidth, bounds.y, bounds.width - buttonWidth, bounds.height ); + } + } else { + // TabbedPaneScrollLayout.layoutContainer() uses insets.left to + // compute button x-location where it should use insets.right + if( buttonsBounds != null && insets.left != insets.right ) + buttonsBounds.x = tabPane.getWidth() - insets.right - buttonsBounds.width; + } + } + // show/hide "more tabs" button and layout it - moreTabsButton.setVisible( scrollButtonsVisible ); + moreTabsButton.setVisible( moreTabsButtonVisible ); if( buttonsBounds != null ) moreTabsButton.setBounds( buttonsBounds ); } 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 6120407c..53cb920c 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 @@ -38,6 +38,7 @@ public class FlatContainerTest public static void main( String[] args ) { SwingUtilities.invokeLater( () -> { FlatTestFrame frame = FlatTestFrame.create( args, "FlatContainerTest" ); + frame.useApplyComponentOrientation = true; frame.showFrame( FlatContainerTest::new ); } ); } @@ -115,7 +116,7 @@ public class FlatContainerTest for( int i = oldTabCount + 1; i <= newTabCount; i++ ) addTab( tabbedPane, "Tab " + i, "tab content " + i ); } else if( newTabCount < oldTabCount ) { - while( tabbedPane.getTabCount() > 4 ) + while( tabbedPane.getTabCount() > newTabCount ) tabbedPane.removeTabAt( tabbedPane.getTabCount() - 1 ); } } @@ -171,7 +172,7 @@ public class FlatContainerTest private void customBorderChanged() { Border border = customBorderCheckBox.isSelected() - ? new LineBorder( Color.magenta, 20 ) + ? new MatteBorder( 10, 20, 25, 35, Color.green ) : null; tabbedPane1.setBorder( border ); From a909f1012a3164ceed96ed774af3e4bd789181d0 Mon Sep 17 00:00:00 2001 From: Karl Tauber Date: Fri, 16 Oct 2020 12:25:04 +0200 Subject: [PATCH 6/7] TabbedPane: finally get rid of the cropped edge (issue #40) --- .../com/formdev/flatlaf/ui/FlatTabbedPaneUI.java | 15 +++++++++++++++ 1 file changed, 15 insertions(+) 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 80cbed5d..5861481a 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 @@ -435,6 +435,21 @@ public class FlatTabbedPaneUI return Math.max( tabHeight, super.calculateTabHeight( tabPlacement, tabIndex, fontHeight ) - 2 /* was added by superclass */ ); } + @Override + protected Insets getTabAreaInsets( int tabPlacement ) { + Insets currentTabAreaInsets = super.getTabAreaInsets( tabPlacement ); + Insets insets = (Insets) currentTabAreaInsets.clone(); + + // This is a "trick" to get rid of the cropped edge: + // super.getTabAreaInsets() returns private field BasicTabbedPaneUI.currentTabAreaInsets, + // which is also used to translate the origin of the cropped edge in + // BasicTabbedPaneUI.CroppedEdge.paintComponent(). + // Giving it large values clips painting of the cropped edge and makes it invisible. + currentTabAreaInsets.top = currentTabAreaInsets.left = -10000; + + return insets; + } + /** * The content border insets are used to create a separator between tabs and content. * If client property JTabbedPane.hasFullBorder is true, then the content border insets From bfaac6d16495b0e9a6e527944c2e625fc4f246c6 Mon Sep 17 00:00:00 2001 From: Karl Tauber Date: Fri, 16 Oct 2020 21:13:06 +0200 Subject: [PATCH 7/7] TabbedPane: fixed: content separator was painted at wrong position if using TabbedPane.tabAreaInsets (regression since changing TabbedPane.tabsOverlapBorder to false in commit c58f5a6ca7c4b5dc5aacd5e25ad6cc6d38c891ef) exit paintContentBorder() early if content separator is not painted --- .../formdev/flatlaf/ui/FlatTabbedPaneUI.java | 48 +++++++++---------- 1 file changed, 22 insertions(+), 26 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 5861481a..0f77cfc2 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 @@ -146,7 +146,6 @@ public class FlatTabbedPaneUI protected boolean showTabSeparators; protected boolean tabSeparatorsFullHeight; protected boolean hasFullBorder; - protected boolean tabsOverlapBorder; protected int hiddenTabsNavigation = MORE_TABS_BUTTON; @@ -204,7 +203,6 @@ public class FlatTabbedPaneUI showTabSeparators = UIManager.getBoolean( "TabbedPane.showTabSeparators" ); tabSeparatorsFullHeight = UIManager.getBoolean( "TabbedPane.tabSeparatorsFullHeight" ); hasFullBorder = UIManager.getBoolean( "TabbedPane.hasFullBorder" ); - tabsOverlapBorder = UIManager.getBoolean( "TabbedPane.tabsOverlapBorder" ); Locale l = tabPane.getLocale(); moreTabsButtonToolTipText = UIManager.getString( "TabbedPane.moreTabsButtonToolTipText", l ); @@ -654,13 +652,17 @@ public class FlatTabbedPaneUI /** * Actually does nearly the same as super.paintContentBorder() but * - not using UIManager.getColor("TabbedPane.contentAreaColor") to be GUI builder friendly + * - tabsOverlapBorder is always true + * - paint full border (if enabled) * - not invoking paintContentBorder*Edge() methods * - repaint selection */ @Override protected void paintContentBorder( Graphics g, int tabPlacement, int selectedIndex ) { - if( tabPane.getTabCount() <= 0 ) - return; + if( tabPane.getTabCount() <= 0 || + contentSeparatorHeight == 0 || + !clientPropertyBoolean( tabPane, TABBED_PANE_SHOW_CONTENT_SEPARATOR, true ) ) + return; Insets insets = tabPane.getInsets(); Insets tabAreaInsets = getTabAreaInsets( tabPlacement ); @@ -675,46 +677,40 @@ public class FlatTabbedPaneUI case TOP: default: y += calculateTabAreaHeight( tabPlacement, runCount, maxTabHeight ); - if( tabsOverlapBorder ) - y -= tabAreaInsets.bottom; + y -= tabAreaInsets.bottom; h -= (y - insets.top); break; case BOTTOM: h -= calculateTabAreaHeight( tabPlacement, runCount, maxTabHeight ); - if( tabsOverlapBorder ) - h += tabAreaInsets.top; + h += tabAreaInsets.top; break; case LEFT: x += calculateTabAreaWidth( tabPlacement, runCount, maxTabWidth ); - if( tabsOverlapBorder ) - x -= tabAreaInsets.right; + x -= tabAreaInsets.right; w -= (x - insets.left); break; case RIGHT: w -= calculateTabAreaWidth( tabPlacement, runCount, maxTabWidth ); - if( tabsOverlapBorder ) - w += tabAreaInsets.left; + w += tabAreaInsets.left; break; } - if( contentSeparatorHeight != 0 && clientPropertyBoolean( tabPane, TABBED_PANE_SHOW_CONTENT_SEPARATOR, true ) ) { - // compute insets for separator or full border - boolean hasFullBorder = clientPropertyBoolean( tabPane, TABBED_PANE_HAS_FULL_BORDER, this.hasFullBorder ); - int sh = scale( contentSeparatorHeight * 100 ); // multiply by 100 because rotateInsets() does not use floats - Insets ci = new Insets( 0, 0, 0, 0 ); - rotateInsets( hasFullBorder ? new Insets( sh, sh, sh, sh ) : new Insets( sh, 0, 0, 0 ), ci, tabPlacement ); + // compute insets for separator or full border + boolean hasFullBorder = clientPropertyBoolean( tabPane, TABBED_PANE_HAS_FULL_BORDER, this.hasFullBorder ); + int sh = scale( contentSeparatorHeight * 100 ); // multiply by 100 because rotateInsets() does not use floats + Insets ci = new Insets( 0, 0, 0, 0 ); + rotateInsets( hasFullBorder ? new Insets( sh, sh, sh, sh ) : new Insets( sh, 0, 0, 0 ), ci, tabPlacement ); - // paint content separator or full border - g.setColor( contentAreaColor ); - Path2D path = new Path2D.Float( Path2D.WIND_EVEN_ODD ); - path.append( new Rectangle2D.Float( x, y, w, h ), false ); - path.append( new Rectangle2D.Float( x + (ci.left / 100f), y + (ci.top / 100f), - w - (ci.left / 100f) - (ci.right / 100f), h - (ci.top / 100f) - (ci.bottom / 100f) ), false ); - ((Graphics2D)g).fill( path ); - } + // paint content separator or full border + g.setColor( contentAreaColor ); + Path2D path = new Path2D.Float( Path2D.WIND_EVEN_ODD ); + path.append( new Rectangle2D.Float( x, y, w, h ), false ); + path.append( new Rectangle2D.Float( x + (ci.left / 100f), y + (ci.top / 100f), + w - (ci.left / 100f) - (ci.right / 100f), h - (ci.top / 100f) - (ci.bottom / 100f) ), false ); + ((Graphics2D)g).fill( path ); // repaint selection in scroll-tab-layout because it may be painted before // the content border was painted (from BasicTabbedPaneUI$ScrollableTabPanel)