diff --git a/CHANGELOG.md b/CHANGELOG.md index 237afbc8..ce6dba2a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -21,9 +21,13 @@ FlatLaf Change Log - TabbedPane: Support closable tabs. (PR #193; issues #31 and #40) - TabbedPane: Support minimum or maximum tab widths. (set client property `JTabbedPane.minimumTabWidth` or `JTabbedPane.maximumTabWidth` to an integer) + (PR #199) - TabbedPane: Support alignment of tab area. (set client property `JTabbedPane.tabAreaAlignment` to `"leading"`, `"trailing"`, `"center"` or - `"fill"`) + `"fill"`) (PR #199) +- TabbedPane: Support equal and compact tab width modes. (set client property + `JTabbedPane.tabWidthMode` to `"preferred"`, `"equal"` or `"compact"`) (PR + #199) - Support painting separator line between window title and content (use UI value `TitlePane.borderColor`). (issue #184) - Extras: `FlatSVGIcon` now allows specifying icon width and height in 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 e6c06481..570b3eab 100644 --- a/flatlaf-core/src/main/java/com/formdev/flatlaf/FlatClientProperties.java +++ b/flatlaf-core/src/main/java/com/formdev/flatlaf/FlatClientProperties.java @@ -410,6 +410,38 @@ public interface FlatClientProperties */ String TABBED_PANE_TAB_AREA_ALIGN_FILL = "fill"; + /** + * Specifies how the tabs should be sized. + *

+ * Component {@link javax.swing.JTabbedPane}
+ * Value type {@link java.lang.String} + * Allowed Values {@link #TABBED_PANE_TAB_WIDTH_MODE_PREFERRED} (default), + * {@link #TABBED_PANE_TAB_WIDTH_MODE_EQUAL} or {@link #TABBED_PANE_TAB_WIDTH_MODE_COMPACT} + */ + String TABBED_PANE_TAB_WIDTH_MODE = "JTabbedPane.tabWidthMode"; + + /** + * Tab width is adjusted to tab icon and title. + * + * @see #TABBED_PANE_TAB_WIDTH_MODE + */ + String TABBED_PANE_TAB_WIDTH_MODE_PREFERRED = "preferred"; + + /** + * All tabs in a tabbed pane has same width. + * + * @see #TABBED_PANE_TAB_WIDTH_MODE + */ + String TABBED_PANE_TAB_WIDTH_MODE_EQUAL = "equal"; + + /** + * Unselected tabs are smaller because they show only the tab icon, but no tab title. + * Selected tabs show both. + * + * @see #TABBED_PANE_TAB_WIDTH_MODE + */ + String TABBED_PANE_TAB_WIDTH_MODE_COMPACT = "compact"; + /** * Specifies a component that will be placed at the leading edge of the tabs area. *

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 228227e6..927ef56a 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 @@ -128,6 +128,7 @@ import com.formdev.flatlaf.util.UIScale; * @uiDefault TabbedPane.hasFullBorder boolean * @uiDefault TabbedPane.hiddenTabsNavigation String moreTabsButton (default) or arrowButtons * @uiDefault TabbedPane.tabAreaAlignment String leading (default), center, trailing or fill + * @uiDefault TabbedPane.tabWidthMode String preferred (default), equal or compact * @uiDefault ScrollPane.smoothScrolling boolean * @uiDefault TabbedPane.closeIcon Icon * @@ -148,6 +149,10 @@ public class FlatTabbedPaneUI protected static final int ALIGN_CENTER = 2; protected static final int ALIGN_FILL = 3; + protected static final int WIDTH_MODE_PREFERRED = 0; + protected static final int WIDTH_MODE_EQUAL = 1; + protected static final int WIDTH_MODE_COMPACT = 2; + private static Set focusForwardTraversalKeys; private static Set focusBackwardTraversalKeys; @@ -174,6 +179,7 @@ public class FlatTabbedPaneUI private String hiddenTabsNavigationStr; private String tabAreaAlignmentStr; + private String tabWidthModeStr; protected Icon closeIcon; protected String moreTabsButtonToolTipText; @@ -241,6 +247,7 @@ public class FlatTabbedPaneUI tabsOpaque = UIManager.getBoolean( "TabbedPane.tabsOpaque" ); hiddenTabsNavigationStr = UIManager.getString( "TabbedPane.hiddenTabsNavigation" ); tabAreaAlignmentStr = UIManager.getString( "TabbedPane.tabAreaAlignment" ); + tabWidthModeStr = UIManager.getString( "TabbedPane.tabWidthMode" ); closeIcon = UIManager.getIcon( "TabbedPane.closeIcon" ); Locale l = tabPane.getLocale(); @@ -529,12 +536,35 @@ public class FlatTabbedPaneUI tabPane.repaint( r ); } + private boolean inCalculateEqual; + @Override protected int calculateTabWidth( int tabPlacement, int tabIndex, FontMetrics metrics ) { + int tabWidthMode = getTabWidthMode(); + if( tabWidthMode == WIDTH_MODE_EQUAL && isHorizontalTabPlacement() && !inCalculateEqual ) { + inCalculateEqual = true; + try { + return calculateMaxTabWidth( tabPlacement ); + } finally { + inCalculateEqual = false; + } + } + // update textIconGap before used in super class textIconGap = scale( textIconGapUnscaled ); - int tabWidth = super.calculateTabWidth( tabPlacement, tabIndex, metrics ) - 3 /* was added by superclass */; + int tabWidth; + Icon icon; + if( tabWidthMode == WIDTH_MODE_COMPACT && + tabIndex != tabPane.getSelectedIndex() && + isHorizontalTabPlacement() && + tabPane.getTabComponentAt( tabIndex ) == null && + (icon = getIconForTab( tabIndex )) != null ) + { + Insets tabInsets = getTabInsets( tabPlacement, tabIndex ); + tabWidth = icon.getIconWidth() + tabInsets.left + tabInsets.right; + } else + tabWidth = super.calculateTabWidth( tabPlacement, tabIndex, metrics ) - 3 /* was added by superclass */; // make tab wider if closable if( isTabClosable( tabIndex ) ) @@ -663,14 +693,26 @@ public class FlatTabbedPaneUI int tabIndex, Rectangle iconRect, Rectangle textRect ) { Rectangle tabRect = rects[tabIndex]; + int x = tabRect.x; + int y = tabRect.y; + int w = tabRect.width; + int h = tabRect.height; boolean isSelected = (tabIndex == tabPane.getSelectedIndex()); // paint background if( tabsOpaque || tabPane.isOpaque() ) - paintTabBackground( g, tabPlacement, tabIndex, tabRect.x, tabRect.y, tabRect.width, tabRect.height, isSelected ); + paintTabBackground( g, tabPlacement, tabIndex, x, y, w, h, isSelected ); // paint border - paintTabBorder( g, tabPlacement, tabIndex, tabRect.x, tabRect.y, tabRect.width, tabRect.height, isSelected ); + paintTabBorder( g, tabPlacement, tabIndex, x, y, w, h, isSelected ); + + // paint tab close button + if( isTabClosable( tabIndex ) ) + paintTabCloseButton( g, tabIndex, x, y, w, h ); + + // paint selection indicator + if( isSelected ) + paintTabSelection( g, tabPlacement, x, y, w, h ); if( tabPane.getTabComponentAt( tabIndex ) != null ) return; @@ -680,9 +722,12 @@ public class FlatTabbedPaneUI Icon icon = getIconForTab( tabIndex ); Font font = tabPane.getFont(); FontMetrics metrics = tabPane.getFontMetrics( font ); + boolean isCompact = (icon != null && !isSelected && getTabWidthMode() == WIDTH_MODE_COMPACT && isHorizontalTabPlacement()); + if( isCompact ) + title = null; String clippedTitle = layoutAndClipLabel( tabPlacement, metrics, tabIndex, title, icon, tabRect, iconRect, textRect, isSelected ); - // special title clipping for scroll layout where title off last visible tab on right side may be truncated + // special title clipping for scroll layout where title of last visible tab on right side may be truncated if( tabViewport != null && (tabPlacement == TOP || tabPlacement == BOTTOM) ) { Rectangle viewRect = tabViewport.getViewRect(); viewRect.width -= 4; // subtract width of cropped edge @@ -694,7 +739,8 @@ public class FlatTabbedPaneUI } // paint title and icon - paintText( g, tabPlacement, font, metrics, tabIndex, clippedTitle, textRect, isSelected ); + if( !isCompact ) + paintText( g, tabPlacement, font, metrics, tabIndex, clippedTitle, textRect, isSelected ); paintIcon( g, tabPlacement, tabIndex, icon, iconRect, isSelected ); } @@ -747,17 +793,10 @@ public class FlatTabbedPaneUI protected void paintTabBorder( Graphics g, int tabPlacement, int tabIndex, int x, int y, int w, int h, boolean isSelected ) { - // paint tab close button - if( isTabClosable( tabIndex ) ) - paintTabCloseButton( g, tabIndex, x, y, w, h ); - // paint tab separators if( clientPropertyBoolean( tabPane, TABBED_PANE_SHOW_TAB_SEPARATORS, showTabSeparators ) && !isLastInRun( tabIndex ) ) paintTabSeparator( g, tabPlacement, x, y, w, h ); - - if( isSelected ) - paintTabSelection( g, tabPlacement, x, y, w, h ); } protected void paintTabCloseButton( Graphics g, int tabIndex, int x, int y, int w, int h ) { @@ -1104,6 +1143,13 @@ public class FlatTabbedPaneUI return parseTabAreaAlignment( str ); } + protected int getTabWidthMode() { + String str = (String) tabPane.getClientProperty( TABBED_PANE_TAB_WIDTH_MODE ); + if( str == null ) + str = tabWidthModeStr; + return parseTabWidthMode( str ); + } + protected static int parseHiddenTabsNavigation( String str ) { if( str == null ) return MORE_TABS_BUTTON; @@ -1128,6 +1174,18 @@ public class FlatTabbedPaneUI } } + protected static int parseTabWidthMode( String str ) { + if( str == null ) + return WIDTH_MODE_PREFERRED; + + switch( str ) { + default: + case TABBED_PANE_TAB_WIDTH_MODE_PREFERRED: return WIDTH_MODE_PREFERRED; + case TABBED_PANE_TAB_WIDTH_MODE_EQUAL: return WIDTH_MODE_EQUAL; + case TABBED_PANE_TAB_WIDTH_MODE_COMPACT: return WIDTH_MODE_COMPACT; + } + } + private void runWithOriginalLayoutManager( Runnable runnable ) { LayoutManager layout = tabPane.getLayout(); if( layout instanceof FlatTabbedPaneScrollLayout ) { @@ -1881,6 +1939,7 @@ public class FlatTabbedPaneUI case TABBED_PANE_TAB_INSETS: case TABBED_PANE_HIDDEN_TABS_NAVIGATION: case TABBED_PANE_TAB_AREA_ALIGNMENT: + case TABBED_PANE_TAB_WIDTH_MODE: case TABBED_PANE_TAB_CLOSABLE: tabPane.revalidate(); tabPane.repaint(); 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 8ae1476d..c3d2593a 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 @@ -172,10 +172,8 @@ public class FlatContainerTest ? new ScaledImageIcon( new ImageIcon( getClass().getResource( "/com/formdev/flatlaf/testing/test" + iconSize + ".png" ) ) ) : null; int tabCount = tabbedPane.getTabCount(); - if( tabCount > 0 ) - tabbedPane.setIconAt( 0, icon ); - if( tabCount > 1 ) - tabbedPane.setIconAt( 1, icon ); + for( int i = 0; i < tabCount; i++ ) + tabbedPane.setIconAt( i, icon ); } private void customBorderChanged() { @@ -252,6 +250,13 @@ public class FlatContainerTest putTabbedPanesClientProperty( TABBED_PANE_TAB_AREA_ALIGNMENT, value ); } + private void tabWidthModeChanged() { + String value = (String) tabWidthModeField.getSelectedItem(); + if( "default".equals( value ) ) + value = null; + putTabbedPanesClientProperty( TABBED_PANE_TAB_WIDTH_MODE, value ); + } + private void tabBackForegroundChanged() { tabBackForegroundChanged( tabbedPane1 ); tabBackForegroundChanged( tabbedPane2 ); @@ -387,6 +392,8 @@ public class FlatContainerTest tabIconSizeSpinner = new JSpinner(); JLabel tabAreaAlignmentLabel = new JLabel(); tabAreaAlignmentField = new JComboBox<>(); + JLabel tabWidthModeLabel = new JLabel(); + tabWidthModeField = new JComboBox<>(); tabsClosableCheckBox = new JCheckBox(); customBorderCheckBox = new JCheckBox(); tabAreaInsetsCheckBox = new JCheckBox(); @@ -600,6 +607,20 @@ public class FlatContainerTest tabAreaAlignmentField.addActionListener(e -> tabAreaAlignmentChanged()); tabbedPaneControlPanel.add(tabAreaAlignmentField, "cell 1 3"); + //---- tabWidthModeLabel ---- + tabWidthModeLabel.setText("Tab width mode:"); + tabbedPaneControlPanel.add(tabWidthModeLabel, "cell 2 3"); + + //---- tabWidthModeField ---- + tabWidthModeField.setModel(new DefaultComboBoxModel<>(new String[] { + "default", + "preferred", + "equal", + "compact" + })); + tabWidthModeField.addActionListener(e -> tabWidthModeChanged()); + tabbedPaneControlPanel.add(tabWidthModeField, "cell 2 3"); + //---- tabsClosableCheckBox ---- tabsClosableCheckBox.setText("Tabs closable"); tabsClosableCheckBox.addActionListener(e -> tabsClosableChanged()); @@ -690,6 +711,7 @@ public class FlatContainerTest private JCheckBox tabIconsCheckBox; private JSpinner tabIconSizeSpinner; private JComboBox tabAreaAlignmentField; + private JComboBox tabWidthModeField; private JCheckBox tabsClosableCheckBox; private JCheckBox customBorderCheckBox; private JCheckBox tabAreaInsetsCheckBox; 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 68b6614b..b5c7003a 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 @@ -285,6 +285,29 @@ new FormModel { }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { "value": "cell 1 3" } ) + add( new FormComponent( "javax.swing.JLabel" ) { + name: "tabWidthModeLabel" + "text": "Tab width mode:" + }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { + "value": "cell 2 3" + } ) + add( new FormComponent( "javax.swing.JComboBox" ) { + name: "tabWidthModeField" + "model": new javax.swing.DefaultComboBoxModel { + selectedItem: "default" + addElement( "default" ) + addElement( "preferred" ) + addElement( "equal" ) + addElement( "compact" ) + } + auxiliary() { + "JavaCodeGenerator.variableLocal": false + "JavaCodeGenerator.typeParameters": "String" + } + addEvent( new FormEvent( "java.awt.event.ActionListener", "actionPerformed", "tabWidthModeChanged", false ) ) + }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { + "value": "cell 2 3" + } ) add( new FormComponent( "javax.swing.JCheckBox" ) { name: "tabsClosableCheckBox" "text": "Tabs closable"