com.formdev.flatlaf.ui.FlatTabbedPaneUI Maven / Gradle / Ivy
/*
* Copyright 2019 FormDev Software GmbH
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
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;
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.ContainerEvent;
import java.awt.event.ContainerListener;
import java.awt.event.InputEvent;
import java.awt.event.KeyEvent;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
import java.awt.event.MouseListener;
import java.awt.event.MouseMotionListener;
import java.awt.event.MouseWheelEvent;
import java.awt.geom.Path2D;
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 java.util.function.BiConsumer;
import java.util.function.IntConsumer;
import javax.accessibility.Accessible;
import javax.accessibility.AccessibleContext;
import javax.swing.Action;
import javax.swing.ActionMap;
import javax.swing.ButtonModel;
import javax.swing.Icon;
import javax.swing.JButton;
import javax.swing.JComponent;
import javax.swing.JLabel;
import javax.swing.JMenuItem;
import javax.swing.JPanel;
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;
import javax.swing.text.JTextComponent;
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.StringUtils;
import com.formdev.flatlaf.util.UIScale;
/**
* Provides the Flat LaF UI delegate for {@link javax.swing.JTabbedPane}.
*
* @clientProperty JTabbedPane.showTabSeparators boolean
* @clientProperty JTabbedPane.hasFullBorder boolean
*
*
*
* @uiDefault TabbedPane.font Font
* @uiDefault TabbedPane.background Color
* @uiDefault TabbedPane.foreground Color
* @uiDefault TabbedPane.shadow Color used for scroll arrows and cropped line
* @uiDefault TabbedPane.textIconGap int
* @uiDefault TabbedPane.tabInsets Insets
* @uiDefault TabbedPane.selectedTabPadInsets Insets unused
* @uiDefault TabbedPane.tabAreaInsets Insets
* @uiDefault TabbedPane.tabsOverlapBorder boolean
* @uiDefault TabbedPane.tabRunOverlay int
* @uiDefault TabbedPane.tabsOpaque boolean
* @uiDefault TabbedPane.contentOpaque boolean unused
* @uiDefault TabbedPane.opaque boolean
* @uiDefault TabbedPane.selectionFollowsFocus boolean default is true
*
*
*
* @uiDefault TabbedPane.disabledForeground Color
* @uiDefault TabbedPane.selectedBackground Color optional
* @uiDefault TabbedPane.selectedForeground Color
* @uiDefault TabbedPane.underlineColor Color
* @uiDefault TabbedPane.disabledUnderlineColor Color
* @uiDefault TabbedPane.hoverColor Color
* @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
* @uiDefault TabbedPane.showTabSeparators boolean
* @uiDefault TabbedPane.tabSeparatorsFullHeight boolean
* @uiDefault TabbedPane.hasFullBorder boolean
*
* @uiDefault TabbedPane.tabLayoutPolicy String wrap (default) or scroll
* @uiDefault TabbedPane.tabsPopupPolicy String never or asNeeded (default)
* @uiDefault TabbedPane.scrollButtonsPolicy String never, asNeeded or asNeededSingle (default)
* @uiDefault TabbedPane.scrollButtonsPlacement String both (default) or trailing
*
* @uiDefault TabbedPane.tabAreaAlignment String leading (default), center, trailing or fill
* @uiDefault TabbedPane.tabAlignment String leading, center (default) or trailing
* @uiDefault TabbedPane.tabWidthMode String preferred (default), equal or compact
* @uiDefault ScrollPane.smoothScrolling boolean
* @uiDefault TabbedPane.closeIcon Icon
*
* @uiDefault TabbedPane.arrowType String chevron (default) or triangle
* @uiDefault TabbedPane.buttonInsets Insets
* @uiDefault TabbedPane.buttonArc int
* @uiDefault TabbedPane.buttonHoverBackground Color
* @uiDefault TabbedPane.buttonPressedBackground Color
*
* @uiDefault TabbedPane.moreTabsButtonToolTipText String
*
* @author Karl Tauber
*/
public class FlatTabbedPaneUI
extends BasicTabbedPaneUI
{
// tabs popup policy / scroll arrows policy
protected static final int NEVER = 0;
// protected static final int ALWAYS = 1;
protected static final int AS_NEEDED = 2;
protected static final int AS_NEEDED_SINGLE = 3;
// scroll arrows placement
protected static final int BOTH = 100;
// tab area alignment
protected static final int FILL = 100;
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;
protected Color foreground;
protected Color disabledForeground;
protected Color selectedBackground;
protected Color selectedForeground;
protected Color underlineColor;
protected Color disabledUnderlineColor;
protected Color hoverColor;
protected Color focusColor;
protected Color tabSeparatorColor;
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 int tabsPopupPolicy;
private int scrollButtonsPolicy;
private int scrollButtonsPlacement;
private int tabAreaAlignment;
private int tabAlignment;
private int tabWidthMode;
protected Icon closeIcon;
protected String arrowType;
protected Insets buttonInsets;
protected int buttonArc;
protected Color buttonHoverBackground;
protected Color buttonPressedBackground;
protected String moreTabsButtonToolTipText;
protected JViewport tabViewport;
protected FlatWheelTabScroller wheelTabScroller;
private JButton tabCloseButton;
private JButton moreTabsButton;
private Container leadingComponent;
private Container trailingComponent;
private Dimension scrollBackwardButtonPrefSize;
private Handler handler;
private boolean blockRollover;
private boolean rolloverTabClose;
private boolean pressedTabClose;
private Object[] oldRenderingHints;
public static ComponentUI createUI( JComponent c ) {
return new FlatTabbedPaneUI();
}
@Override
public void installUI( JComponent c ) {
// initialize tab layout policy (if specified)
String tabLayoutPolicyStr = UIManager.getString( "TabbedPane.tabLayoutPolicy" );
if( tabLayoutPolicyStr != null ) {
int tabLayoutPolicy;
switch( tabLayoutPolicyStr ) {
default:
case "wrap": tabLayoutPolicy = JTabbedPane.WRAP_TAB_LAYOUT; break;
case "scroll": tabLayoutPolicy = JTabbedPane.SCROLL_TAB_LAYOUT; break;
}
((JTabbedPane)c).setTabLayoutPolicy( tabLayoutPolicy );
}
// initialize this defaults here because they are used in constructor
// of FlatTabAreaButton, which is invoked before installDefaults()
arrowType = UIManager.getString( "TabbedPane.arrowType" );
foreground = UIManager.getColor( "TabbedPane.foreground" );
disabledForeground = UIManager.getColor( "TabbedPane.disabledForeground" );
buttonHoverBackground = UIManager.getColor( "TabbedPane.buttonHoverBackground" );
buttonPressedBackground = UIManager.getColor( "TabbedPane.buttonPressedBackground" );
super.installUI( c );
}
@Override
protected void 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();
selectedBackground = UIManager.getColor( "TabbedPane.selectedBackground" );
selectedForeground = UIManager.getColor( "TabbedPane.selectedForeground" );
underlineColor = UIManager.getColor( "TabbedPane.underlineColor" );
disabledUnderlineColor = UIManager.getColor( "TabbedPane.disabledUnderlineColor" );
hoverColor = UIManager.getColor( "TabbedPane.hoverColor" );
focusColor = UIManager.getColor( "TabbedPane.focusColor" );
tabSeparatorColor = UIManager.getColor( "TabbedPane.tabSeparatorColor" );
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" );
tabsPopupPolicy = parseTabsPopupPolicy( UIManager.getString( "TabbedPane.tabsPopupPolicy" ) );
scrollButtonsPolicy = parseScrollButtonsPolicy( UIManager.getString( "TabbedPane.scrollButtonsPolicy" ) );
scrollButtonsPlacement = parseScrollButtonsPlacement( UIManager.getString( "TabbedPane.scrollButtonsPlacement" ) );
tabAreaAlignment = parseAlignment( UIManager.getString( "TabbedPane.tabAreaAlignment" ), LEADING );
tabAlignment = parseAlignment( UIManager.getString( "TabbedPane.tabAlignment" ), CENTER );
tabWidthMode = parseTabWidthMode( UIManager.getString( "TabbedPane.tabWidthMode" ) );
closeIcon = UIManager.getIcon( "TabbedPane.closeIcon" );
buttonInsets = UIManager.getInsets( "TabbedPane.buttonInsets" );
buttonArc = UIManager.getInt( "TabbedPane.buttonArc" );
Locale l = tabPane.getLocale();
moreTabsButtonToolTipText = UIManager.getString( "TabbedPane.moreTabsButtonToolTipText", l );
// scale
textIconGap = scale( textIconGapUnscaled );
// replace focus forward/backward traversal keys with TAB/Shift+TAB because
// the default also includes Ctrl+TAB/Ctrl+Shift+TAB, which we need to switch tabs
if( focusForwardTraversalKeys == null ) {
focusForwardTraversalKeys = Collections.singleton( KeyStroke.getKeyStroke( KeyEvent.VK_TAB, 0 ) );
focusBackwardTraversalKeys = Collections.singleton( KeyStroke.getKeyStroke( KeyEvent.VK_TAB, InputEvent.SHIFT_DOWN_MASK ) );
}
// Ideally we should use `LookAndFeel.installProperty( tabPane, "focusTraversalKeysForward", keys )` here
// instead of `tabPane.setFocusTraversalKeys()`, but WindowsTabbedPaneUI also uses later method
// and switching from Windows LaF to FlatLaf would not replace the keys and Ctrl+TAB would not work.
tabPane.setFocusTraversalKeys( KeyboardFocusManager.FORWARD_TRAVERSAL_KEYS, focusForwardTraversalKeys );
tabPane.setFocusTraversalKeys( KeyboardFocusManager.BACKWARD_TRAVERSAL_KEYS, focusBackwardTraversalKeys );
MigLayoutVisualPadding.install( tabPane, null );
}
@Override
protected void uninstallDefaults() {
// restore focus forward/backward traversal keys
tabPane.setFocusTraversalKeys( KeyboardFocusManager.FORWARD_TRAVERSAL_KEYS, null );
tabPane.setFocusTraversalKeys( KeyboardFocusManager.BACKWARD_TRAVERSAL_KEYS, null );
super.uninstallDefaults();
foreground = null;
disabledForeground = null;
selectedBackground = null;
selectedForeground = null;
underlineColor = null;
disabledUnderlineColor = null;
hoverColor = null;
focusColor = null;
tabSeparatorColor = null;
contentAreaColor = null;
closeIcon = null;
buttonHoverBackground = null;
buttonPressedBackground = null;
MigLayoutVisualPadding.uninstall( tabPane );
}
@Override
protected void installComponents() {
super.installComponents();
// find scrollable tab viewport
tabViewport = null;
if( isScrollTabLayout() ) {
for( Component c : tabPane.getComponents() ) {
if( c instanceof JViewport && c.getClass().getName().equals( "javax.swing.plaf.basic.BasicTabbedPaneUI$ScrollableTabViewport" ) ) {
tabViewport = (JViewport) c;
break;
}
}
}
installHiddenTabsNavigation();
installLeadingComponent();
installTrailingComponent();
}
@Override
protected void uninstallComponents() {
// uninstall hidden tabs navigation before invoking super.uninstallComponents() for
// correct uninstallation of BasicTabbedPaneUI tab scroller support
uninstallHiddenTabsNavigation();
uninstallLeadingComponent();
uninstallTrailingComponent();
super.uninstallComponents();
tabCloseButton = null;
tabViewport = null;
}
protected void installHiddenTabsNavigation() {
if( !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;
}
}
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
protected void installListeners() {
super.installListeners();
getHandler().installListeners();
if( tabViewport != null && (wheelTabScroller = createWheelTabScroller()) != null ) {
// ideally we would add the mouse listeners to the viewport, but then the
// mouse listener of the tabbed pane would not receive events while
// the mouse pointer is over the viewport
tabPane.addMouseWheelListener( wheelTabScroller );
tabPane.addMouseMotionListener( wheelTabScroller );
tabPane.addMouseListener( wheelTabScroller );
}
}
@Override
protected void uninstallListeners() {
super.uninstallListeners();
if( handler != null ) {
handler.uninstallListeners();
handler = null;
}
if( wheelTabScroller != null ) {
wheelTabScroller.uninstall();
tabPane.removeMouseWheelListener( wheelTabScroller );
tabPane.removeMouseMotionListener( wheelTabScroller );
tabPane.removeMouseListener( wheelTabScroller );
wheelTabScroller = null;
}
}
@Override
protected void installKeyboardActions() {
super.installKeyboardActions();
// get shared action map, used for all tabbed panes
ActionMap map = SwingUtilities.getUIActionMap( tabPane );
if( map != null ) {
// this is required for the case that those actions are used from outside
// (e.g. wheel tab scroller in NetBeans)
RunWithOriginalLayoutManagerDelegateAction.install( map, "scrollTabsForwardAction" );
RunWithOriginalLayoutManagerDelegateAction.install( map, "scrollTabsBackwardAction" );
}
}
private Handler getHandler() {
if( handler == null )
handler = new Handler();
return handler;
}
protected FlatWheelTabScroller createWheelTabScroller() {
return new FlatWheelTabScroller();
}
@Override
protected MouseListener createMouseListener() {
Handler handler = getHandler();
handler.mouseDelegate = super.createMouseListener();
return handler;
}
@Override
protected PropertyChangeListener createPropertyChangeListener() {
Handler handler = getHandler();
handler.propertyChangeDelegate = super.createPropertyChangeListener();
return handler;
}
@Override
protected ChangeListener createChangeListener() {
Handler handler = getHandler();
handler.changeDelegate = super.createChangeListener();
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 );
}
protected JButton createMoreTabsButton() {
return new FlatMoreTabsButton();
}
@Override
protected JButton createScrollButton( int direction ) {
return new FlatScrollableTabButton( direction );
}
protected void setRolloverTab( int x, int y ) {
setRolloverTab( tabForCoordinate( tabPane, x, y ) );
}
@Override
protected void setRolloverTab( int index ) {
if( blockRollover )
return;
int oldIndex = getRolloverTab();
super.setRolloverTab( index );
if( index == oldIndex )
return;
// repaint old and new hover tabs
repaintTab( oldIndex );
repaintTab( index );
}
protected boolean isRolloverTabClose() {
return rolloverTabClose;
}
protected void setRolloverTabClose( boolean rollover ) {
if( rolloverTabClose == rollover )
return;
rolloverTabClose = rollover;
repaintTab( getRolloverTab() );
}
protected boolean isPressedTabClose() {
return pressedTabClose;
}
protected void setPressedTabClose( boolean pressed ) {
if( pressedTabClose == pressed )
return;
pressedTabClose = pressed;
repaintTab( getRolloverTab() );
}
private void repaintTab( int tabIndex ) {
if( tabIndex < 0 || tabIndex >= tabPane.getTabCount() )
return;
Rectangle r = getTabBounds( tabPane, tabIndex );
if( r != null )
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;
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 {
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 ) )
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;
}
@Override
protected int calculateTabHeight( int tabPlacement, int tabIndex, int fontHeight ) {
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
protected int calculateMaxTabWidth( int tabPlacement ) {
return hideTabArea() ? 0 : super.calculateMaxTabWidth( tabPlacement );
}
@Override
protected int calculateMaxTabHeight( int tabPlacement ) {
return hideTabArea() ? 0 : super.calculateMaxTabHeight( tabPlacement );
}
@Override
protected int calculateTabAreaWidth( int tabPlacement, int vertRunCount, int maxTabWidth ) {
return hideTabArea() ? 0 : super.calculateTabAreaWidth( tabPlacement, vertRunCount, maxTabWidth );
}
@Override
protected int calculateTabAreaHeight( int tabPlacement, int horizRunCount, int maxTabHeight ) {
return hideTabArea() ? 0 : super.calculateTabAreaHeight( tabPlacement, horizRunCount, maxTabHeight );
}
@Override
protected Insets getTabInsets( int tabPlacement, int tabIndex ) {
Object value = getTabClientProperty( tabIndex, TABBED_PANE_TAB_INSETS );
return scale( (value instanceof Insets)
? (Insets) value
: super.getTabInsets( tabPlacement, tabIndex ) );
}
@Override
protected Insets getSelectedTabPadInsets( int tabPlacement ) {
return new Insets( 0, 0, 0, 0 );
}
protected Insets getRealTabAreaInsets( int tabPlacement ) {
// this is to avoid potential NPE in ensureSelectedTabIsVisible()
// (see https://github.com/JFormDesigner/FlatLaf/issues/299)
// but now should actually never occur because added more checks to
// ensureSelectedTabIsVisibleLater() and ensureSelectedTabIsVisible()
if( tabAreaInsets == null )
tabAreaInsets = new Insets( 0, 0, 0, 0 );
Insets currentTabAreaInsets = super.getTabAreaInsets( tabPlacement );
Insets insets = (Insets) currentTabAreaInsets.clone();
Object value = tabPane.getClientProperty( TABBED_PANE_TAB_AREA_INSETS );
if( value instanceof Insets )
rotateInsets( (Insets) value, insets, tabPlacement );
// 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;
// scale insets (before adding leading/trailing component sizes)
insets = scale( insets );
return insets;
}
@Override
protected Insets getTabAreaInsets( int tabPlacement ) {
Insets insets = getRealTabAreaInsets( tabPlacement );
// increase insets for wrap layout if using leading/trailing components
if( tabPane.getTabLayoutPolicy() == JTabbedPane.WRAP_TAB_LAYOUT ) {
if( isHorizontalTabPlacement() ) {
insets.left += getLeadingPreferredWidth();
insets.right += getTrailingPreferredWidth();
} else {
insets.top += getLeadingPreferredHeight();
insets.bottom += getTrailingPreferredHeight();
}
}
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
* are also used for the border.
*/
@Override
protected Insets getContentBorderInsets( int tabPlacement ) {
if( hideTabArea() || contentSeparatorHeight == 0 || !clientPropertyBoolean( tabPane, TABBED_PANE_SHOW_CONTENT_SEPARATOR, true ) )
return new Insets( 0, 0, 0, 0 );
boolean hasFullBorder = clientPropertyBoolean( tabPane, TABBED_PANE_HAS_FULL_BORDER, this.hasFullBorder );
int sh = scale( contentSeparatorHeight );
Insets insets = hasFullBorder ? new Insets( sh, sh, sh, sh ) : new Insets( sh, 0, 0, 0 );
Insets contentBorderInsets = new Insets( 0, 0, 0, 0 );
rotateInsets( insets, contentBorderInsets, tabPlacement );
return contentBorderInsets;
}
@Override
protected int getTabLabelShiftX( int tabPlacement, int tabIndex, boolean isSelected ) {
if( isTabClosable( tabIndex ) ) {
int shift = closeIcon.getIconWidth() / 2;
return isLeftToRight() ? -shift : shift;
}
return 0;
}
@Override
protected int getTabLabelShiftY( int tabPlacement, int tabIndex, boolean isSelected ) {
return 0;
}
@Override
public void update( Graphics g, JComponent c ) {
oldRenderingHints = FlatUIUtils.setRenderingHints( g );
super.update( g, c );
FlatUIUtils.resetRenderingHints( g, oldRenderingHints );
oldRenderingHints = null;
}
@Override
public void paint( Graphics g, JComponent c ) {
if( hideTabArea() )
return;
ensureCurrentLayout();
int tabPlacement = tabPane.getTabPlacement();
int selectedIndex = tabPane.getSelectedIndex();
paintContentBorder( g, tabPlacement, selectedIndex );
if( !isScrollTabLayout() )
paintTabArea( g, tabPlacement, selectedIndex );
}
@Override
protected void paintTabArea( Graphics g, int tabPlacement, int selectedIndex ) {
// need to set rendering hints here too because this method is also invoked
// from BasicTabbedPaneUI.ScrollableTabPanel.paintComponent()
Object[] oldHints = FlatUIUtils.setRenderingHints( g );
super.paintTabArea( g, tabPlacement, selectedIndex );
FlatUIUtils.resetRenderingHints( g, oldHints );
}
@Override
protected void paintTab( Graphics g, int tabPlacement, Rectangle[] rects,
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, x, y, w, h, isSelected );
// paint border
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;
// layout title and icon
String title = tabPane.getTitleAt( tabIndex );
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 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
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
if( !isCompact )
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 )
{
g.setFont( font );
FlatUIUtils.runWithoutRenderingHints( g, oldRenderingHints, () -> {
// html
View view = getTextViewForTab( tabIndex );
if( view != null ) {
view.paint( g, textRect );
return;
}
// plain text
Color color;
if( tabPane.isEnabled() && tabPane.isEnabledAt( tabIndex ) ) {
color = tabPane.getForegroundAt( tabIndex );
if( isSelected && selectedForeground != null && color == tabPane.getForeground() )
color = selectedForeground;
} else
color = disabledForeground;
int mnemIndex = FlatLaf.isShowMnemonics() ? tabPane.getDisplayedMnemonicIndexAt( tabIndex ) : -1;
g.setColor( color );
FlatUIUtils.drawStringUnderlineCharAt( tabPane, g, title, mnemIndex,
textRect.x, textRect.y + metrics.getAscent() );
} );
}
@Override
protected void paintTabBackground( Graphics g, int tabPlacement, int tabIndex,
int x, int y, int w, int h, boolean isSelected )
{
// paint tab background
boolean enabled = tabPane.isEnabled();
Color background = enabled && tabPane.isEnabledAt( tabIndex ) && getRolloverTab() == tabIndex
? hoverColor
: (enabled && isSelected && FlatUIUtils.isPermanentFocusOwner( tabPane )
? focusColor
: (selectedBackground != null && enabled && isSelected
? selectedBackground
: tabPane.getBackgroundAt( tabIndex )));
g.setColor( FlatUIUtils.deriveColor( background, tabPane.getBackground() ) );
g.fillRect( x, y, w, h );
}
@Override
protected void paintTabBorder( Graphics g, int tabPlacement, int tabIndex,
int x, int y, int w, int h, boolean isSelected )
{
// paint tab separators
if( clientPropertyBoolean( tabPane, TABBED_PANE_SHOW_TAB_SEPARATORS, showTabSeparators ) &&
!isLastInRun( tabIndex ) )
paintTabSeparator( g, tabPlacement, x, y, w, h );
}
protected void paintTabCloseButton( Graphics g, int tabIndex, int x, int y, int w, int h ) {
// create tab close button
if( tabCloseButton == null ) {
tabCloseButton = new TabCloseButton();
tabCloseButton.setVisible( false );
}
// update state of tab close button
boolean rollover = (tabIndex == getRolloverTab());
ButtonModel bm = tabCloseButton.getModel();
bm.setRollover( rollover && isRolloverTabClose() );
bm.setPressed( rollover && isPressedTabClose() );
// copy colors from tabbed pane because close icon uses derives colors
tabCloseButton.setBackground( tabPane.getBackground() );
tabCloseButton.setForeground( tabPane.getForeground() );
// paint tab close icon
Rectangle tabCloseRect = getTabCloseBounds( tabIndex, x, y, w, h, calcRect );
closeIcon.paintIcon( tabCloseButton, g, tabCloseRect.x, tabCloseRect.y );
}
protected void paintTabSeparator( Graphics g, int tabPlacement, int x, int y, int w, int h ) {
float sepWidth = UIScale.scale( 1f );
float offset = tabSeparatorsFullHeight ? 0 : UIScale.scale( 5f );
g.setColor( (tabSeparatorColor != null) ? tabSeparatorColor : contentAreaColor );
if( tabPlacement == LEFT || tabPlacement == RIGHT ) {
// paint tab separator at bottom side
((Graphics2D)g).fill( new Rectangle2D.Float( x + offset, y + h - sepWidth, w - (offset * 2), sepWidth ) );
} else if( isLeftToRight() ) {
// paint tab separator at right side
((Graphics2D)g).fill( new Rectangle2D.Float( x + w - sepWidth, y + offset, sepWidth, h - (offset * 2) ) );
} else {
// paint tab separator at left side
((Graphics2D)g).fill( new Rectangle2D.Float( x, y + offset, sepWidth, h - (offset * 2) ) );
}
}
protected void paintTabSelection( Graphics g, int tabPlacement, int x, int y, int w, int h ) {
g.setColor( tabPane.isEnabled() ? underlineColor : disabledUnderlineColor );
// paint underline selection
Insets contentInsets = getContentBorderInsets( tabPlacement );
int tabSelectionHeight = scale( this.tabSelectionHeight );
switch( tabPlacement ) {
case TOP:
default:
int sy = y + h + contentInsets.top - tabSelectionHeight;
g.fillRect( x, sy, w, tabSelectionHeight );
break;
case BOTTOM:
g.fillRect( x, y - contentInsets.bottom, w, tabSelectionHeight );
break;
case LEFT:
int sx = x + w + contentInsets.left - tabSelectionHeight;
g.fillRect( sx, y, tabSelectionHeight, h );
break;
case RIGHT:
g.fillRect( x - contentInsets.right, y, tabSelectionHeight, h );
break;
}
}
/**
* 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 ||
contentSeparatorHeight == 0 ||
!clientPropertyBoolean( tabPane, TABBED_PANE_SHOW_CONTENT_SEPARATOR, true ) )
return;
Insets insets = tabPane.getInsets();
Insets tabAreaInsets = getTabAreaInsets( tabPlacement );
int x = insets.left;
int y = insets.top;
int w = tabPane.getWidth() - insets.right - insets.left;
int h = tabPane.getHeight() - insets.top - insets.bottom;
// remove tabs from bounds
switch( tabPlacement ) {
case TOP:
default:
y += calculateTabAreaHeight( tabPlacement, runCount, maxTabHeight );
y -= tabAreaInsets.bottom;
h -= (y - insets.top);
break;
case BOTTOM:
h -= calculateTabAreaHeight( tabPlacement, runCount, maxTabHeight );
h += tabAreaInsets.top;
break;
case LEFT:
x += calculateTabAreaWidth( tabPlacement, runCount, maxTabWidth );
x -= tabAreaInsets.right;
w -= (x - insets.left);
break;
case RIGHT:
w -= calculateTabAreaWidth( tabPlacement, runCount, maxTabWidth );
w += tabAreaInsets.left;
break;
}
// 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 );
// repaint selection in scroll-tab-layout because it may be painted before
// the content border was painted (from BasicTabbedPaneUI$ScrollableTabPanel)
if( isScrollTabLayout() && selectedIndex >= 0 && tabViewport != null ) {
Rectangle tabRect = getTabBounds( tabPane, selectedIndex );
// clip to "scrolling sides" of viewport
// (left and right if horizontal, top and bottom if vertical)
Shape oldClip = g.getClip();
Rectangle vr = tabViewport.getBounds();
if( isHorizontalTabPlacement() )
g.clipRect( vr.x, 0, vr.width, tabPane.getHeight() );
else
g.clipRect( 0, vr.y, tabPane.getWidth(), vr.height );
paintTabSelection( g, tabPlacement, tabRect.x, tabRect.y, tabRect.width, tabRect.height );
g.setClip( oldClip );
}
}
@Override
protected void paintFocusIndicator( Graphics g, int tabPlacement, Rectangle[] rects, int tabIndex,
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 )
{
// 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();
}
// icon placement
int verticalTextPosition;
int horizontalTextPosition;
switch( clientPropertyInt( tabPane, TABBED_PANE_TAB_ICON_PLACEMENT, LEADING ) ) {
default:
case LEADING: verticalTextPosition = CENTER; horizontalTextPosition = TRAILING; break;
case TRAILING: verticalTextPosition = CENTER; 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 );
// 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,
CENTER, getTabAlignment( tabIndex ), verticalTextPosition, horizontalTextPosition,
tabRect, iconRect, textRect, scale( textIconGapUnscaled ) );
// remove temporary client property
tabPane.putClientProperty( "html", null );
return clippedTitle;
}
@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 Rectangle getTabCloseBounds( int tabIndex, int x, int y, int w, int h, Rectangle dest ) {
int iconWidth = closeIcon.getIconWidth();
int iconHeight = closeIcon.getIconHeight();
Insets tabInsets = getTabInsets( tabPane.getTabPlacement(), tabIndex );
// use one-third of right/left tab insets as gap between tab text and close button
dest.x = isLeftToRight()
? (x + w - (tabInsets.right / 3 * 2) - iconWidth)
: (x + (tabInsets.left / 3 * 2));
dest.y = y + (h - iconHeight) / 2;
dest.width = iconWidth;
dest.height = iconHeight;
return dest;
}
protected Rectangle getTabCloseHitArea( int tabIndex ) {
Rectangle tabRect = getTabBounds( tabPane, tabIndex );
Rectangle tabCloseRect = getTabCloseBounds( tabIndex, tabRect.x, tabRect.y, tabRect.width, tabRect.height, calcRect );
return new Rectangle( tabCloseRect.x, tabRect.y, tabCloseRect.width, tabRect.height );
}
protected boolean isTabClosable( int tabIndex ) {
Object value = getTabClientProperty( tabIndex, TABBED_PANE_TAB_CLOSABLE );
return (value instanceof Boolean) ? (boolean) value : false;
}
@SuppressWarnings( { "unchecked" } )
protected void closeTab( int tabIndex ) {
Object callback = getTabClientProperty( tabIndex, TABBED_PANE_TAB_CLOSE_CALLBACK );
if( callback instanceof IntConsumer )
((IntConsumer)callback).accept( tabIndex );
else if( callback instanceof BiConsumer )
((BiConsumer)callback).accept( tabPane, tabIndex );
else {
throw new RuntimeException( "Missing tab close callback. "
+ "Set client property 'JTabbedPane.tabCloseCallback' "
+ "to a 'java.util.function.IntConsumer' "
+ "or 'java.util.function.BiConsumer'" );
}
}
protected Object getTabClientProperty( int tabIndex, String key ) {
if( tabIndex < 0 )
return null;
Component c = tabPane.getComponentAt( tabIndex );
if( c instanceof JComponent ) {
Object value = ((JComponent)c).getClientProperty( key );
if( value != null )
return value;
}
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
super.getTabRunCount( tabPane );
}
private boolean isLastInRun( int tabIndex ) {
int run = getRunForTab( tabPane.getTabCount(), tabIndex );
return lastTabInRun( tabPane.getTabCount(), run ) == tabIndex;
}
private boolean isScrollTabLayout() {
return tabPane.getTabLayoutPolicy() == JTabbedPane.SCROLL_TAB_LAYOUT;
}
private boolean isLeftToRight() {
return tabPane.getComponentOrientation().isLeftToRight();
}
protected boolean isHorizontalTabPlacement() {
int tabPlacement = tabPane.getTabPlacement();
return tabPlacement == TOP || tabPlacement == BOTTOM;
}
protected boolean isSmoothScrollingEnabled() {
if( !Animator.useAnimation() )
return false;
// Note: Getting UI value "ScrollPane.smoothScrolling" here to allow
// applications to turn smooth scrolling on or off at any time
// (e.g. in application options dialog).
return UIManager.getBoolean( "ScrollPane.smoothScrolling" );
}
protected boolean hideTabArea() {
return tabPane.getTabCount() == 1 &&
leadingComponent == null &&
trailingComponent == null &&
clientPropertyBoolean( tabPane, TABBED_PANE_HIDE_TAB_AREA_WITH_ONE_TAB, false );
}
protected int getTabsPopupPolicy() {
Object value = tabPane.getClientProperty( TABBED_PANE_TABS_POPUP_POLICY );
return (value instanceof String)
? parseTabsPopupPolicy( (String) value )
: tabsPopupPolicy;
}
protected int getScrollButtonsPolicy() {
Object value = tabPane.getClientProperty( TABBED_PANE_SCROLL_BUTTONS_POLICY );
return (value instanceof String)
? parseScrollButtonsPolicy( (String) value )
: scrollButtonsPolicy;
}
protected int getScrollButtonsPlacement() {
Object value = tabPane.getClientProperty( TABBED_PANE_SCROLL_BUTTONS_PLACEMENT );
return (value instanceof String)
? parseScrollButtonsPlacement( (String) value )
: scrollButtonsPlacement;
}
protected int getTabAreaAlignment() {
Object value = tabPane.getClientProperty( TABBED_PANE_TAB_AREA_ALIGNMENT );
if( value instanceof Integer )
return (Integer) value;
return (value instanceof String)
? parseAlignment( (String) value, LEADING )
: tabAreaAlignment;
}
protected int getTabAlignment( int tabIndex ) {
Object value = getTabClientProperty( tabIndex, TABBED_PANE_TAB_ALIGNMENT );
if( value instanceof Integer )
return (Integer) value;
return (value instanceof String)
? parseAlignment( (String) value, CENTER )
: tabAlignment;
}
protected int getTabWidthMode() {
Object value = tabPane.getClientProperty( TABBED_PANE_TAB_WIDTH_MODE );
return (value instanceof String)
? parseTabWidthMode( (String) value )
: tabWidthMode;
}
protected static int parseTabsPopupPolicy( String str ) {
if( str == null )
return AS_NEEDED;
switch( str ) {
default:
case TABBED_PANE_POLICY_AS_NEEDED: return AS_NEEDED;
case TABBED_PANE_POLICY_NEVER: return NEVER;
}
}
protected static int parseScrollButtonsPolicy( String str ) {
if( str == null )
return AS_NEEDED_SINGLE;
switch( str ) {
default:
case TABBED_PANE_POLICY_AS_NEEDED_SINGLE: return AS_NEEDED_SINGLE;
case TABBED_PANE_POLICY_AS_NEEDED: return AS_NEEDED;
case TABBED_PANE_POLICY_NEVER: return NEVER;
}
}
protected static int parseScrollButtonsPlacement( String str ) {
if( str == null )
return BOTH;
switch( str ) {
default:
case TABBED_PANE_PLACEMENT_BOTH: return BOTH;
case TABBED_PANE_PLACEMENT_TRAILING: return TRAILING;
}
}
protected static int parseAlignment( String str, int defaultValue ) {
if( str == null )
return defaultValue;
switch( str ) {
case TABBED_PANE_ALIGN_LEADING: return LEADING;
case TABBED_PANE_ALIGN_TRAILING: return TRAILING;
case TABBED_PANE_ALIGN_CENTER: return CENTER;
case TABBED_PANE_ALIGN_FILL: return FILL;
default: return defaultValue;
}
}
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 ) {
// 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 ensureSelectedTabIsVisibleLater() {
// do nothing if not yet displayable or if not invoked from dispatch thread,
// which may be the case when creating/modifying in another thread
if( !tabPane.isDisplayable() || !EventQueue.isDispatchThread() )
return;
EventQueue.invokeLater( () -> {
ensureSelectedTabIsVisible();
} );
}
protected void ensureSelectedTabIsVisible() {
if( tabPane == null || tabViewport == null || !tabPane.isDisplayable() )
return;
ensureCurrentLayout();
int selectedIndex = tabPane.getSelectedIndex();
if( selectedIndex < 0 || selectedIndex >= rects.length )
return;
((JComponent)tabViewport.getView()).scrollRectToVisible( (Rectangle) rects[selectedIndex].clone() );
}
private int getLeadingPreferredWidth() {
return (leadingComponent != null) ? leadingComponent.getPreferredSize().width : 0;
}
private int getLeadingPreferredHeight() {
return (leadingComponent != null) ? leadingComponent.getPreferredSize().height : 0;
}
private int getTrailingPreferredWidth() {
return (trailingComponent != null) ? trailingComponent.getPreferredSize().width : 0;
}
private int getTrailingPreferredHeight() {
return (trailingComponent != null) ? trailingComponent.getPreferredSize().height : 0;
}
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 );
}
}
private void stretchTabsWidth( int sw, boolean leftToRight ) {
int rsw = sw / rects.length;
int x = rects[0].x - (leftToRight ? 0 : rsw);
for( int i = 0; i < rects.length; i++ ) {
// fix tab component location
Component c = tabPane.getTabComponentAt( i );
if( c != null )
c.setLocation( x + (c.getX() - rects[i].x) + (rsw / 2), c.getY() );
// fix x location and width in rects
rects[i].x = x;
rects[i].width += rsw;
if( leftToRight )
x += rects[i].width;
else if( i + 1 < rects.length )
x = rects[i].x - rects[i+1].width - rsw;
}
// fix width of last tab
int diff = sw - (rsw * rects.length);
rects[rects.length-1].width += diff;
if( !leftToRight )
rects[rects.length-1].x -= diff;
}
private void stretchTabsHeight( int sh ) {
int rsh = sh / rects.length;
int y = rects[0].y;
for( int i = 0; i < rects.length; i++ ) {
// fix tab component location
Component c = tabPane.getTabComponentAt( i );
if( c != null )
c.setLocation( c.getX(), y + (c.getY() - rects[i].y) + (rsh / 2) );
// fix y location and height in rects
rects[i].y = y;
rects[i].height += rsh;
y += rects[i].height;
}
// fix height of last tab
rects[rects.length-1].height += (sh - (rsh * rects.length));
}
private int rectsTotalWidth( boolean leftToRight ) {
int last = rects.length - 1;
return leftToRight
? (rects[last].x + rects[last].width) - rects[0].x
: (rects[0].x + rects[0].width) - rects[last].x;
}
private int rectsTotalHeight() {
int last = rects.length - 1;
return (rects[last].y + rects[last].height) - rects[0].y;
}
//---- class TabCloseButton -----------------------------------------------
private class TabCloseButton
extends JButton
implements UIResource
{
private TabCloseButton() {
}
}
//---- class ContainerUIResource ------------------------------------------
private class ContainerUIResource
extends JPanel
implements UIResource
{
private ContainerUIResource( Component c ) {
super( new BorderLayout() );
add( c );
}
}
//---- class FlatTabAreaButton --------------------------------------------
protected class FlatTabAreaButton
extends FlatArrowButton
{
public FlatTabAreaButton( int direction ) {
super( direction, arrowType,
FlatTabbedPaneUI.this.foreground, FlatTabbedPaneUI.this.disabledForeground,
null, buttonHoverBackground, null, buttonPressedBackground );
setArrowWidth( 10 );
}
@Override
protected Color deriveBackground( Color background ) {
return FlatUIUtils.deriveColor( background, tabPane.getBackground() );
}
@Override
public void paint( Graphics g ) {
// fill button background
if( tabsOpaque || tabPane.isOpaque() ) {
g.setColor( tabPane.getBackground() );
g.fillRect( 0, 0, getWidth(), getHeight() );
}
super.paint( g );
}
@Override
protected void paintBackground( Graphics2D g ) {
// rotate button insets
Insets insets = new Insets( 0, 0, 0, 0 );
rotateInsets( buttonInsets, insets, tabPane.getTabPlacement() );
// use UIScale.scale2() here because this gives smaller insets at 150% and 175%
int top = UIScale.scale2( insets.top );
int left = UIScale.scale2( insets.left );
int bottom = UIScale.scale2( insets.bottom );
int right = UIScale.scale2( insets.right );
FlatUIUtils.paintComponentBackground( g, left, top,
getWidth() - left - right,
getHeight() - top - bottom,
0, scale( (float) buttonArc ) );
}
}
//---- class FlatMoreTabsButton -------------------------------------------
protected class FlatMoreTabsButton
extends FlatTabAreaButton
implements ActionListener, PopupMenuListener
{
private boolean popupVisible;
public FlatMoreTabsButton() {
super( SOUTH );
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 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
if( direction == EAST || direction == WEST ) {
int xoffset = Math.max( UIScale.unscale( (getWidth() - getHeight()) / 2 ) - 4, 0 );
setXOffset( (direction == EAST) ? xoffset : -xoffset );
} else
setXOffset( 0 );
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( createTabMenuItem( i ) );
}
}
// compute popup menu location
int buttonWidth = getWidth();
int buttonHeight = getHeight();
Dimension popupSize = popupMenu.getPreferredSize();
int x = isLeftToRight() ? 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 createTabMenuItem( int tabIndex ) {
// search for tab name in this places
// 1. tab title
// 2. text of label or text component in custom tab component (including children)
// 3. accessible name of tab
// 4. accessible name of custom tab component (including children)
// 5. string "n. Tab"
String title = tabPane.getTitleAt( tabIndex );
if( StringUtils.isEmpty( title ) ) {
Component tabComp = tabPane.getTabComponentAt( tabIndex );
if( tabComp != null )
title = findTabTitle( tabComp );
if( StringUtils.isEmpty( title ) )
title = tabPane.getAccessibleContext().getAccessibleChild( tabIndex ).getAccessibleContext().getAccessibleName();
if( StringUtils.isEmpty( title ) && tabComp instanceof Accessible )
title = findTabTitleInAccessible( (Accessible) tabComp );
if( StringUtils.isEmpty( title ) )
title = (tabIndex + 1) + ". Tab";
}
JMenuItem menuItem = new JMenuItem( title, tabPane.getIconAt( tabIndex ) );
menuItem.setDisabledIcon( tabPane.getDisabledIconAt( tabIndex ) );
menuItem.setToolTipText( tabPane.getToolTipTextAt( tabIndex ) );
Color foregroundAt = tabPane.getForegroundAt( tabIndex );
if( foregroundAt != tabPane.getForeground() )
menuItem.setForeground( foregroundAt );
Color backgroundAt = tabPane.getBackgroundAt( tabIndex );
if( backgroundAt != tabPane.getBackground() ) {
menuItem.setBackground( backgroundAt );
menuItem.setOpaque( true );
}
if( !tabPane.isEnabledAt( tabIndex ) )
menuItem.setEnabled( false );
menuItem.addActionListener( e -> selectTab( tabIndex ) );
return menuItem;
}
/**
* Search for label or text component in custom tab component and return its text.
*/
private String findTabTitle( Component c ) {
String title = null;
if( c instanceof JLabel )
title = ((JLabel)c).getText();
else if( c instanceof JTextComponent )
title = ((JTextComponent)c).getText();
if( !StringUtils.isEmpty( title ) )
return title;
if( c instanceof Container ) {
for( Component child : ((Container)c).getComponents() ) {
title = findTabTitle( child );
if( title != null )
return title;
}
}
return null;
}
/**
* Search for accessible name.
*/
private String findTabTitleInAccessible( Accessible accessible ) {
AccessibleContext context = accessible.getAccessibleContext();
if( context == null )
return null;
String title = context.getAccessibleName();
if( !StringUtils.isEmpty( title ) )
return title;
int childrenCount = context.getAccessibleChildrenCount();
for( int i = 0; i < childrenCount; i++ ) {
title = findTabTitleInAccessible( context.getAccessibleChild( i ) );
if( title != null )
return title;
}
return null;
}
protected void selectTab( int tabIndex ) {
tabPane.setSelectedIndex( tabIndex );
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
extends FlatTabAreaButton
implements MouseListener
{
private Timer autoRepeatTimer;
protected FlatScrollableTabButton( int direction ) {
super( direction );
addMouseListener( this );
}
@Override
protected void fireActionPerformed( ActionEvent event ) {
runWithOriginalLayoutManager( () -> {
super.fireActionPerformed( event );
} );
}
@Override
public void mousePressed( MouseEvent e ) {
if( SwingUtilities.isLeftMouseButton( e ) && isEnabled() ) {
if( autoRepeatTimer == null ) {
// using same delays as in BasicScrollBarUI and BasicSpinnerUI
autoRepeatTimer = new Timer( 60, e2 -> {
if( isEnabled() )
doClick();
} );
autoRepeatTimer.setInitialDelay( 300 );
}
autoRepeatTimer.start();
}
}
@Override
public void mouseReleased( MouseEvent e ) {
if( autoRepeatTimer != null )
autoRepeatTimer.stop();
}
@Override
public void mouseClicked( MouseEvent e ) {
}
@Override
public void mouseEntered( MouseEvent e ) {
if( autoRepeatTimer != null && isPressed() )
autoRepeatTimer.start();
}
@Override
public void mouseExited( MouseEvent e ) {
if( autoRepeatTimer != null )
autoRepeatTimer.stop();
}
}
//---- class FlatWheelTabScroller -----------------------------------------
protected class FlatWheelTabScroller
extends MouseAdapter
{
private int lastMouseX;
private int lastMouseY;
private boolean inViewport;
private boolean scrolled;
private Timer rolloverTimer;
private Timer exitedTimer;
private Animator animator;
private Point startViewPosition;
private Point targetViewPosition;
protected void uninstall() {
if( rolloverTimer != null )
rolloverTimer.stop();
if( exitedTimer != null )
exitedTimer.stop();
if( animator != null )
animator.cancel();
}
@Override
public void mouseWheelMoved( MouseWheelEvent e ) {
// disable wheel scrolling if application has added its own mouse wheel listener
if( tabPane.getMouseWheelListeners().length > 1 )
return;
// because this listener receives mouse events for the whole tabbed pane,
// we have to check whether the mouse is located over the viewport
if( !isInViewport( e.getX(), e.getY() ) )
return;
lastMouseX = e.getX();
lastMouseY = e.getY();
double preciseWheelRotation = e.getPreciseWheelRotation();
boolean isPreciseWheel = (preciseWheelRotation != 0 && preciseWheelRotation != e.getWheelRotation());
int amount = (int) (maxTabHeight * preciseWheelRotation);
// scroll at least one pixel to avoid "hanging"
if( amount == 0 ) {
if( preciseWheelRotation > 0 )
amount = 1;
else if( preciseWheelRotation < 0 )
amount = -1;
}
// compute new view position
Point viewPosition = (targetViewPosition != null)
? targetViewPosition
: tabViewport.getViewPosition();
Dimension viewSize = tabViewport.getViewSize();
boolean horizontal = isHorizontalTabPlacement();
int x = viewPosition.x;
int y = viewPosition.y;
if( horizontal )
x += isLeftToRight() ? amount : -amount;
else
y += amount;
// In case of having scroll buttons on both sides and hiding disabled buttons,
// the viewport is moved when the scroll backward button becomes visible
// or is hidden. For non-precise wheel scrolling (e.g. mouse wheel on Windows),
// this is no problem because the scroll amount is at least a tab-height.
// For precise wheel scrolling (e.g. touchpad on Mac), this is a problem
// because it is possible to scroll by a fraction of a tab-height.
if( isPreciseWheel &&
getScrollButtonsPlacement() == BOTH &&
getScrollButtonsPolicy() == AS_NEEDED_SINGLE &&
(isLeftToRight() || !horizontal) || // scroll buttons are hidden in right-to-left
scrollBackwardButtonPrefSize != null )
{
// special cases for scrolling with touchpad or high-resolution wheel:
// 1. if view is at 0/0 and scrolling right/down, then the scroll backward button
// becomes visible, which moves the viewport right/down by the width/height of
// the button --> add button width/height to new view position so that
// tabs seems to stay in place at screen
// 2. if scrolling left/up to the beginning, then the scroll backward button
// becomes hidden, which moves the viewport left/up by the width/height of
// the button --> set new view position to 0/0 so that
// tabs seems to stay in place at screen
if( horizontal ) {
//
if( viewPosition.x == 0 && x > 0 )
x += scrollBackwardButtonPrefSize.width;
else if( amount < 0 && x <= scrollBackwardButtonPrefSize.width )
x = 0;
} else {
if( viewPosition.y == 0 && y > 0 )
y += scrollBackwardButtonPrefSize.height;
else if( amount < 0 && y <= scrollBackwardButtonPrefSize.height )
y = 0;
}
}
// limit new view position
if( horizontal )
x = Math.min( Math.max( x, 0 ), viewSize.width - tabViewport.getWidth() );
else
y = Math.min( Math.max( y, 0 ), viewSize.height - tabViewport.getHeight() );
// check whether view position has changed
Point newViewPosition = new Point( x, y );
if( newViewPosition.equals( viewPosition ) )
return;
// update view position
if( isPreciseWheel ) {
// do not use animation for precise scrolling (e.g. with trackpad)
// stop running animation (if any)
if( animator != null )
animator.stop();
tabViewport.setViewPosition( newViewPosition );
updateRolloverDelayed();
} else
setViewPositionAnimated( newViewPosition );
scrolled = true;
}
protected void setViewPositionAnimated( Point viewPosition ) {
// check whether position is equal to current position
if( viewPosition.equals( tabViewport.getViewPosition() ) )
return;
// do not use animation if disabled
if( !isSmoothScrollingEnabled() ) {
tabViewport.setViewPosition( viewPosition );
updateRolloverDelayed();
return;
}
// remember start and target view positions
startViewPosition = tabViewport.getViewPosition();
targetViewPosition = viewPosition;
// create animator
if( animator == null ) {
// using same delays as in FlatScrollBarUI
int duration = 200;
int resolution = 10;
animator = new Animator( duration, fraction -> {
if( tabViewport == null || !tabViewport.isShowing() ) {
animator.stop();
return;
}
// update view position
int x = startViewPosition.x + Math.round( (targetViewPosition.x - startViewPosition.x) * fraction );
int y = startViewPosition.y + Math.round( (targetViewPosition.y - startViewPosition.y) * fraction );
tabViewport.setViewPosition( new Point( x, y ) );
}, () -> {
startViewPosition = targetViewPosition = null;
if( tabPane != null )
setRolloverTab( lastMouseX, lastMouseY );
} );
animator.setResolution( resolution );
animator.setInterpolator( new CubicBezierEasing( 0.5f, 0.5f, 0.5f, 1 ) );
}
// restart animator
animator.restart();
}
protected void updateRolloverDelayed() {
blockRollover = true;
// keep rollover on last tab until it would move to another tab, then clear it
int oldIndex = getRolloverTab();
if( oldIndex >= 0 ) {
int index = tabForCoordinate( tabPane, lastMouseX, lastMouseY );
if( index >= 0 && index != oldIndex ) {
// clear if moved to another tab
blockRollover = false;
setRolloverTab( -1 );
blockRollover = true;
}
}
// create timer
if( rolloverTimer == null ) {
rolloverTimer = new Timer( 150, e -> {
blockRollover = false;
// highlight tab at mouse location
if( tabPane != null )
setRolloverTab( lastMouseX, lastMouseY );
} );
rolloverTimer.setRepeats( false );
}
// restart timer
rolloverTimer.restart();
}
@Override
public void mouseMoved( MouseEvent e ) {
checkViewportExited( e.getX(), e.getY() );
}
@Override
public void mouseExited( MouseEvent e ) {
// this event occurs also if mouse is moved to a custom tab component
// that handles mouse events (e.g. a close button)
checkViewportExited( e.getX(), e.getY() );
}
@Override
public void mousePressed( MouseEvent e ) {
// for the case that the tab was only partly visible before the user clicked it
setRolloverTab( e.getX(), e.getY() );
}
protected boolean isInViewport( int x, int y ) {
return (tabViewport != null && tabViewport.getBounds().contains( x, y ) );
}
protected void checkViewportExited( int x, int y ) {
lastMouseX = x;
lastMouseY = y;
boolean wasInViewport = inViewport;
inViewport = isInViewport( x, y );
if( inViewport != wasInViewport ) {
if( !inViewport )
viewportExited();
else if( exitedTimer != null )
exitedTimer.stop();
}
}
protected void viewportExited() {
if( !scrolled )
return;
if( exitedTimer == null ) {
exitedTimer = new Timer( 500, e -> ensureSelectedTabVisible() );
exitedTimer.setRepeats( false );
}
exitedTimer.start();
}
protected void ensureSelectedTabVisible() {
// check whether UI delegate was uninstalled because this method is invoked via timer
if( tabPane == null || tabViewport == null )
return;
if( !scrolled || tabViewport == null )
return;
scrolled = false;
// scroll selected tab into visible area
ensureSelectedTabIsVisible();
}
}
//---- class Handler ------------------------------------------------------
private class Handler
implements MouseListener, MouseMotionListener, PropertyChangeListener,
ChangeListener, ComponentListener, ContainerListener
{
MouseListener mouseDelegate;
PropertyChangeListener propertyChangeDelegate;
ChangeListener changeDelegate;
private final PropertyChangeListener contentListener = this::contentPropertyChange;
private int pressedTabIndex = -1;
private int lastTipTabIndex = -1;
private String lastTip;
void installListeners() {
tabPane.addMouseMotionListener( this );
tabPane.addComponentListener( this );
tabPane.addContainerListener( this );
for( Component c : tabPane.getComponents() ) {
if( !(c instanceof UIResource) )
c.addPropertyChangeListener( contentListener );
}
}
void uninstallListeners() {
tabPane.removeMouseMotionListener( this );
tabPane.removeComponentListener( this );
tabPane.removeContainerListener( this );
for( Component c : tabPane.getComponents() ) {
if( !(c instanceof UIResource) )
c.removePropertyChangeListener( contentListener );
}
}
//---- interface MouseListener ----
@Override
public void mouseClicked( MouseEvent e ) {
mouseDelegate.mouseClicked( e );
}
@Override
public void mousePressed( MouseEvent e ) {
updateRollover( e );
if( !isPressedTabClose() )
mouseDelegate.mousePressed( e );
}
@Override
public void mouseReleased( MouseEvent e ) {
if( isPressedTabClose() ) {
updateRollover( e );
if( pressedTabIndex >= 0 && pressedTabIndex == getRolloverTab() ) {
restoreTabToolTip();
closeTab( pressedTabIndex );
}
} else
mouseDelegate.mouseReleased( e );
pressedTabIndex = -1;
updateRollover( e );
}
@Override
public void mouseEntered( MouseEvent e ) {
// this is necessary for "more tabs" button
updateRollover( e );
}
@Override
public void mouseExited( MouseEvent e ) {
// this event occurs also if mouse is moved to a custom tab component
// that handles mouse events (e.g. a close button)
// --> make sure that the tab stays highlighted
updateRollover( e );
}
//---- interface MouseMotionListener ----
@Override
public void mouseDragged( MouseEvent e ) {
updateRollover( e );
}
@Override
public void mouseMoved( MouseEvent e ) {
updateRollover( e );
}
private void updateRollover( MouseEvent e ) {
int x = e.getX();
int y = e.getY();
int tabIndex = tabForCoordinate( tabPane, x, y );
setRolloverTab( tabIndex );
// check whether mouse hit tab close area
boolean hitClose = isTabClosable( tabIndex )
? getTabCloseHitArea( tabIndex ).contains( x, y )
: false;
if( e.getID() == MouseEvent.MOUSE_PRESSED )
pressedTabIndex = hitClose ? tabIndex : -1;
setRolloverTabClose( hitClose );
setPressedTabClose( hitClose && tabIndex == pressedTabIndex );
// update tooltip
if( tabIndex >= 0 && hitClose ) {
Object closeTip = getTabClientProperty( tabIndex, TABBED_PANE_TAB_CLOSE_TOOLTIPTEXT );
if( closeTip instanceof String )
setCloseToolTip( tabIndex, (String) closeTip );
else
restoreTabToolTip();
} else
restoreTabToolTip();
}
private void setCloseToolTip( int tabIndex, String closeTip ) {
if( tabIndex == lastTipTabIndex )
return; // closeTip already set
if( tabIndex != lastTipTabIndex )
restoreTabToolTip();
lastTipTabIndex = tabIndex;
lastTip = tabPane.getToolTipTextAt( lastTipTabIndex );
tabPane.setToolTipTextAt( lastTipTabIndex, closeTip );
}
private void restoreTabToolTip() {
if( lastTipTabIndex < 0 )
return;
if( lastTipTabIndex < tabPane.getTabCount() )
tabPane.setToolTipTextAt( lastTipTabIndex, lastTip );
lastTip = null;
lastTipTabIndex = -1;
}
//---- interface PropertyChangeListener ----
@Override
public void propertyChange( PropertyChangeEvent 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 "componentOrientation":
ensureSelectedTabIsVisibleLater();
break;
case TABBED_PANE_SHOW_TAB_SEPARATORS:
case TABBED_PANE_SHOW_CONTENT_SEPARATOR:
case TABBED_PANE_HAS_FULL_BORDER:
case TABBED_PANE_HIDE_TAB_AREA_WITH_ONE_TAB:
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_TAB_AREA_INSETS:
case TABBED_PANE_TABS_POPUP_POLICY:
case TABBED_PANE_SCROLL_BUTTONS_POLICY:
case TABBED_PANE_SCROLL_BUTTONS_PLACEMENT:
case TABBED_PANE_TAB_AREA_ALIGNMENT:
case TABBED_PANE_TAB_ALIGNMENT:
case TABBED_PANE_TAB_WIDTH_MODE:
case TABBED_PANE_TAB_ICON_PLACEMENT:
case TABBED_PANE_TAB_CLOSABLE:
tabPane.revalidate();
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;
}
}
//---- interface ChangeListener ----
@Override
public void stateChanged( ChangeEvent e ) {
changeDelegate.stateChanged( e );
// scroll selected tab into visible area
if( moreTabsButton != null )
ensureSelectedTabIsVisible();
}
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_ALIGNMENT:
case TABBED_PANE_TAB_CLOSABLE:
tabPane.revalidate();
tabPane.repaint();
break;
}
}
//---- interface ComponentListener ----
@Override
public void componentResized( ComponentEvent e ) {
// make sure that selected tab stays visible when component size changed
ensureSelectedTabIsVisibleLater();
}
@Override public void componentMoved( ComponentEvent e ) {}
@Override public void componentShown( ComponentEvent e ) {}
@Override public void componentHidden( ComponentEvent e ) {}
//---- interface ContainerListener ----
@Override
public void componentAdded( ContainerEvent e ) {
Component c = e.getChild();
if( !(c instanceof UIResource) )
c.addPropertyChangeListener( contentListener );
}
@Override
public void componentRemoved( ContainerEvent e ) {
Component c = e.getChild();
if( !(c instanceof UIResource) )
c.removePropertyChangeListener( contentListener );
}
}
//---- class FlatTabbedPaneLayout -----------------------------------------
protected class FlatTabbedPaneLayout
extends TabbedPaneLayout
{
@Override
protected Dimension calculateSize( boolean minimum ) {
if( isContentEmpty() )
return calculateTabAreaSize();
return super.calculateSize( minimum );
}
/**
* Check whether all content components are either {@code null} or have zero preferred size.
*
* If {@code true}, assume that the tabbed pane is used without any content and
* use the size of the tab area (single run) as minimum/preferred size.
*/
protected boolean isContentEmpty() {
int tabCount = tabPane.getTabCount();
if( tabCount == 0 )
return false;
for( int i = 0; i < tabCount; i++ ) {
Component c = tabPane.getComponentAt( i );
if( c != null ) {
Dimension cs = c.getPreferredSize();
if( cs.width != 0 || cs.height != 0 )
return false;
}
}
return true;
}
protected Dimension calculateTabAreaSize() {
boolean horizontal = isHorizontalTabPlacement();
int tabPlacement = tabPane.getTabPlacement();
FontMetrics metrics = getFontMetrics();
int fontHeight = metrics.getHeight();
// calculate size of tabs
int width = 0;
int height = 0;
int tabCount = tabPane.getTabCount();
for( int i = 0; i < tabCount; i++ ) {
if( horizontal ) {
width += calculateTabWidth( tabPlacement, i, metrics );
height = Math.max( height, calculateTabHeight( tabPlacement, i, fontHeight ) );
} else {
width = Math.max( width, calculateTabWidth( tabPlacement, i, metrics ) );
height += calculateTabHeight( tabPlacement, i, fontHeight );
}
}
// add content separator thickness
if( horizontal )
height += scale( contentSeparatorHeight );
else
width += scale( contentSeparatorHeight );
// add insets
Insets insets = tabPane.getInsets();
Insets tabAreaInsets = getTabAreaInsets( tabPlacement );
return new Dimension(
width + insets.left + insets.right + tabAreaInsets.left + tabAreaInsets.right,
height + insets.bottom + insets.top + tabAreaInsets.top + tabAreaInsets.bottom );
}
@Override
public void layoutContainer( Container parent ) {
super.layoutContainer( parent );
Rectangle bounds = tabPane.getBounds();
Insets insets = tabPane.getInsets();
int tabPlacement = tabPane.getTabPlacement();
int tabAreaAlignment = getTabAreaAlignment();
Insets tabAreaInsets = getRealTabAreaInsets( tabPlacement );
boolean leftToRight = isLeftToRight();
// layout leading and trailing components in tab area
if( tabPlacement == TOP || tabPlacement == BOTTOM ) {
// fix x-locations of tabs in right-to-left component orientation
if( !leftToRight )
shiftTabs( insets.left + tabAreaInsets.right + getTrailingPreferredWidth(), 0 );
// tab area height (maxTabHeight is zero if tab count is zero)
int tabAreaHeight = (maxTabHeight > 0)
? maxTabHeight
: Math.max(
Math.max( getLeadingPreferredHeight(), getTrailingPreferredHeight() ),
scale( clientPropertyInt( tabPane, TABBED_PANE_TAB_HEIGHT, tabHeight ) ) );
// tab area bounds
int tx = insets.left;
int ty = (tabPlacement == TOP)
? insets.top + tabAreaInsets.top
: (bounds.height - insets.bottom - tabAreaInsets.bottom - tabAreaHeight);
int tw = bounds.width - insets.left - insets.right;
int th = tabAreaHeight;
int leadingWidth = getLeadingPreferredWidth();
int trailingWidth = getTrailingPreferredWidth();
// apply tab area alignment
if( runCount == 1 && rects.length > 0 ) {
int availWidth = tw - leadingWidth - trailingWidth - tabAreaInsets.left - tabAreaInsets.right;
int totalTabWidth = rectsTotalWidth( leftToRight );
int diff = availWidth - totalTabWidth;
switch( tabAreaAlignment ) {
case LEADING:
trailingWidth += diff;
break;
case TRAILING:
shiftTabs( leftToRight ? diff : -diff, 0 );
leadingWidth += diff;
break;
case CENTER:
shiftTabs( (leftToRight ? diff : -diff) / 2, 0 );
leadingWidth += diff / 2;
trailingWidth += diff - (diff / 2);
break;
case FILL:
stretchTabsWidth( diff, leftToRight );
break;
}
} else if( rects.length == 0 )
trailingWidth = tw - leadingWidth;
// layout left component
Container leftComponent = leftToRight ? leadingComponent : trailingComponent;
if( leftComponent != null ) {
int leftWidth = leftToRight ? leadingWidth : trailingWidth;
leftComponent.setBounds( tx, ty, leftWidth, th );
}
// layout right component
Container rightComponent = leftToRight ? trailingComponent : leadingComponent;
if( rightComponent != null ) {
int rightWidth = leftToRight ? trailingWidth : leadingWidth;
rightComponent.setBounds( tx + tw - rightWidth, ty, rightWidth, th );
}
} else { // LEFT and RIGHT tab placement
// tab area width (maxTabWidth is zero if tab count is zero)
int tabAreaWidth = (maxTabWidth > 0)
? maxTabWidth
: Math.max( getLeadingPreferredWidth(), getTrailingPreferredWidth() );
// tab area bounds
int tx = (tabPlacement == LEFT)
? insets.left + tabAreaInsets.left
: (bounds.width - insets.right - tabAreaInsets.right - tabAreaWidth);
int ty = insets.top;
int tw = tabAreaWidth;
int th = bounds.height - insets.top - insets.bottom;
int topHeight = getLeadingPreferredHeight();
int bottomHeight = getTrailingPreferredHeight();
// apply tab area alignment
if( runCount == 1 && rects.length > 0 ) {
int availHeight = th - topHeight - bottomHeight - tabAreaInsets.top - tabAreaInsets.bottom;
int totalTabHeight = rectsTotalHeight();
int diff = availHeight - totalTabHeight;
switch( tabAreaAlignment ) {
case LEADING:
bottomHeight += diff;
break;
case TRAILING:
shiftTabs( 0, diff );
topHeight += diff;
break;
case CENTER:
shiftTabs( 0, (diff) / 2 );
topHeight += diff / 2;
bottomHeight += diff - (diff / 2);
break;
case FILL:
stretchTabsHeight( diff );
break;
}
} else if( rects.length == 0 )
bottomHeight = th - topHeight;
// layout top component
if( leadingComponent != null )
leadingComponent.setBounds( tx, ty, tw, topHeight );
// layout bottom component
if( trailingComponent != null )
trailingComponent.setBounds( tx, ty + th - bottomHeight, tw, bottomHeight );
}
}
}
//---- class FlatTabbedPaneScrollLayout -----------------------------------
/**
* Layout manager used for scroll tab layout policy.
*
* Although this class delegates all methods to the original layout manager
* {@code 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 FlatTabbedPaneLayout
implements LayoutManager
{
private final TabbedPaneLayout delegate;
protected FlatTabbedPaneScrollLayout( TabbedPaneLayout delegate ) {
this.delegate = delegate;
}
@Override
public void calculateLayoutInfo() {
delegate.calculateLayoutInfo();
}
@Override
protected Dimension calculateTabAreaSize() {
Dimension size = super.calculateTabAreaSize();
// limit width/height in scroll layout
if( isHorizontalTabPlacement() )
size.width = Math.min( size.width, scale( 100 ) );
else
size.height = Math.min( size.height, scale( 100 ) );
return size;
}
//---- interface LayoutManager ----
@Override
public Dimension preferredLayoutSize( Container parent ) {
if( isContentEmpty() )
return calculateTabAreaSize();
return delegate.preferredLayoutSize( parent );
}
@Override
public Dimension minimumLayoutSize( Container parent ) {
if( isContentEmpty() )
return calculateTabAreaSize();
return delegate.minimumLayoutSize( parent );
}
@Override
public void addLayoutComponent( String name, Component comp ) {
delegate.addLayoutComponent( name, comp );
}
@Override
public void removeLayoutComponent( Component comp ) {
delegate.removeLayoutComponent( comp );
}
@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 );
} );
int tabsPopupPolicy = getTabsPopupPolicy();
int scrollButtonsPolicy = getScrollButtonsPolicy();
int scrollButtonsPlacement = getScrollButtonsPlacement();
boolean useMoreTabsButton = (tabsPopupPolicy == AS_NEEDED);
boolean useScrollButtons = (scrollButtonsPolicy == AS_NEEDED || scrollButtonsPolicy == AS_NEEDED_SINGLE);
boolean hideDisabledScrollButtons = (scrollButtonsPolicy == AS_NEEDED_SINGLE && scrollButtonsPlacement == BOTH);
boolean trailingScrollButtons = (scrollButtonsPlacement == TRAILING);
// for right-to-left always use "more tabs" button for horizontal scrolling
// because methods scrollForward() and scrollBackward() in class
// BasicTabbedPaneUI.ScrollableTabSupport do not work for right-to-left
boolean leftToRight = isLeftToRight();
if( !leftToRight && isHorizontalTabPlacement() ) {
useMoreTabsButton = true;
useScrollButtons = false;
}
// find backward/forward scroll buttons
JButton backwardButton = null;
JButton forwardButton = null;
for( Component c : tabPane.getComponents() ) {
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( backwardButton == null || forwardButton == null )
return; // should never occur
Rectangle bounds = tabPane.getBounds();
Insets insets = tabPane.getInsets();
int tabPlacement = tabPane.getTabPlacement();
int tabAreaAlignment = getTabAreaAlignment();
Insets tabAreaInsets = getRealTabAreaInsets( tabPlacement );
boolean moreTabsButtonVisible = false;
boolean backwardButtonVisible = false;
boolean forwardButtonVisible = false;
// 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 );
// 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) ) );
}
// layout tab area
if( tabPlacement == TOP || tabPlacement == BOTTOM ) {
// avoid that tab area "jump" to the right when backward button becomes hidden
if( useScrollButtons && hideDisabledScrollButtons ) {
Point viewPosition = tabViewport.getViewPosition();
if( viewPosition.x <= backwardButton.getPreferredSize().width )
tabViewport.setViewPosition( new Point( 0, viewPosition.y ) );
}
// tab area height (maxTabHeight is zero if tab count is zero)
int tabAreaHeight = (maxTabHeight > 0)
? maxTabHeight
: Math.max(
Math.max( getLeadingPreferredHeight(), getTrailingPreferredHeight() ),
scale( clientPropertyInt( tabPane, TABBED_PANE_TAB_HEIGHT, tabHeight ) ) );
// tab area bounds
int tx = insets.left;
int ty = (tabPlacement == TOP)
? insets.top + tabAreaInsets.top
: (bounds.height - insets.bottom - tabAreaInsets.bottom - tabAreaHeight);
int tw = bounds.width - insets.left - insets.right;
int th = tabAreaHeight;
int leadingWidth = getLeadingPreferredWidth();
int trailingWidth = getTrailingPreferredWidth();
int availWidth = tw - leadingWidth - trailingWidth - tabAreaInsets.left - tabAreaInsets.right;
int totalTabWidth = (rects.length > 0) ? rectsTotalWidth( leftToRight ) : 0;
// apply tab area alignment
if( totalTabWidth < availWidth && rects.length > 0 ) {
int diff = availWidth - totalTabWidth;
switch( tabAreaAlignment ) {
case LEADING:
trailingWidth += diff;
break;
case TRAILING:
leadingWidth += diff;
break;
case CENTER:
leadingWidth += diff / 2;
trailingWidth += diff - (diff / 2);
break;
case FILL:
stretchTabsWidth( diff, leftToRight );
totalTabWidth = rectsTotalWidth( leftToRight );
break;
}
} else if( rects.length == 0 )
trailingWidth = tw - leadingWidth;
// layout left component
Container leftComponent = leftToRight ? leadingComponent : trailingComponent;
int leftWidth = leftToRight ? leadingWidth : trailingWidth;
if( leftComponent != null )
leftComponent.setBounds( tx, ty, leftWidth, th );
// layout right component
Container rightComponent = leftToRight ? trailingComponent : leadingComponent;
int rightWidth = leftToRight ? trailingWidth : leadingWidth;
if( rightComponent != null )
rightComponent.setBounds( tx + tw - rightWidth, ty, rightWidth, th );
// layout tab viewport and buttons
if( rects.length > 0 ) {
int txi = tx + leftWidth + (leftToRight ? tabAreaInsets.left : tabAreaInsets.right);
int twi = tw - leftWidth - rightWidth - tabAreaInsets.left - tabAreaInsets.right;
// layout viewport and buttons
int x = txi;
int w = twi;
if( w < totalTabWidth ) {
// available width is too small for all tabs --> need buttons
// layout more button on trailing side
if( useMoreTabsButton ) {
int buttonWidth = moreTabsButton.getPreferredSize().width;
moreTabsButton.setBounds( leftToRight ? (x + w - buttonWidth) : x, ty, buttonWidth, th );
x += leftToRight ? 0 : buttonWidth;
w -= buttonWidth;
moreTabsButtonVisible = true;
}
if( useScrollButtons ) {
// layout forward button on trailing side
if( !hideDisabledScrollButtons || forwardButton.isEnabled() ) {
int buttonWidth = forwardButton.getPreferredSize().width;
forwardButton.setBounds( leftToRight ? (x + w - buttonWidth) : x, ty, buttonWidth, th );
x += leftToRight ? 0 : buttonWidth;
w -= buttonWidth;
forwardButtonVisible = true;
}
// layout backward button
if( !hideDisabledScrollButtons || backwardButton.isEnabled() ) {
int buttonWidth = backwardButton.getPreferredSize().width;
if( trailingScrollButtons ) {
// on trailing side
backwardButton.setBounds( leftToRight ? (x + w - buttonWidth) : x, ty, buttonWidth, th );
x += leftToRight ? 0 : buttonWidth;
} else {
// on leading side
backwardButton.setBounds( leftToRight ? x : (x + w - buttonWidth), ty, buttonWidth, th );
x += leftToRight ? buttonWidth : 0;
}
w -= buttonWidth;
backwardButtonVisible = true;
}
}
}
tabViewport.setBounds( x, ty, w, 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 and RIGHT tab placement
// avoid that tab area "jump" to the top when backward button becomes hidden
if( useScrollButtons && hideDisabledScrollButtons ) {
Point viewPosition = tabViewport.getViewPosition();
if( viewPosition.y <= backwardButton.getPreferredSize().height )
tabViewport.setViewPosition( new Point( viewPosition.x, 0 ) );
}
// tab area width (maxTabWidth is zero if tab count is zero)
int tabAreaWidth = (maxTabWidth > 0)
? maxTabWidth
: Math.max( getLeadingPreferredWidth(), getTrailingPreferredWidth() );
// tab area bounds
int tx = (tabPlacement == LEFT)
? insets.left + tabAreaInsets.left
: (bounds.width - insets.right - tabAreaInsets.right - tabAreaWidth);
int ty = insets.top;
int tw = tabAreaWidth;
int th = bounds.height - insets.top - insets.bottom;
int topHeight = getLeadingPreferredHeight();
int bottomHeight = getTrailingPreferredHeight();
int availHeight = th - topHeight - bottomHeight - tabAreaInsets.top - tabAreaInsets.bottom;
int totalTabHeight = (rects.length > 0) ? rectsTotalHeight() : 0;
// apply tab area alignment
if( totalTabHeight < availHeight && rects.length > 0 ) {
int diff = availHeight - totalTabHeight;
switch( tabAreaAlignment ) {
case LEADING:
bottomHeight += diff;
break;
case TRAILING:
topHeight += diff;
break;
case CENTER:
topHeight += diff / 2;
bottomHeight += diff - (diff / 2);
break;
case FILL:
stretchTabsHeight( diff );
totalTabHeight = rectsTotalHeight();
break;
}
} else if( rects.length == 0 )
bottomHeight = th - topHeight;
// layout top component
if( leadingComponent != null )
leadingComponent.setBounds( tx, ty, tw, topHeight );
// layout bottom component
if( trailingComponent != null )
trailingComponent.setBounds( tx, ty + th - bottomHeight, tw, bottomHeight );
// layout tab viewport and buttons
if( rects.length > 0 ) {
int tyi = ty + topHeight + tabAreaInsets.top;
int thi = th - topHeight - bottomHeight - tabAreaInsets.top - tabAreaInsets.bottom;
// layout viewport and buttons
int y = tyi;
int h = thi;
if( h < totalTabHeight ) {
// available height is too small for all tabs --> need buttons
// layout more button on bottom side
if( useMoreTabsButton ) {
int buttonHeight = moreTabsButton.getPreferredSize().height;
moreTabsButton.setBounds( tx, y + h - buttonHeight, tw, buttonHeight );
h -= buttonHeight;
moreTabsButtonVisible = true;
}
if( useScrollButtons ) {
// layout forward button on bottom side
if( !hideDisabledScrollButtons || forwardButton.isEnabled() ) {
int buttonHeight = forwardButton.getPreferredSize().height;
forwardButton.setBounds( tx, y + h - buttonHeight, tw, buttonHeight );
h -= buttonHeight;
forwardButtonVisible = true;
}
// layout backward button
if( !hideDisabledScrollButtons || backwardButton.isEnabled() ) {
int buttonHeight = backwardButton.getPreferredSize().height;
if( trailingScrollButtons ) {
// on bottom side
backwardButton.setBounds( tx, y + h - buttonHeight, tw, buttonHeight );
} else {
// on top side
backwardButton.setBounds( tx, y, tw, buttonHeight );
y += buttonHeight;
}
h -= buttonHeight;
backwardButtonVisible = true;
}
}
}
tabViewport.setBounds( tx, y, tw, h );
}
}
// show/hide viewport and buttons
tabViewport.setVisible( rects.length > 0 );
moreTabsButton.setVisible( moreTabsButtonVisible );
backwardButton.setVisible( backwardButtonVisible );
forwardButton.setVisible( forwardButtonVisible );
scrollBackwardButtonPrefSize = backwardButton.getPreferredSize();
}
}
//---- class RunWithOriginalLayoutManagerDelegateAction -------------------
private static class RunWithOriginalLayoutManagerDelegateAction
implements Action
{
private final Action delegate;
static void install( ActionMap map, String key ) {
Action oldAction = map.get( key );
if( oldAction == null || oldAction instanceof RunWithOriginalLayoutManagerDelegateAction )
return; // not found or already installed
map.put( key, new RunWithOriginalLayoutManagerDelegateAction( oldAction ) );
}
private RunWithOriginalLayoutManagerDelegateAction( Action delegate ) {
this.delegate = delegate;
}
@Override
public Object getValue( String key ) {
return delegate.getValue( key );
}
@Override
public boolean isEnabled() {
return delegate.isEnabled();
}
@Override public void putValue( String key, Object value ) {}
@Override public void setEnabled( boolean b ) {}
@Override public void addPropertyChangeListener( PropertyChangeListener listener ) {}
@Override public void removePropertyChangeListener( PropertyChangeListener listener ) {}
@Override
public void actionPerformed( ActionEvent e ) {
JTabbedPane tabbedPane = (JTabbedPane) e.getSource();
ComponentUI ui = tabbedPane.getUI();
if( ui instanceof FlatTabbedPaneUI ) {
((FlatTabbedPaneUI)ui).runWithOriginalLayoutManager( () -> {
delegate.actionPerformed( e );
} );
} else
delegate.actionPerformed( e );
}
}
}