From 10746a454a22c852423c4e6bca9dbb1bb5688b28 Mon Sep 17 00:00:00 2001 From: Karl Tauber Date: Sat, 17 Oct 2020 11:30:04 +0200 Subject: [PATCH 1/4] TabbedPane: support adding custom components to left and right sides of tabs area if "more tabs" button is used (issue #40) --- CHANGELOG.md | 3 + .../formdev/flatlaf/FlatClientProperties.java | 16 + .../formdev/flatlaf/ui/FlatTabbedPaneUI.java | 336 ++++++++++++++---- .../flatlaf/testing/FlatContainerTest.java | 37 ++ .../flatlaf/testing/FlatContainerTest.jfd | 22 +- 5 files changed, 340 insertions(+), 74 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 57c98ef3..023c94a9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,9 @@ FlatLaf Change Log - 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) +- TabbedPane: Support adding custom components to left and right sides of tabs + area. (set client property `JTabbedPane.leadingComponent` or + `JTabbedPane.trailingComponent` to a `java.awt.Component`) (issue #40) - Support painting separator line between window title and content (use UI value `TitlePane.borderColor`). (issue #184) 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 3ad1dd8f..974d7a85 100644 --- a/flatlaf-core/src/main/java/com/formdev/flatlaf/FlatClientProperties.java +++ b/flatlaf-core/src/main/java/com/formdev/flatlaf/FlatClientProperties.java @@ -264,6 +264,22 @@ public interface FlatClientProperties */ String TABBED_PANE_HIDDEN_TABS_NAVIGATION_ARROW_BUTTONS = "arrowButtons"; + /** + * Specifies a component that will be placed at the leading edge of the tabs area. + *

+ * Component {@link javax.swing.JTabbedPane}
+ * Value type {@link java.awt.Component} + */ + String TABBED_PANE_LEADING_COMPONENT = "JTabbedPane.leadingComponent"; + + /** + * Specifies a component that will be placed at the trailing edge of the tabs area. + *

+ * Component {@link javax.swing.JTabbedPane}
+ * Value type {@link java.awt.Component} + */ + String TABBED_PANE_TRAILING_COMPONENT = "JTabbedPane.trailingComponent"; + /** * 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 0f77cfc2..e7104fde 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 @@ -18,6 +18,7 @@ package com.formdev.flatlaf.ui; import static com.formdev.flatlaf.util.UIScale.scale; import static com.formdev.flatlaf.FlatClientProperties.*; +import java.awt.BorderLayout; import java.awt.Color; import java.awt.Component; import java.awt.Container; @@ -53,6 +54,7 @@ import java.util.Set; import javax.swing.JButton; import javax.swing.JComponent; import javax.swing.JMenuItem; +import javax.swing.JPanel; import javax.swing.JPopupMenu; import javax.swing.JTabbedPane; import javax.swing.JViewport; @@ -155,6 +157,8 @@ public class FlatTabbedPaneUI protected FlatWheelTabScroller wheelTabScroller; private JButton moreTabsButton; + private Container leadingComponent; + private Container trailingComponent; private Handler handler; private boolean blockRollover; @@ -303,6 +307,9 @@ public class FlatTabbedPaneUI // create and add "more tabs" button moreTabsButton = createMoreTabsButton(); tabPane.add( moreTabsButton ); + + installLeadingComponent(); + installTrailingComponent(); } protected void uninstallHiddenTabsNavigation() { @@ -315,6 +322,39 @@ public class FlatTabbedPaneUI tabPane.remove( moreTabsButton ); moreTabsButton = null; } + + uninstallLeadingComponent(); + uninstallTrailingComponent(); + } + + protected void installLeadingComponent() { + Object c = tabPane.getClientProperty( TABBED_PANE_LEADING_COMPONENT ); + if( c instanceof Component ) { + leadingComponent = new ContainerUIResource( (Component) c ); + tabPane.add( leadingComponent ); + } + } + + protected void uninstallLeadingComponent() { + if( leadingComponent != null ) { + tabPane.remove( leadingComponent ); + leadingComponent = null; + } + } + + protected void installTrailingComponent() { + Object c = tabPane.getClientProperty( TABBED_PANE_TRAILING_COMPONENT ); + if( c instanceof Component ) { + trailingComponent = new ContainerUIResource( (Component) c ); + tabPane.add( trailingComponent ); + } + } + + protected void uninstallTrailingComponent() { + if( trailingComponent != null ) { + tabPane.remove( trailingComponent ); + trailingComponent = null; + } } @Override @@ -776,6 +816,11 @@ public class FlatTabbedPaneUI return tabPane.getTabLayoutPolicy() == JTabbedPane.SCROLL_TAB_LAYOUT; } + protected boolean isHorizontalTabPlacement() { + int tabPlacement = tabPane.getTabPlacement(); + return tabPlacement == TOP || tabPlacement == BOTTOM; + } + protected boolean isSmoothScrollingEnabled() { if( !Animator.useAnimation() ) return false; @@ -809,6 +854,12 @@ public class FlatTabbedPaneUI runnable.run(); } + protected void ensureSelectedTabIsVisibleLater() { + EventQueue.invokeLater( () -> { + ensureSelectedTabIsVisible(); + } ); + } + protected void ensureSelectedTabIsVisible() { if( tabPane == null || tabViewport == null ) return; @@ -822,6 +873,18 @@ public class FlatTabbedPaneUI ((JComponent)tabViewport.getView()).scrollRectToVisible( (Rectangle) rects[selectedIndex].clone() ); } + //---- class ContainerUIResource ------------------------------------------ + + private class ContainerUIResource + extends JPanel + implements UIResource + { + private ContainerUIResource( Component c ) { + super( new BorderLayout() ); + add( c ); + } + } + //---- class FlatMoreTabsButton ------------------------------------------- protected class FlatMoreTabsButton @@ -855,6 +918,16 @@ public class FlatTabbedPaneUI setDirection( direction ); } + @Override + public Dimension getPreferredSize() { + Dimension size = super.getPreferredSize(); + boolean horizontal = (direction == SOUTH || direction == NORTH); + int margin = scale( 8 ); + return new Dimension( + size.width + (horizontal ? margin : 0), + size.height + (horizontal ? 0 : margin) ); + } + @Override public void paint( Graphics g ) { // paint arrow button near separator line @@ -979,21 +1052,11 @@ 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( - 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 ); - } + boolean horizontal = (direction == WEST || direction == EAST); + return new Dimension( + horizontal ? size.width : Math.max( size.width, maxTabWidth ), + horizontal ? Math.max( size.height, maxTabHeight ) : size.height ); } @Override @@ -1315,6 +1378,10 @@ public class FlatTabbedPaneUI ((FlatMoreTabsButton)moreTabsButton).updateDirection(); break; + case "componentOrientation": + ensureSelectedTabIsVisibleLater(); + break; + case TABBED_PANE_SHOW_TAB_SEPARATORS: case TABBED_PANE_SHOW_CONTENT_SEPARATOR: case TABBED_PANE_HAS_FULL_BORDER: @@ -1328,6 +1395,22 @@ public class FlatTabbedPaneUI installHiddenTabsNavigation(); tabPane.repaint(); break; + + case TABBED_PANE_LEADING_COMPONENT: + uninstallLeadingComponent(); + installLeadingComponent(); + tabPane.revalidate(); + tabPane.repaint(); + ensureSelectedTabIsVisibleLater(); + break; + + case TABBED_PANE_TRAILING_COMPONENT: + uninstallTrailingComponent(); + installTrailingComponent(); + tabPane.revalidate(); + tabPane.repaint(); + ensureSelectedTabIsVisibleLater(); + break; } } @@ -1347,9 +1430,7 @@ public class FlatTabbedPaneUI @Override public void componentResized( ComponentEvent e ) { // make sure that selected tab stays visible when component size changed - EventQueue.invokeLater( () -> { - ensureSelectedTabIsVisible(); - } ); + ensureSelectedTabIsVisibleLater(); } @Override public void componentMoved( ComponentEvent e ) {} @@ -1397,12 +1478,32 @@ public class FlatTabbedPaneUI @Override public Dimension preferredLayoutSize( Container parent ) { - return delegate.preferredLayoutSize( parent ); + Dimension size = delegate.preferredLayoutSize( parent ); + size = addLayoutSize( size, leadingComponent, false ); + size = addLayoutSize( size, trailingComponent, false ); + return size; } @Override public Dimension minimumLayoutSize( Container parent ) { - return delegate.minimumLayoutSize( parent ); + Dimension size = delegate.minimumLayoutSize( parent ); + size = addLayoutSize( size, leadingComponent, true ); + size = addLayoutSize( size, trailingComponent, true ); + return size; + } + + private Dimension addLayoutSize( Dimension size, Container c, boolean minimum ) { + if( c == null ) + return size; + + Dimension compSize = minimum ? c.getMinimumSize() : c.getPreferredSize(); + + size = (Dimension) size.clone(); + if( isHorizontalTabPlacement() ) + size.width += compSize.width; + else + size.height += compSize.height; + return size; } @Override @@ -1415,69 +1516,158 @@ public class FlatTabbedPaneUI 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; + // hide scroll buttons 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 + if( c instanceof FlatScrollableTabButton && c.isVisible() ) 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() + Rectangle bounds = tabPane.getBounds(); + Insets insets = tabPane.getInsets(); + int tabPlacement = tabPane.getTabPlacement(); + Insets tabAreaInsets = getTabAreaInsets( tabPlacement ); + Rectangle lastRect = rects[rects.length - 1]; + Dimension moreButtonSize = moreTabsButton.getPreferredSize(); + boolean leftToRight = tabPane.getComponentOrientation().isLeftToRight(); + boolean moreTabsButtonVisible = false; - // 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; + // TabbedPaneScrollLayout adds tabAreaInsets to tab coordinates, + // but we use it to position the viewport + if( tabAreaInsets.left != 0 || tabAreaInsets.top != 0 ) { + // remove tabAreaInsets from tab locations + shiftTabs( -tabAreaInsets.left, -tabAreaInsets.top ); - // 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; - } + // reduce preferred size of view + Component view = tabViewport.getView(); + Dimension viewSize = view.getPreferredSize(); + boolean horizontal = (tabPlacement == TOP || tabPlacement == BOTTOM); + view.setPreferredSize( new Dimension( + viewSize.width - (horizontal ? tabAreaInsets.left : 0), + viewSize.height - (horizontal ? 0 : tabAreaInsets.top) ) ); } - // show/hide "more tabs" button and layout it + // layout tab area + if( tabPlacement == TOP || tabPlacement == BOTTOM ) { + // tab area bounds + int tx = insets.left; + int ty = (tabPlacement == TOP) + ? insets.top + : (bounds.height - insets.bottom - maxTabHeight - tabAreaInsets.top - tabAreaInsets.bottom); + int tw = bounds.width - insets.left - insets.right; + int th = maxTabHeight; + + int tyi = ty + tabAreaInsets.top; + + // layout left component + Container leftComponent = leftToRight ? leadingComponent : trailingComponent; + if( leftComponent != null ) { + Dimension leftSize = leftComponent.getPreferredSize(); + leftComponent.setBounds( tx, tyi, leftSize.width, th ); + + // reduce tab area bounds + tx += leftSize.width; + tw -= leftSize.width; + } + + // layout right component + Container rightComponent = leftToRight ? trailingComponent : leadingComponent; + if( rightComponent != null ) { + Dimension rightSize = rightComponent.getPreferredSize(); + rightComponent.setBounds( tx + tw - rightSize.width, tyi, rightSize.width, th ); + + // reduce tab area bounds + tw -= rightSize.width; + } + + int txi = tx + tabAreaInsets.left; + int twi = tw - tabAreaInsets.left - tabAreaInsets.right; + + // layout viewport and "more tabs" button + int viewportWidth = twi; + int tabsWidth = leftToRight + ? (lastRect.x + lastRect.width) + : (rects[0].x + rects[0].width - lastRect.x); + if( viewportWidth < tabsWidth ) { + // need "more tabs" button + viewportWidth = Math.max( viewportWidth - moreButtonSize.width, 0 ); + + moreTabsButton.setBounds( leftToRight ? (txi + twi - moreButtonSize.width) : txi, tyi, moreButtonSize.width, th ); + tabViewport.setBounds( leftToRight ? txi : (txi + moreButtonSize.width), tyi, viewportWidth, th ); + moreTabsButtonVisible = true; + } else + tabViewport.setBounds( txi, tyi, viewportWidth, th ); + + if( !leftToRight ) { + // layout viewport so that we can get correct view width below + tabViewport.doLayout(); + + // fix x-locations of tabs so that they are right-aligned in the view + shiftTabs( tabViewport.getView().getWidth() - (rects[0].x + rects[0].width), 0 ); + } + } else { // LEFT, RIGHT + // tab area bounds + int tx = (tabPlacement == LEFT) + ? insets.left + : (bounds.width - insets.right - maxTabWidth - tabAreaInsets.left - tabAreaInsets.right); + int ty = insets.top; + int tw = maxTabWidth; + int th = bounds.height - insets.top - insets.bottom; + + int txi = tx + tabAreaInsets.left; + + // layout top component + if( leadingComponent != null ) { + Dimension topSize = leadingComponent.getPreferredSize(); + leadingComponent.setBounds( txi, ty, tw, topSize.height ); + + // reduce tab area bounds + ty += topSize.height; + th -= topSize.height; + } + + // layout bottom component + if( trailingComponent != null ) { + Dimension bottomSize = trailingComponent.getPreferredSize(); + trailingComponent.setBounds( txi, ty + th - bottomSize.height, tw, bottomSize.height ); + + // reduce tab area bounds + th -= bottomSize.height; + } + + int tyi = ty + tabAreaInsets.top; + int thi = th - tabAreaInsets.top - tabAreaInsets.bottom; + + // layout viewport and "more tabs" button + int viewportHeight = thi; + int tabsHeight = lastRect.y + lastRect.height; + if( viewportHeight < tabsHeight ) { + // need "more tabs" button + viewportHeight = Math.max( viewportHeight - moreButtonSize.height, 0 ); + + moreTabsButton.setBounds( txi, tyi + thi - moreButtonSize.height, tw, moreButtonSize.height ); + moreTabsButtonVisible = true; + } + tabViewport.setBounds( txi, tyi, tw, viewportHeight ); + } + + // show/hide "more tabs" button moreTabsButton.setVisible( moreTabsButtonVisible ); - if( buttonsBounds != null ) - moreTabsButton.setBounds( buttonsBounds ); + } + + private void shiftTabs( int sx, int sy ) { + if( sx == 0 && sy == 0 ) + return; + + for( int i = 0; i < rects.length; i++ ) { + // fix x location in rects + rects[i].x += sx; + rects[i].y += sy; + + // fix tab component location + Component c = tabPane.getTabComponentAt( i ); + if( c != null ) + c.setLocation( c.getX() + sx, c.getY() + sy ); + } } } } 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 53cb920c..d7386af4 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 @@ -248,6 +248,28 @@ public class FlatContainerTest tabbedPane1.setForegroundAt( 1, enabled ? Color.red : null ); } + private void leadingComponentChanged() { + leadingTrailingComponentChanged( leadingComponentCheckBox.isSelected(), TABBED_PANE_LEADING_COMPONENT, "Leading", 4 ); + } + + private void trailingComponentChanged() { + leadingTrailingComponentChanged( trailingComponentCheckBox.isSelected(), TABBED_PANE_TRAILING_COMPONENT, "Trailing", 12 ); + } + + private void leadingTrailingComponentChanged( boolean enabled, String key, String text, int gap ) { + JTabbedPane[] tabbedPanes = new JTabbedPane[] { tabbedPane1, tabbedPane2, tabbedPane3, tabbedPane4 }; + for( JTabbedPane tabbedPane : tabbedPanes ) { + JComponent c = null; + if( enabled ) { + c = new JLabel( text ); + c.setOpaque( true ); + c.setBackground( Color.cyan ); + c.setBorder( new EmptyBorder( gap, gap, gap, gap ) ); + } + tabbedPane.putClientProperty( key, c ); + } + } + private void initComponents() { // JFormDesigner - Component initialization - DO NOT MODIFY //GEN-BEGIN:initComponents JPanel panel9 = new JPanel(); @@ -282,6 +304,8 @@ public class FlatContainerTest JLabel hiddenTabsNavigationLabel = new JLabel(); hiddenTabsNavigationField = new JComboBox<>(); tabBackForegroundCheckBox = new JCheckBox(); + leadingComponentCheckBox = new JCheckBox(); + trailingComponentCheckBox = new JCheckBox(); CellConstraints cc = new CellConstraints(); //======== this ======== @@ -395,6 +419,7 @@ public class FlatContainerTest // rows "[center]" + "[]" + + "[]" + "[]")); //---- moreTabsCheckBox ---- @@ -483,6 +508,16 @@ public class FlatContainerTest tabBackForegroundCheckBox.setText("Tab back/foreground"); tabBackForegroundCheckBox.addActionListener(e -> tabBackForegroundChanged()); panel14.add(tabBackForegroundCheckBox, "cell 4 2"); + + //---- leadingComponentCheckBox ---- + leadingComponentCheckBox.setText("Leading"); + leadingComponentCheckBox.addActionListener(e -> leadingComponentChanged()); + panel14.add(leadingComponentCheckBox, "cell 0 3"); + + //---- trailingComponentCheckBox ---- + trailingComponentCheckBox.setText("Trailing"); + trailingComponentCheckBox.addActionListener(e -> trailingComponentChanged()); + panel14.add(trailingComponentCheckBox, "cell 1 3"); } panel9.add(panel14, cc.xywh(1, 11, 3, 1)); } @@ -508,6 +543,8 @@ public class FlatContainerTest private JComboBox tabPlacementField; private JComboBox hiddenTabsNavigationField; private JCheckBox tabBackForegroundCheckBox; + private JCheckBox leadingComponentCheckBox; + private JCheckBox trailingComponentCheckBox; // 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 4e481d5b..c9e16cf6 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 @@ -307,6 +307,26 @@ new FormModel { }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { "value": "cell 4 2" } ) + add( new FormComponent( "javax.swing.JCheckBox" ) { + name: "leadingComponentCheckBox" + "text": "Leading" + auxiliary() { + "JavaCodeGenerator.variableLocal": false + } + addEvent( new FormEvent( "java.awt.event.ActionListener", "actionPerformed", "leadingComponentChanged", false ) ) + }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { + "value": "cell 0 3" + } ) + add( new FormComponent( "javax.swing.JCheckBox" ) { + name: "trailingComponentCheckBox" + "text": "Trailing" + auxiliary() { + "JavaCodeGenerator.variableLocal": false + } + addEvent( new FormEvent( "java.awt.event.ActionListener", "actionPerformed", "trailingComponentChanged", false ) ) + }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { + "value": "cell 1 3" + } ) }, new FormLayoutConstraints( class com.jgoodies.forms.layout.CellConstraints ) { "gridY": 11 "gridWidth": 3 From 15718cdb4662e3e1f3dec236035c2f6864a80a34 Mon Sep 17 00:00:00 2001 From: Karl Tauber Date: Sat, 17 Oct 2020 15:19:39 +0200 Subject: [PATCH 2/4] TabbedPane: support adding custom components to left and right sides of tabs area if scroll backward/foreward buttons are used (issue #40) this also fixes some minor layout issues when using tabAreaInsets and arrow buttons --- .../formdev/flatlaf/ui/FlatTabbedPaneUI.java | 110 +++++++++++------- 1 file changed, 66 insertions(+), 44 deletions(-) diff --git a/flatlaf-core/src/main/java/com/formdev/flatlaf/ui/FlatTabbedPaneUI.java b/flatlaf-core/src/main/java/com/formdev/flatlaf/ui/FlatTabbedPaneUI.java index e7104fde..d3bb380c 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 @@ -120,6 +120,8 @@ import com.formdev.flatlaf.util.UIScale; * @uiDefault TabbedPane.hiddenTabsNavigation String moreTabsButton (default) or arrowButtons * @uiDefault ScrollPane.smoothScrolling boolean * + * @uiDefault TabbedPane.moreTabsButtonToolTipText String + * * @author Karl Tauber */ public class FlatTabbedPaneUI @@ -149,7 +151,7 @@ public class FlatTabbedPaneUI protected boolean tabSeparatorsFullHeight; protected boolean hasFullBorder; - protected int hiddenTabsNavigation = MORE_TABS_BUTTON; + private String hiddenTabsNavigationStr; protected String moreTabsButtonToolTipText; @@ -207,6 +209,7 @@ public class FlatTabbedPaneUI showTabSeparators = UIManager.getBoolean( "TabbedPane.showTabSeparators" ); tabSeparatorsFullHeight = UIManager.getBoolean( "TabbedPane.tabSeparatorsFullHeight" ); hasFullBorder = UIManager.getBoolean( "TabbedPane.hasFullBorder" ); + hiddenTabsNavigationStr = UIManager.getString( "TabbedPane.hiddenTabsNavigation" ); Locale l = tabPane.getLocale(); moreTabsButtonToolTipText = UIManager.getString( "TabbedPane.moreTabsButtonToolTipText", l ); @@ -285,16 +288,8 @@ public class FlatTabbedPaneUI } 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; + if( !isScrollTabLayout() || tabViewport == null ) + return; // At this point, BasicTabbedPaneUI already has installed // TabbedPaneScrollLayout (in super.createLayoutManager()) and @@ -550,10 +545,9 @@ public class FlatTabbedPaneUI return; } - // clip title if "more tabs" button is used + // clip title if our layout manager is used // (normally this is done by invoker, but fails in this case) - if( hiddenTabsNavigation == MORE_TABS_BUTTON && - tabViewport != null && + if( tabViewport != null && (tabPlacement == TOP || tabPlacement == BOTTOM) ) { Rectangle viewRect = tabViewport.getViewRect(); @@ -831,6 +825,13 @@ public class FlatTabbedPaneUI return UIManager.getBoolean( "ScrollPane.smoothScrolling" ); } + protected int getHiddenTabsNavigation() { + String hiddenTabsNavigationStr = (String) tabPane.getClientProperty( TABBED_PANE_HIDDEN_TABS_NAVIGATION ); + if( hiddenTabsNavigationStr == null ) + hiddenTabsNavigationStr = this.hiddenTabsNavigationStr; + return parseHiddenTabsNavigation( hiddenTabsNavigationStr ); + } + protected static int parseHiddenTabsNavigation( String str ) { if( str == null ) return MORE_TABS_BUTTON; @@ -1051,12 +1052,10 @@ public class FlatTabbedPaneUI } @Override - public Dimension getPreferredSize() { - Dimension size = super.getPreferredSize(); - boolean horizontal = (direction == WEST || direction == EAST); - return new Dimension( - horizontal ? size.width : Math.max( size.width, maxTabWidth ), - horizontal ? Math.max( size.height, maxTabHeight ) : size.height ); + protected void fireActionPerformed( ActionEvent event ) { + runWithOriginalLayoutManager( () -> { + super.fireActionPerformed( event ); + } ); } @Override @@ -1386,13 +1385,8 @@ public class FlatTabbedPaneUI case TABBED_PANE_SHOW_CONTENT_SEPARATOR: case TABBED_PANE_HAS_FULL_BORDER: case TABBED_PANE_TAB_HEIGHT: - tabPane.revalidate(); - tabPane.repaint(); - break; - case TABBED_PANE_HIDDEN_TABS_NAVIGATION: - uninstallHiddenTabsNavigation(); - installHiddenTabsNavigation(); + tabPane.revalidate(); tabPane.repaint(); break; @@ -1516,20 +1510,34 @@ public class FlatTabbedPaneUI delegate.layoutContainer( parent ); } ); - // hide scroll buttons + boolean useMoreButton = (getHiddenTabsNavigation() == MORE_TABS_BUTTON); + + // find backward/forward scroll buttons + JButton backwardButton = null; + JButton forwardButton = null; for( Component c : tabPane.getComponents() ) { - if( c instanceof FlatScrollableTabButton && c.isVisible() ) - c.setVisible( false ); + if( c instanceof FlatScrollableTabButton ) { + int direction = ((FlatScrollableTabButton)c).getDirection(); + if( direction == WEST || direction == NORTH ) + backwardButton = (JButton) c; + else if( direction == EAST || direction == SOUTH ) + forwardButton = (JButton) c; + } } + if( !useMoreButton && (backwardButton == null || forwardButton == null) ) + return; // should never occur + Rectangle bounds = tabPane.getBounds(); Insets insets = tabPane.getInsets(); int tabPlacement = tabPane.getTabPlacement(); Insets tabAreaInsets = getTabAreaInsets( tabPlacement ); Rectangle lastRect = rects[rects.length - 1]; - Dimension moreButtonSize = moreTabsButton.getPreferredSize(); + Dimension moreButtonSize = useMoreButton ? moreTabsButton.getPreferredSize() : null; + Dimension backwardButtonSize = useMoreButton ? null : backwardButton.getPreferredSize(); + Dimension forwardButtonSize = useMoreButton ? null : forwardButton.getPreferredSize(); boolean leftToRight = tabPane.getComponentOrientation().isLeftToRight(); - boolean moreTabsButtonVisible = false; + boolean buttonsVisible = false; // TabbedPaneScrollLayout adds tabAreaInsets to tab coordinates, // but we use it to position the viewport @@ -1582,18 +1590,24 @@ public class FlatTabbedPaneUI int txi = tx + tabAreaInsets.left; int twi = tw - tabAreaInsets.left - tabAreaInsets.right; - // layout viewport and "more tabs" button + // layout viewport and buttons int viewportWidth = twi; int tabsWidth = leftToRight ? (lastRect.x + lastRect.width) : (rects[0].x + rects[0].width - lastRect.x); if( viewportWidth < tabsWidth ) { - // need "more tabs" button - viewportWidth = Math.max( viewportWidth - moreButtonSize.width, 0 ); + // need buttons + buttonsVisible = true; + int buttonsWidth = useMoreButton ? moreButtonSize.width : (backwardButtonSize.width + forwardButtonSize.width); + viewportWidth = Math.max( viewportWidth - buttonsWidth, 0 ); - moreTabsButton.setBounds( leftToRight ? (txi + twi - moreButtonSize.width) : txi, tyi, moreButtonSize.width, th ); - tabViewport.setBounds( leftToRight ? txi : (txi + moreButtonSize.width), tyi, viewportWidth, th ); - moreTabsButtonVisible = true; + if( useMoreButton ) + moreTabsButton.setBounds( leftToRight ? (txi + twi - buttonsWidth) : txi, tyi, moreButtonSize.width, th ); + else { + backwardButton.setBounds( leftToRight ? (txi + twi - buttonsWidth) : txi, tyi, backwardButtonSize.width, th ); + forwardButton.setBounds( leftToRight ? (txi + twi - forwardButtonSize.width) : (txi + backwardButtonSize.width), tyi, forwardButtonSize.width, th ); + } + tabViewport.setBounds( leftToRight ? txi : (txi + buttonsWidth), tyi, viewportWidth, th ); } else tabViewport.setBounds( txi, tyi, viewportWidth, th ); @@ -1637,21 +1651,29 @@ public class FlatTabbedPaneUI int tyi = ty + tabAreaInsets.top; int thi = th - tabAreaInsets.top - tabAreaInsets.bottom; - // layout viewport and "more tabs" button + // layout viewport and buttons int viewportHeight = thi; int tabsHeight = lastRect.y + lastRect.height; if( viewportHeight < tabsHeight ) { - // need "more tabs" button - viewportHeight = Math.max( viewportHeight - moreButtonSize.height, 0 ); + // need buttons + buttonsVisible = true; + int buttonsHeight = useMoreButton ? moreButtonSize.height : (backwardButtonSize.height + forwardButtonSize.height); + viewportHeight = Math.max( viewportHeight - buttonsHeight, 0 ); - moreTabsButton.setBounds( txi, tyi + thi - moreButtonSize.height, tw, moreButtonSize.height ); - moreTabsButtonVisible = true; + if( useMoreButton ) + moreTabsButton.setBounds( txi, tyi + thi - buttonsHeight, tw, moreButtonSize.height ); + else { + backwardButton.setBounds( txi, tyi + thi - buttonsHeight, tw, backwardButtonSize.height ); + forwardButton.setBounds( txi, tyi + thi - forwardButtonSize.height, tw, forwardButtonSize.height ); + } } tabViewport.setBounds( txi, tyi, tw, viewportHeight ); } - // show/hide "more tabs" button - moreTabsButton.setVisible( moreTabsButtonVisible ); + // show/hide buttons + moreTabsButton.setVisible( useMoreButton && buttonsVisible ); + backwardButton.setVisible( !useMoreButton && buttonsVisible ); + forwardButton.setVisible( !useMoreButton && buttonsVisible ); } private void shiftTabs( int sx, int sy ) { From c34ce389a433d16d3ece956d05807e7432f7d269 Mon Sep 17 00:00:00 2001 From: Karl Tauber Date: Sat, 17 Oct 2020 16:46:56 +0200 Subject: [PATCH 3/4] TabbedPane: do not include preferred/minimum size of leading/trailing components in calculating preferred/minimum size of tabbed pane, because the largest tab content determines the size --- .../formdev/flatlaf/ui/FlatTabbedPaneUI.java | 32 +------------------ 1 file changed, 1 insertion(+), 31 deletions(-) diff --git a/flatlaf-core/src/main/java/com/formdev/flatlaf/ui/FlatTabbedPaneUI.java b/flatlaf-core/src/main/java/com/formdev/flatlaf/ui/FlatTabbedPaneUI.java index d3bb380c..05567e77 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 @@ -1435,7 +1435,7 @@ public class FlatTabbedPaneUI //---- class FlatTabbedPaneScrollLayout ----------------------------------- /** - * Layout manager used if "TabbedPane.hiddenTabsNavigation" is "moreTabsButton". + * Layout manager used for scroll tab layout policy. *

* Although this class delegates all methods to the original layout manager * {@link BasicTabbedPaneUI.TabbedPaneScrollLayout}, which extends @@ -1470,36 +1470,6 @@ public class FlatTabbedPaneUI delegate.removeLayoutComponent( comp ); } - @Override - public Dimension preferredLayoutSize( Container parent ) { - Dimension size = delegate.preferredLayoutSize( parent ); - size = addLayoutSize( size, leadingComponent, false ); - size = addLayoutSize( size, trailingComponent, false ); - return size; - } - - @Override - public Dimension minimumLayoutSize( Container parent ) { - Dimension size = delegate.minimumLayoutSize( parent ); - size = addLayoutSize( size, leadingComponent, true ); - size = addLayoutSize( size, trailingComponent, true ); - return size; - } - - private Dimension addLayoutSize( Dimension size, Container c, boolean minimum ) { - if( c == null ) - return size; - - Dimension compSize = minimum ? c.getMinimumSize() : c.getPreferredSize(); - - size = (Dimension) size.clone(); - if( isHorizontalTabPlacement() ) - size.width += compSize.width; - else - size.height += compSize.height; - return size; - } - @Override public void layoutContainer( Container parent ) { // delegate to original layout manager and let it layout tabs and buttons From 3818790cedc02ce077b3f59611ef2e891377266f Mon Sep 17 00:00:00 2001 From: Karl Tauber Date: Sat, 17 Oct 2020 18:17:45 +0200 Subject: [PATCH 4/4] TabbedPane: support adding custom components to left and right sides of tabs area if wrap layout is used (issue #40) --- .../formdev/flatlaf/ui/FlatTabbedPaneUI.java | 174 ++++++++++++++---- 1 file changed, 138 insertions(+), 36 deletions(-) diff --git a/flatlaf-core/src/main/java/com/formdev/flatlaf/ui/FlatTabbedPaneUI.java b/flatlaf-core/src/main/java/com/formdev/flatlaf/ui/FlatTabbedPaneUI.java index 05567e77..1c26fc39 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 @@ -274,6 +274,8 @@ public class FlatTabbedPaneUI } installHiddenTabsNavigation(); + installLeadingComponent(); + installTrailingComponent(); } @Override @@ -281,6 +283,8 @@ public class FlatTabbedPaneUI // uninstall hidden tabs navigation before invoking super.uninstallComponents() for // correct uninstallation of BasicTabbedPaneUI tab scroller support uninstallHiddenTabsNavigation(); + uninstallLeadingComponent(); + uninstallTrailingComponent(); super.uninstallComponents(); @@ -302,9 +306,6 @@ public class FlatTabbedPaneUI // create and add "more tabs" button moreTabsButton = createMoreTabsButton(); tabPane.add( moreTabsButton ); - - installLeadingComponent(); - installTrailingComponent(); } protected void uninstallHiddenTabsNavigation() { @@ -317,9 +318,6 @@ public class FlatTabbedPaneUI tabPane.remove( moreTabsButton ); moreTabsButton = null; } - - uninstallLeadingComponent(); - uninstallTrailingComponent(); } protected void installLeadingComponent() { @@ -415,6 +413,14 @@ public class FlatTabbedPaneUI return handler; } + @Override + protected LayoutManager createLayoutManager() { + if( tabPane.getTabLayoutPolicy() == JTabbedPane.WRAP_TAB_LAYOUT ) + return new FlatTabbedPaneLayout(); + + return super.createLayoutManager(); + } + protected LayoutManager createScrollLayoutManager( TabbedPaneLayout delegate ) { return new FlatTabbedPaneScrollLayout( delegate ); } @@ -480,6 +486,24 @@ public class FlatTabbedPaneUI // Giving it large values clips painting of the cropped edge and makes it invisible. currentTabAreaInsets.top = currentTabAreaInsets.left = -10000; + // increase insets for wrap layout if using leading/trailing components + if( tabPane.getTabLayoutPolicy() == JTabbedPane.WRAP_TAB_LAYOUT ) { + if( leadingComponent != null ) { + Dimension leadingSize = leadingComponent.getPreferredSize(); + if( isHorizontalTabPlacement() ) + insets.left += leadingSize.width; + else + insets.top += leadingSize.height; + } + if( trailingComponent != null ) { + Dimension trailingSize = trailingComponent.getPreferredSize(); + if( isHorizontalTabPlacement() ) + insets.right += trailingSize.width; + else + insets.bottom += trailingSize.height; + } + } + return insets; } @@ -874,6 +898,22 @@ public class FlatTabbedPaneUI ((JComponent)tabViewport.getView()).scrollRectToVisible( (Rectangle) rects[selectedIndex].clone() ); } + private void shiftTabs( int sx, int sy ) { + if( sx == 0 && sy == 0 ) + return; + + for( int i = 0; i < rects.length; i++ ) { + // fix x location in rects + rects[i].x += sx; + rects[i].y += sy; + + // fix tab component location + Component c = tabPane.getTabComponentAt( i ); + if( c != null ) + c.setLocation( c.getX() + sx, c.getY() + sy ); + } + } + //---- class ContainerUIResource ------------------------------------------ private class ContainerUIResource @@ -1432,6 +1472,84 @@ public class FlatTabbedPaneUI @Override public void componentHidden( ComponentEvent e ) {} } + //---- class FlatTabbedPaneLayout ----------------------------------------- + + protected class FlatTabbedPaneLayout + extends TabbedPaneLayout + { + @Override + public void layoutContainer( Container parent ) { + super.layoutContainer( parent ); + + Rectangle bounds = tabPane.getBounds(); + Insets insets = tabPane.getInsets(); + int tabPlacement = tabPane.getTabPlacement(); + Insets tabAreaInsets = getTabAreaInsets( tabPlacement ); + boolean leftToRight = tabPane.getComponentOrientation().isLeftToRight(); + + // layout leading and trailing components in tab area + if( tabPlacement == TOP || tabPlacement == BOTTOM ) { + // tab area bounds + int tx = insets.left; + int ty = (tabPlacement == TOP) + ? insets.top + : (bounds.height - insets.bottom - maxTabHeight - tabAreaInsets.top - tabAreaInsets.bottom); + int tw = bounds.width - insets.left - insets.right; + int th = maxTabHeight; + + int tyi = ty + tabAreaInsets.top; + + // layout left component + Container leftComponent = leftToRight ? leadingComponent : trailingComponent; + if( leftComponent != null ) { + int leftWidth = leftComponent.getPreferredSize().width; + leftComponent.setBounds( tx, tyi, leftWidth, th ); + + // reduce tab area bounds + tx += leftWidth; + tw -= leftWidth; + } + + // layout right component + Container rightComponent = leftToRight ? trailingComponent : leadingComponent; + if( rightComponent != null ) { + int rightWidth = rightComponent.getPreferredSize().width; + rightComponent.setBounds( tx + tw - rightWidth, tyi, rightWidth, th ); + } + + // fix x-locations of tabs in right-to-left component orientation + if( !leftToRight ) + shiftTabs( insets.left + tabAreaInsets.right, 0 ); + } else { // LEFT, RIGHT + // tab area bounds + int tx = (tabPlacement == LEFT) + ? insets.left + : (bounds.width - insets.right - maxTabWidth - tabAreaInsets.left - tabAreaInsets.right); + int ty = insets.top; + int tw = maxTabWidth; + int th = bounds.height - insets.top - insets.bottom; + + int txi = tx + tabAreaInsets.left; + + // layout top component + if( leadingComponent != null ) { + int topHeight = leadingComponent.getPreferredSize().height; + leadingComponent.setBounds( txi, ty, tw, topHeight ); + + // reduce tab area bounds + ty += topHeight; + th -= topHeight; + } + + // layout bottom component + if( trailingComponent != null ) { + int bottomHeight = trailingComponent.getPreferredSize().height; + trailingComponent.setBounds( txi, ty + th - bottomHeight, tw, bottomHeight ); + } + } + } + } + //---- class FlatTabbedPaneScrollLayout ----------------------------------- /** @@ -1539,22 +1657,22 @@ public class FlatTabbedPaneUI // layout left component Container leftComponent = leftToRight ? leadingComponent : trailingComponent; if( leftComponent != null ) { - Dimension leftSize = leftComponent.getPreferredSize(); - leftComponent.setBounds( tx, tyi, leftSize.width, th ); + int leftWidth = leftComponent.getPreferredSize().width; + leftComponent.setBounds( tx, tyi, leftWidth, th ); // reduce tab area bounds - tx += leftSize.width; - tw -= leftSize.width; + tx += leftWidth; + tw -= leftWidth; } // layout right component Container rightComponent = leftToRight ? trailingComponent : leadingComponent; if( rightComponent != null ) { - Dimension rightSize = rightComponent.getPreferredSize(); - rightComponent.setBounds( tx + tw - rightSize.width, tyi, rightSize.width, th ); + int rightWidth = rightComponent.getPreferredSize().width; + rightComponent.setBounds( tx + tw - rightWidth, tyi, rightWidth, th ); // reduce tab area bounds - tw -= rightSize.width; + tw -= rightWidth; } int txi = tx + tabAreaInsets.left; @@ -1601,21 +1719,21 @@ public class FlatTabbedPaneUI // layout top component if( leadingComponent != null ) { - Dimension topSize = leadingComponent.getPreferredSize(); - leadingComponent.setBounds( txi, ty, tw, topSize.height ); + int topHeight = leadingComponent.getPreferredSize().height; + leadingComponent.setBounds( txi, ty, tw, topHeight ); // reduce tab area bounds - ty += topSize.height; - th -= topSize.height; + ty += topHeight; + th -= topHeight; } // layout bottom component if( trailingComponent != null ) { - Dimension bottomSize = trailingComponent.getPreferredSize(); - trailingComponent.setBounds( txi, ty + th - bottomSize.height, tw, bottomSize.height ); + int bottomHeight = trailingComponent.getPreferredSize().height; + trailingComponent.setBounds( txi, ty + th - bottomHeight, tw, bottomHeight ); // reduce tab area bounds - th -= bottomSize.height; + th -= bottomHeight; } int tyi = ty + tabAreaInsets.top; @@ -1645,21 +1763,5 @@ public class FlatTabbedPaneUI backwardButton.setVisible( !useMoreButton && buttonsVisible ); forwardButton.setVisible( !useMoreButton && buttonsVisible ); } - - private void shiftTabs( int sx, int sy ) { - if( sx == 0 && sy == 0 ) - return; - - for( int i = 0; i < rects.length; i++ ) { - // fix x location in rects - rects[i].x += sx; - rects[i].y += sy; - - // fix tab component location - Component c = tabPane.getTabComponentAt( i ); - if( c != null ) - c.setLocation( c.getX() + sx, c.getY() + sy ); - } - } } }