diff --git a/CHANGELOG.md b/CHANGELOG.md index a8482898..64d3c4be 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -19,6 +19,8 @@ FlatLaf Change Log `JTabbedPane.trailingComponent` to a `java.awt.Component`) (PR #192; issue #40) - 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) - 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 f604288a..5a9bb722 100644 --- a/flatlaf-core/src/main/java/com/formdev/flatlaf/FlatClientProperties.java +++ b/flatlaf-core/src/main/java/com/formdev/flatlaf/FlatClientProperties.java @@ -246,6 +246,27 @@ public interface FlatClientProperties */ String TABBED_PANE_HAS_FULL_BORDER = "JTabbedPane.hasFullBorder"; + /** + * Specifies the minimum width of a tab. + *

+ * Component {@link javax.swing.JTabbedPane}
+ * or tab content components (see {@link javax.swing.JTabbedPane#setComponentAt(int, java.awt.Component)})
+ * Value type {@link java.lang.Integer} + */ + String TABBED_PANE_MINIMUM_TAB_WIDTH = "JTabbedPane.minimumTabWidth"; + + /** + * Specifies the maximum width of a tab. + *

+ * Applied only if tab does not have a custom tab component + * (see {@link javax.swing.JTabbedPane#setTabComponentAt(int, java.awt.Component)}). + *

+ * Component {@link javax.swing.JTabbedPane}
+ * or tab content components (see {@link javax.swing.JTabbedPane#setComponentAt(int, java.awt.Component)})
+ * Value type {@link java.lang.Integer} + */ + String TABBED_PANE_MAXIMUM_TAB_WIDTH = "JTabbedPane.maximumTabWidth"; + /** * Specifies the height of a tab. *

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 20447dae..1c2a8d37 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 @@ -118,6 +118,8 @@ import com.formdev.flatlaf.util.UIScale; * @uiDefault TabbedPane.focusColor Color * @uiDefault TabbedPane.tabSeparatorColor Color optional; defaults to TabbedPane.contentAreaColor * @uiDefault TabbedPane.contentAreaColor Color + * @uiDefault TabbedPane.minimumTabWidth int optional + * @uiDefault TabbedPane.maximumTabWidth int optional * @uiDefault TabbedPane.tabHeight int * @uiDefault TabbedPane.tabSelectionHeight int * @uiDefault TabbedPane.contentSeparatorHeight int @@ -153,12 +155,15 @@ public class FlatTabbedPaneUI protected Color contentAreaColor; private int textIconGapUnscaled; + protected int minimumTabWidth; + protected int maximumTabWidth; protected int tabHeight; protected int tabSelectionHeight; protected int contentSeparatorHeight; protected boolean showTabSeparators; protected boolean tabSeparatorsFullHeight; protected boolean hasFullBorder; + protected boolean tabsOpaque = true; private String hiddenTabsNavigationStr; protected Icon closeIcon; @@ -217,12 +222,15 @@ public class FlatTabbedPaneUI contentAreaColor = UIManager.getColor( "TabbedPane.contentAreaColor" ); textIconGapUnscaled = UIManager.getInt( "TabbedPane.textIconGap" ); + minimumTabWidth = UIManager.getInt( "TabbedPane.minimumTabWidth" ); + maximumTabWidth = UIManager.getInt( "TabbedPane.maximumTabWidth" ); tabHeight = UIManager.getInt( "TabbedPane.tabHeight" ); tabSelectionHeight = UIManager.getInt( "TabbedPane.tabSelectionHeight" ); contentSeparatorHeight = UIManager.getInt( "TabbedPane.contentSeparatorHeight" ); showTabSeparators = UIManager.getBoolean( "TabbedPane.showTabSeparators" ); tabSeparatorsFullHeight = UIManager.getBoolean( "TabbedPane.tabSeparatorsFullHeight" ); hasFullBorder = UIManager.getBoolean( "TabbedPane.hasFullBorder" ); + tabsOpaque = UIManager.getBoolean( "TabbedPane.tabsOpaque" ); hiddenTabsNavigationStr = UIManager.getString( "TabbedPane.hiddenTabsNavigation" ); closeIcon = UIManager.getIcon( "TabbedPane.closeIcon" ); @@ -518,8 +526,19 @@ public class FlatTabbedPaneUI textIconGap = scale( textIconGapUnscaled ); int tabWidth = super.calculateTabWidth( tabPlacement, tabIndex, metrics ) - 3 /* was added by superclass */; + + // make tab wider if closable if( isTabClosable( tabIndex ) ) tabWidth += closeIcon.getIconWidth(); + + // apply minimum and maximum tab width + int min = getTabClientPropertyInt( tabIndex, TABBED_PANE_MINIMUM_TAB_WIDTH, minimumTabWidth ); + int max = getTabClientPropertyInt( tabIndex, TABBED_PANE_MAXIMUM_TAB_WIDTH, maximumTabWidth ); + if( min > 0 ) + tabWidth = Math.max( tabWidth, scale( min ) ); + if( max > 0 && tabPane.getTabComponentAt( tabIndex ) == null ) + tabWidth = Math.min( tabWidth, scale( max ) ); + return tabWidth; } @@ -649,6 +668,46 @@ public class FlatTabbedPaneUI paintTabArea( g, tabPlacement, selectedIndex ); } + @Override + protected void paintTab( Graphics g, int tabPlacement, Rectangle[] rects, + int tabIndex, Rectangle iconRect, Rectangle textRect ) + { + Rectangle tabRect = rects[tabIndex]; + boolean isSelected = (tabIndex == tabPane.getSelectedIndex()); + + // paint background + if( tabsOpaque || tabPane.isOpaque() ) + paintTabBackground( g, tabPlacement, tabIndex, tabRect.x, tabRect.y, tabRect.width, tabRect.height, isSelected ); + + // paint border + paintTabBorder( g, tabPlacement, tabIndex, tabRect.x, tabRect.y, tabRect.width, tabRect.height, isSelected ); + + if( tabPane.getTabComponentAt( tabIndex ) != null ) + return; + + // layout title and icon + String title = tabPane.getTitleAt( tabIndex ); + Icon icon = getIconForTab( tabIndex ); + Font font = tabPane.getFont(); + FontMetrics metrics = tabPane.getFontMetrics( font ); + 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 + if( 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 ) + clippedTitle = JavaCompatibility.getClippedString( null, metrics, title, r.width ); + } + } + + // paint title and icon + paintText( g, tabPlacement, font, metrics, tabIndex, clippedTitle, textRect, isSelected ); + paintIcon( g, tabPlacement, tabIndex, icon, iconRect, isSelected ); + } + @Override protected void paintText( Graphics g, int tabPlacement, Font font, FontMetrics metrics, int tabIndex, String title, Rectangle textRect, boolean isSelected ) @@ -662,20 +721,6 @@ public class FlatTabbedPaneUI return; } - // clip title if our layout manager is used - // (normally this is done by invoker, but fails in this case) - if( 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 ) ) { @@ -898,14 +943,37 @@ public class FlatTabbedPaneUI { } - @Override - protected void layoutLabel( int tabPlacement, FontMetrics metrics, int tabIndex, String title, Icon icon, - Rectangle tabRect, Rectangle iconRect, Rectangle textRect, boolean isSelected ) + protected String layoutAndClipLabel( int tabPlacement, FontMetrics metrics, int tabIndex, + String title, Icon icon, Rectangle tabRect, Rectangle iconRect, Rectangle textRect, boolean isSelected ) { - // update textIconGap before used in super class - textIconGap = scale( textIconGapUnscaled ); + // remove tab insets and space for close button from the tab rectangle + // to get correctly clipped title + tabRect = FlatUIUtils.subtractInsets( tabRect, getTabInsets( tabPlacement, tabIndex ) ); + if( isTabClosable( tabIndex ) ) { + tabRect.width -= closeIcon.getIconWidth(); + if( !isLeftToRight() ) + tabRect.x += closeIcon.getIconWidth(); + } - super.layoutLabel( tabPlacement, metrics, tabIndex, title, icon, tabRect, iconRect, textRect, isSelected ); + // reset rectangles + textRect.setBounds( 0, 0, 0, 0 ); + iconRect.setBounds( 0, 0, 0, 0 ); + + // temporary set "html" client property on tabbed pane, which is used by SwingUtilities.layoutCompoundLabel() + View view = getTextViewForTab( tabIndex ); + if( view != null ) + tabPane.putClientProperty( "html", view ); + + // layout label + String clippedTitle = SwingUtilities.layoutCompoundLabel( tabPane, metrics, title, icon, + SwingUtilities.CENTER, SwingUtilities.CENTER, + SwingUtilities.CENTER, SwingUtilities.TRAILING, + tabRect, iconRect, textRect, scale( textIconGapUnscaled ) ); + + // remove temporary client property + tabPane.putClientProperty( "html", null ); + + return clippedTitle; } @Override @@ -993,6 +1061,11 @@ public class FlatTabbedPaneUI return tabPane.getClientProperty( key ); } + protected int getTabClientPropertyInt( int tabIndex, String key, int defaultValue ) { + Object value = getTabClientProperty( tabIndex, key ); + return (value instanceof Integer) ? (int) value : defaultValue; + } + protected void ensureCurrentLayout() { // since super.ensureCurrentLayout() is private, // use super.getTabRunCount() as workaround @@ -1718,6 +1791,8 @@ public class FlatTabbedPaneUI case TABBED_PANE_SHOW_TAB_SEPARATORS: case TABBED_PANE_SHOW_CONTENT_SEPARATOR: case TABBED_PANE_HAS_FULL_BORDER: + case TABBED_PANE_MINIMUM_TAB_WIDTH: + case TABBED_PANE_MAXIMUM_TAB_WIDTH: case TABBED_PANE_TAB_HEIGHT: case TABBED_PANE_TAB_INSETS: case TABBED_PANE_HIDDEN_TABS_NAVIGATION: @@ -1757,6 +1832,8 @@ public class FlatTabbedPaneUI protected void contentPropertyChange( PropertyChangeEvent e ) { switch( e.getPropertyName() ) { + case TABBED_PANE_MINIMUM_TAB_WIDTH: + case TABBED_PANE_MAXIMUM_TAB_WIDTH: case TABBED_PANE_TAB_INSETS: case TABBED_PANE_TAB_CLOSABLE: tabPane.revalidate(); 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 79ebdb5b..dd707824 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 @@ -323,6 +323,16 @@ public class FlatContainerTest } } + private void minimumTabWidthChanged() { + Integer minimumTabWidth = minimumTabWidthCheckBox.isSelected() ? 100 : null; + putTabbedPanesClientProperty( TABBED_PANE_MINIMUM_TAB_WIDTH, minimumTabWidth ); + } + + private void maximumTabWidthChanged() { + Integer maximumTabWidth = maximumTabWidthCheckBox.isSelected() ? 60 : null; + putTabbedPanesClientProperty( TABBED_PANE_MAXIMUM_TAB_WIDTH, maximumTabWidth ); + } + private void initComponents() { // JFormDesigner - Component initialization - DO NOT MODIFY //GEN-BEGIN:initComponents JPanel panel9 = new JPanel(); @@ -365,6 +375,8 @@ public class FlatContainerTest trailingComponentCheckBox = new JCheckBox(); showTabSeparatorsCheckBox = new JCheckBox(); secondTabWiderCheckBox = new JCheckBox(); + minimumTabWidthCheckBox = new JCheckBox(); + maximumTabWidthCheckBox = new JCheckBox(); CellConstraints cc = new CellConstraints(); //======== this ======== @@ -480,6 +492,8 @@ public class FlatContainerTest "[]" + "[]para" + "[]" + + "[]para" + + "[]" + "[]")); //---- tabScrollCheckBox ---- @@ -605,6 +619,16 @@ public class FlatContainerTest secondTabWiderCheckBox.setText("Second Tab insets wider (4,20,4,20)"); secondTabWiderCheckBox.addActionListener(e -> secondTabWiderChanged()); tabbedPaneControlPanel.add(secondTabWiderCheckBox, "cell 2 6"); + + //---- minimumTabWidthCheckBox ---- + minimumTabWidthCheckBox.setText("Minimum tab width (100)"); + minimumTabWidthCheckBox.addActionListener(e -> minimumTabWidthChanged()); + tabbedPaneControlPanel.add(minimumTabWidthCheckBox, "cell 2 7"); + + //---- maximumTabWidthCheckBox ---- + maximumTabWidthCheckBox.setText("Maximum tab width (60)"); + maximumTabWidthCheckBox.addActionListener(e -> maximumTabWidthChanged()); + tabbedPaneControlPanel.add(maximumTabWidthCheckBox, "cell 2 8"); } panel9.add(tabbedPaneControlPanel, cc.xywh(1, 11, 3, 1)); } @@ -637,6 +661,8 @@ public class FlatContainerTest private JCheckBox trailingComponentCheckBox; private JCheckBox showTabSeparatorsCheckBox; private JCheckBox secondTabWiderCheckBox; + private JCheckBox minimumTabWidthCheckBox; + private JCheckBox maximumTabWidthCheckBox; // 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 789f156b..0f7cb0c5 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( "com.formdev.flatlaf.testing.FlatTestFrame$NoRightToLeftPanel", new FormLayoutManager( class net.miginfocom.swing.MigLayout ) { "$layoutConstraints": "insets 0,hidemode 3" "$columnConstraints": "[][fill][]" - "$rowConstraints": "[center][][]para[][]para[][]" + "$rowConstraints": "[center][][]para[][]para[][]para[][]" } ) { name: "tabbedPaneControlPanel" "opaque": false @@ -381,6 +381,26 @@ new FormModel { }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { "value": "cell 2 6" } ) + add( new FormComponent( "javax.swing.JCheckBox" ) { + name: "minimumTabWidthCheckBox" + "text": "Minimum tab width (100)" + auxiliary() { + "JavaCodeGenerator.variableLocal": false + } + addEvent( new FormEvent( "java.awt.event.ActionListener", "actionPerformed", "minimumTabWidthChanged", false ) ) + }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { + "value": "cell 2 7" + } ) + add( new FormComponent( "javax.swing.JCheckBox" ) { + name: "maximumTabWidthCheckBox" + "text": "Maximum tab width (60)" + auxiliary() { + "JavaCodeGenerator.variableLocal": false + } + addEvent( new FormEvent( "java.awt.event.ActionListener", "actionPerformed", "maximumTabWidthChanged", false ) ) + }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { + "value": "cell 2 8" + } ) }, new FormLayoutConstraints( class com.jgoodies.forms.layout.CellConstraints ) { "gridY": 11 "gridWidth": 3