diff --git a/CHANGELOG.md b/CHANGELOG.md index 8775ba4a..57c98ef3 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/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 983d458f..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 @@ -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; @@ -57,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; /** @@ -100,6 +115,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 +123,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; @@ -126,11 +146,16 @@ public class FlatTabbedPaneUI protected boolean showTabSeparators; protected boolean tabSeparatorsFullHeight; 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,27 @@ 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" ); @@ -158,7 +203,9 @@ 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 ); // scale textIconGap = scale( textIconGap ); @@ -218,6 +265,56 @@ public class FlatTabbedPaneUI } } } + + 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 ) + 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 ); + } + + 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 ); + + if( moreTabsButton != null ) { + tabPane.remove( moreTabsButton ); + moreTabsButton = null; + } } @Override @@ -225,6 +322,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 +341,8 @@ public class FlatTabbedPaneUI if( handler != null ) { tabPane.removeMouseListener( handler ); + tabPane.removeMouseMotionListener( handler ); + tabPane.removeComponentListener( handler ); handler = null; } @@ -272,6 +373,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 ); @@ -317,6 +433,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 @@ -353,6 +484,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 ) @@ -366,6 +510,21 @@ 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 ); + if( r.x > viewRect.x ) + title = JavaCompatibility.getClippedString( null, metrics, title, r.width ); + } + } + // plain text Color color; if( tabPane.isEnabled() && tabPane.isEnabledAt( tabIndex ) ) { @@ -493,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 ); @@ -514,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) @@ -573,6 +730,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 +786,179 @@ 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; + + ensureCurrentLayout(); + + int selectedIndex = tabPane.getSelectedIndex(); + if( selectedIndex < 0 || selectedIndex >= rects.length ) + return; + + ((JComponent)tabViewport.getView()).scrollRectToVisible( (Rectangle) rects[selectedIndex].clone() ); + } + + //---- 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 ) ); + } + } + + // compute popup menu location + int buttonWidth = getWidth(); + int buttonHeight = getHeight(); + 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: 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 ); + } + + 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 -> selectTab( index ) ); + return menuItem; + } + + protected void selectTab( int index ) { + tabPane.setSelectedIndex( index ); + ensureSelectedTabIsVisible(); + } + + @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 +979,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 @@ -695,6 +1072,7 @@ public class FlatTabbedPaneUI lastMouseY = e.getY(); double preciseWheelRotation = e.getPreciseWheelRotation(); + int amount = (int) (maxTabHeight * preciseWheelRotation); // compute new view position Point viewPosition = (targetViewPosition != null) @@ -705,10 +1083,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() ); } @@ -871,11 +1250,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 +1259,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 +1280,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: @@ -907,7 +1322,162 @@ public class FlatTabbedPaneUI tabPane.revalidate(); tabPane.repaint(); break; + + case TABBED_PANE_HIDDEN_TABS_NAVIGATION: + uninstallHiddenTabsNavigation(); + installHiddenTabsNavigation(); + tabPane.repaint(); + 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 moreTabsButtonVisible = false; + Rectangle buttonsBounds = null; + for( Component c : tabPane.getComponents() ) { + if( c instanceof FlatScrollableTabButton && c.isVisible() ) { + moreTabsButtonVisible = 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 ); + } + } + + // 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( moreTabsButtonVisible ); + if( buttonsBounds != null ) + moreTabsButton.setBounds( buttonsBounds ); + } } } 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 ); + } + } } 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..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 @@ -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.*; @@ -40,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 ); } ); } @@ -117,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 ); } } @@ -173,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 ); @@ -215,6 +214,40 @@ 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 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 ); + 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 +277,11 @@ public class FlatContainerTest customBorderCheckBox = new JCheckBox(); customTabsCheckBox = new JCheckBox(); hasFullBorderCheckBox = new JCheckBox(); + JLabel tabPlacementLabel = new JLabel(); + tabPlacementField = new JComboBox<>(); + JLabel hiddenTabsNavigationLabel = new JLabel(); + hiddenTabsNavigationField = new JComboBox<>(); + tabBackForegroundCheckBox = new JCheckBox(); CellConstraints cc = new CellConstraints(); //======== this ======== @@ -356,6 +394,7 @@ public class FlatContainerTest "[fill]", // rows "[center]" + + "[]" + "[]")); //---- moreTabsCheckBox ---- @@ -411,6 +450,39 @@ 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"); + + //---- 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()); + panel14.add(tabBackForegroundCheckBox, "cell 4 2"); } panel9.add(panel14, cc.xywh(1, 11, 3, 1)); } @@ -433,6 +505,9 @@ public class FlatContainerTest private JCheckBox customBorderCheckBox; private JCheckBox customTabsCheckBox; private JCheckBox hasFullBorderCheckBox; + private JComboBox tabPlacementField; + private JComboBox hiddenTabsNavigationField; + 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..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 @@ -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,62 @@ 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.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" + 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 +316,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