From e2a297fa408ac8f41218ab814c7a0920823c6825 Mon Sep 17 00:00:00 2001 From: Karl Tauber Date: Fri, 30 Oct 2020 01:47:14 +0100 Subject: [PATCH] TabbedPane: support left, right, top and bottom tab icon placement --- CHANGELOG.md | 4 ++ .../formdev/flatlaf/FlatClientProperties.java | 32 +++++++--- .../formdev/flatlaf/ui/FlatTabbedPaneUI.java | 64 +++++++++++++++++-- .../flatlaf/testing/FlatContainerTest.java | 23 +++++++ .../flatlaf/testing/FlatContainerTest.jfd | 17 +++++ 5 files changed, 124 insertions(+), 16 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ce6dba2a..2f9a9123 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -28,6 +28,10 @@ FlatLaf Change Log - TabbedPane: Support equal and compact tab width modes. (set client property `JTabbedPane.tabWidthMode` to `"preferred"`, `"equal"` or `"compact"`) (PR #199) +- TabbedPane: Support left, right, top and bottom tab icon placement. (set + client property `JTabbedPane.tabIconPlacement` to `SwingConstants.LEADING`, + `SwingConstants.TRAILING`, `SwingConstants.TOP` or `SwingConstants.BOTTOM`) + (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 c9b8bc5a..2cbf1ab4 100644 --- a/flatlaf-core/src/main/java/com/formdev/flatlaf/FlatClientProperties.java +++ b/flatlaf-core/src/main/java/com/formdev/flatlaf/FlatClientProperties.java @@ -19,6 +19,7 @@ package com.formdev.flatlaf; import java.awt.Color; import java.util.Objects; import javax.swing.JComponent; +import javax.swing.SwingConstants; /** * @author Karl Tauber @@ -101,7 +102,7 @@ public interface FlatClientProperties /** * Specifies whether the button preferred size will be made square (quadratically). *

- * Components {@link javax.swing.JButton} and {@link javax.swing.JToggleButton} + * Components {@link javax.swing.JButton} and {@link javax.swing.JToggleButton}
* Value type {@link java.lang.Boolean} */ String SQUARE_SIZE = "JButton.squareSize"; @@ -113,7 +114,7 @@ public interface FlatClientProperties *

* Component {@link javax.swing.JButton}, {@link javax.swing.JToggleButton}, * {@link javax.swing.JComboBox}, {@link javax.swing.JSpinner} and {@link javax.swing.text.JTextComponent}
- * Value type {@link java.lang.Integer}
+ * Value type {@link java.lang.Integer} */ String MINIMUM_WIDTH = "JComponent.minimumWidth"; @@ -121,7 +122,7 @@ public interface FlatClientProperties * Specifies minimum height of a component. *

* Component {@link javax.swing.JButton} and {@link javax.swing.JToggleButton}
- * Value type {@link java.lang.Integer}
+ * Value type {@link java.lang.Integer} */ String MINIMUM_HEIGHT = "JComponent.minimumHeight"; @@ -157,7 +158,7 @@ public interface FlatClientProperties * Paint the component with round edges. *

* Components {@link javax.swing.JComboBox}, {@link javax.swing.JSpinner}, - * {@link javax.swing.JTextField}, {@link javax.swing.JFormattedTextField} and {@link javax.swing.JPasswordField} + * {@link javax.swing.JTextField}, {@link javax.swing.JFormattedTextField} and {@link javax.swing.JPasswordField}
* Value type {@link java.lang.Boolean} */ String COMPONENT_ROUND_RECT = "JComponent.roundRect"; @@ -249,7 +250,7 @@ public interface FlatClientProperties /** * Specifies the minimum width of a tab. *

- * Component {@link javax.swing.JTabbedPane}
+ * 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} */ @@ -261,7 +262,7 @@ public interface FlatClientProperties * 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}
+ * 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} */ @@ -287,7 +288,7 @@ public interface FlatClientProperties /** * Specifies the insets of the tab area. *

- * Component {@link javax.swing.JTabbedPane} + * Component {@link javax.swing.JTabbedPane}
* Value type {@link java.awt.Insets} */ String TABBED_PANE_TAB_AREA_INSETS = "JTabbedPane.tabAreaInsets"; @@ -359,7 +360,7 @@ public interface FlatClientProperties * Specifies how to navigate to hidden tabs. *

* Component {@link javax.swing.JTabbedPane}
- * Value type {@link java.lang.String} + * 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} */ @@ -383,7 +384,7 @@ public interface FlatClientProperties * Specifies the alignment of the tab area. *

* Component {@link javax.swing.JTabbedPane}
- * Value type {@link java.lang.String} + * Value type {@link java.lang.String}
* Allowed Values {@link #TABBED_PANE_TAB_AREA_ALIGN_LEADING} (default), * {@link #TABBED_PANE_TAB_AREA_ALIGN_TRAILING}, {@link #TABBED_PANE_TAB_AREA_ALIGN_CENTER} * or {@link #TABBED_PANE_TAB_AREA_ALIGN_FILL} @@ -422,7 +423,7 @@ public interface FlatClientProperties * Specifies how the tabs should be sized. *

* Component {@link javax.swing.JTabbedPane}
- * Value type {@link java.lang.String} + * 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} */ @@ -450,6 +451,17 @@ public interface FlatClientProperties */ String TABBED_PANE_TAB_WIDTH_MODE_COMPACT = "compact"; + /** + * Specifies the tab icon placement (relative to tab title). + *

+ * Component {@link javax.swing.JTabbedPane}
+ * Value type {@link java.lang.Integer}
+ * Allowed Values {@link SwingConstants#LEADING} (default), + * {@link SwingConstants#TRAILING}, {@link SwingConstants#TOP} + * or {@link SwingConstants#BOTTOM} + */ + String TABBED_PANE_TAB_ICON_PLACEMENT = "JTabbedPane.tabIconPlacement"; + /** * 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 ff9baeb5..95023fad 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 @@ -563,8 +563,29 @@ public class FlatTabbedPaneUI { Insets tabInsets = getTabInsets( tabPlacement, tabIndex ); tabWidth = icon.getIconWidth() + tabInsets.left + tabInsets.right; - } else - tabWidth = super.calculateTabWidth( tabPlacement, tabIndex, metrics ) - 3 /* was added by superclass */; + } else { + int iconPlacement = clientPropertyInt( tabPane, TABBED_PANE_TAB_ICON_PLACEMENT, LEADING ); + if( (iconPlacement == TOP || iconPlacement == BOTTOM) && + tabPane.getTabComponentAt( tabIndex ) == null && + (icon = getIconForTab( tabIndex )) != null ) + { + // TOP and BOTTOM icon placement + tabWidth = icon.getIconWidth(); + + View view = getTextViewForTab( tabIndex ); + if( view != null ) + tabWidth = Math.max( tabWidth, (int) view.getPreferredSpan( View.X_AXIS ) ); + else { + String title = tabPane.getTitleAt( tabIndex ); + if( title != null ) + tabWidth = Math.max( tabWidth, metrics.stringWidth( title ) ); + } + + Insets tabInsets = getTabInsets( tabPlacement, tabIndex ); + tabWidth += tabInsets.left + tabInsets.right; + } else + tabWidth = super.calculateTabWidth( tabPlacement, tabIndex, metrics ) - 3 /* was added by superclass */; + } // make tab wider if closable if( isTabClosable( tabIndex ) ) @@ -583,8 +604,29 @@ public class FlatTabbedPaneUI @Override protected int calculateTabHeight( int tabPlacement, int tabIndex, int fontHeight ) { - int tabHeight = scale( clientPropertyInt( tabPane, TABBED_PANE_TAB_HEIGHT, this.tabHeight ) ); - return Math.max( tabHeight, super.calculateTabHeight( tabPlacement, tabIndex, fontHeight ) - 2 /* was added by superclass */ ); + int tabHeight; + + Icon icon; + int iconPlacement = clientPropertyInt( tabPane, TABBED_PANE_TAB_ICON_PLACEMENT, LEADING ); + if( (iconPlacement == TOP || iconPlacement == BOTTOM) && + tabPane.getTabComponentAt( tabIndex ) == null && + (icon = getIconForTab( tabIndex )) != null ) + { + // TOP and BOTTOM icon placement + tabHeight = icon.getIconHeight(); + + View view = getTextViewForTab( tabIndex ); + if( view != null ) + tabHeight += (int) view.getPreferredSpan( View.Y_AXIS ) + scale( textIconGapUnscaled ); + else if( tabPane.getTitleAt( tabIndex ) != null ) + tabHeight += fontHeight + scale( textIconGapUnscaled ); + + Insets tabInsets = getTabInsets( tabPlacement, tabIndex ); + tabHeight += tabInsets.top + tabInsets.bottom; + } else + tabHeight = super.calculateTabHeight( tabPlacement, tabIndex, fontHeight ) - 2 /* was added by superclass */; + + return Math.max( tabHeight, scale( clientPropertyInt( tabPane, TABBED_PANE_TAB_HEIGHT, this.tabHeight ) ) ); } @Override @@ -988,6 +1030,16 @@ public class FlatTabbedPaneUI tabRect.x += closeIcon.getIconWidth(); } + // icon placement + int iconPlacement = clientPropertyInt( tabPane, TABBED_PANE_TAB_ICON_PLACEMENT, LEADING ); + int verticalTextPosition = CENTER; + int horizontalTextPosition = TRAILING; + switch( iconPlacement ) { + case TRAILING: horizontalTextPosition = LEADING; break; + case TOP: verticalTextPosition = BOTTOM; horizontalTextPosition = CENTER; break; + case BOTTOM: verticalTextPosition = TOP; horizontalTextPosition = CENTER; break; + } + // reset rectangles textRect.setBounds( 0, 0, 0, 0 ); iconRect.setBounds( 0, 0, 0, 0 ); @@ -999,8 +1051,7 @@ public class FlatTabbedPaneUI // layout label String clippedTitle = SwingUtilities.layoutCompoundLabel( tabPane, metrics, title, icon, - SwingUtilities.CENTER, SwingUtilities.CENTER, - SwingUtilities.CENTER, SwingUtilities.TRAILING, + CENTER, CENTER, verticalTextPosition, horizontalTextPosition, tabRect, iconRect, textRect, scale( textIconGapUnscaled ) ); // remove temporary client property @@ -1945,6 +1996,7 @@ public class FlatTabbedPaneUI case TABBED_PANE_HIDDEN_TABS_NAVIGATION: case TABBED_PANE_TAB_AREA_ALIGNMENT: case TABBED_PANE_TAB_WIDTH_MODE: + case TABBED_PANE_TAB_ICON_PLACEMENT: 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 80d935fd..9dcf9194 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 @@ -176,6 +176,17 @@ public class FlatContainerTest tabbedPane.setIconAt( i, icon ); } + private void iconPlacementChanged() { + Object iconPlacement = null; + switch( (String) iconPlacementField.getSelectedItem() ) { + case "leading": iconPlacement = SwingConstants.LEADING; break; + case "trailing": iconPlacement = SwingConstants.TRAILING; break; + case "top": iconPlacement = SwingConstants.TOP; break; + case "bottom": iconPlacement = SwingConstants.BOTTOM; break; + } + putTabbedPanesClientProperty( TABBED_PANE_TAB_ICON_PLACEMENT, iconPlacement ); + } + private void customBorderChanged() { Border border = customBorderCheckBox.isSelected() ? new MatteBorder( 10, 20, 25, 35, Color.green ) @@ -390,6 +401,7 @@ public class FlatContainerTest tabPlacementField = new JComboBox<>(); tabIconsCheckBox = new JCheckBox(); tabIconSizeSpinner = new JSpinner(); + iconPlacementField = new JComboBox<>(); JLabel tabAreaAlignmentLabel = new JLabel(); tabAreaAlignmentField = new JComboBox<>(); JLabel tabWidthModeLabel = new JLabel(); @@ -592,6 +604,16 @@ public class FlatContainerTest tabIconSizeSpinner.addChangeListener(e -> tabIconsChanged()); tabbedPaneControlPanel.add(tabIconSizeSpinner, "cell 2 2"); + //---- iconPlacementField ---- + iconPlacementField.setModel(new DefaultComboBoxModel<>(new String[] { + "leading", + "trailing", + "top", + "bottom" + })); + iconPlacementField.addActionListener(e -> iconPlacementChanged()); + tabbedPaneControlPanel.add(iconPlacementField, "cell 2 2"); + //---- tabAreaAlignmentLabel ---- tabAreaAlignmentLabel.setText("Tab area alignment:"); tabbedPaneControlPanel.add(tabAreaAlignmentLabel, "cell 0 3"); @@ -710,6 +732,7 @@ public class FlatContainerTest private JComboBox tabPlacementField; private JCheckBox tabIconsCheckBox; private JSpinner tabIconSizeSpinner; + private JComboBox iconPlacementField; private JComboBox tabAreaAlignmentField; private JComboBox tabWidthModeField; private JCheckBox tabsClosableCheckBox; 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 b5c7003a..050f03e4 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 @@ -261,6 +261,23 @@ new FormModel { }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { "value": "cell 2 2" } ) + add( new FormComponent( "javax.swing.JComboBox" ) { + name: "iconPlacementField" + "model": new javax.swing.DefaultComboBoxModel { + selectedItem: "leading" + addElement( "leading" ) + addElement( "trailing" ) + addElement( "top" ) + addElement( "bottom" ) + } + auxiliary() { + "JavaCodeGenerator.variableLocal": false + "JavaCodeGenerator.typeParameters": "String" + } + addEvent( new FormEvent( "java.awt.event.ActionListener", "actionPerformed", "iconPlacementChanged", false ) ) + }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { + "value": "cell 2 2" + } ) add( new FormComponent( "javax.swing.JLabel" ) { name: "tabAreaAlignmentLabel" "text": "Tab area alignment:"