diff --git a/CHANGELOG.md b/CHANGELOG.md index 023c94a9..c38b53b7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -16,6 +16,7 @@ FlatLaf Change Log - 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) +- TabbedPane: Support closable tabs. (issues #31 and #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 974d7a85..6a3a2c6f 100644 --- a/flatlaf-core/src/main/java/com/formdev/flatlaf/FlatClientProperties.java +++ b/flatlaf-core/src/main/java/com/formdev/flatlaf/FlatClientProperties.java @@ -25,6 +25,8 @@ import javax.swing.JComponent; */ public interface FlatClientProperties { + //---- JButton ------------------------------------------------------------ + /** * Specifies type of a button. *
@@ -104,6 +106,8 @@ public interface FlatClientProperties */ String SQUARE_SIZE = "JButton.squareSize"; + //---- JComponent --------------------------------------------------------- + /** * Specifies minimum width of a component. *
@@ -158,6 +162,8 @@ public interface FlatClientProperties */ String COMPONENT_ROUND_RECT = "JComponent.roundRect"; + //---- Popup -------------------------------------------------------------- + /** * Specifies whether a drop shadow is painted if the component is shown in a popup * or if the component is the owner of another component that is shown in a popup. @@ -167,6 +173,8 @@ public interface FlatClientProperties */ String POPUP_DROP_SHADOW_PAINTED = "Popup.dropShadowPainted"; + //---- JProgressBar ------------------------------------------------------- + /** * Specifies whether the progress bar has always the larger height even if no string is painted. *
@@ -183,6 +191,8 @@ public interface FlatClientProperties */ String PROGRESS_BAR_SQUARE = "JProgressBar.square"; + //---- JRootPane ---------------------------------------------------------- + /** * Specifies whether the menu bar is embedded into the title pane if custom * window decorations are enabled. Default is {@code true}. @@ -192,6 +202,8 @@ public interface FlatClientProperties */ String MENU_BAR_EMBEDDED = "JRootPane.menuBarEmbedded"; + //---- JScrollBar --------------------------------------------------------- + /** * Specifies whether the decrease/increase arrow buttons of a scrollbar are shown. *
@@ -208,6 +220,8 @@ public interface FlatClientProperties */ String SCROLL_PANE_SMOOTH_SCROLLING = "JScrollPane.smoothScrolling"; + //---- JTabbedPane -------------------------------------------------------- + /** * Specifies whether separators are shown between tabs. *
@@ -240,6 +254,78 @@ public interface FlatClientProperties */ String TABBED_PANE_TAB_HEIGHT = "JTabbedPane.tabHeight"; + /** + * Specifies the insets of a tab. + *
+ * Component {@link javax.swing.JTabbedPane}
+ * or tab content components (see {@link javax.swing.JTabbedPane#setComponentAt(int, java.awt.Component)})
+ * Value type {@link java.awt.Insets}
+ */
+ String TABBED_PANE_TAB_INSETS = "JTabbedPane.tabInsets";
+
+ /**
+ * Specifies whether tabs are closable.
+ * If set to {@code true} on a tabbed pane component, all tabs in that tabbed pane are closable.
+ * To make individual tabs closable, set it to {@code true} on a tab content component.
+ *
+ * Note that you have to specify a callback (see {@link #TABBED_PANE_TAB_CLOSABLE}) + * that is invoked when the user clicks a tab close button. + * The callback is responsible for closing the tab. + *
+ * Component {@link javax.swing.JTabbedPane}
+ * or tab content components (see {@link javax.swing.JTabbedPane#setComponentAt(int, java.awt.Component)})
+ * Value type {@link java.lang.Boolean}
+ *
+ * @see #TABBED_PANE_TAB_CLOSE_CALLBACK
+ */
+ String TABBED_PANE_TAB_CLOSABLE = "JTabbedPane.tabClosable";
+
+ /**
+ * Specifies the tooltip text used for tab close buttons.
+ *
+ * Component {@link javax.swing.JTabbedPane}
+ * or tab content components (see {@link javax.swing.JTabbedPane#setComponentAt(int, java.awt.Component)})
+ * Value type {@link java.lang.String}
+ */
+ String TABBED_PANE_TAB_CLOSE_TOOLTIPTEXT = "JTabbedPane.tabCloseToolTipText";
+
+ /**
+ * Specifies the callback that is invoked when a tab close button is clicked.
+ * The callback is responsible for closing the tab.
+ *
+ * Either use a {@link java.util.function.IntConsumer} that received the tab index as parameter: + *
{@code
+ * myTabbedPane.putClientProperty( "JTabbedPane.tabCloseCallback",
+ * (IntConsumer) tabIndex -> {
+ * // close tab here
+ * } );
+ * }
+ * Or use a {@link java.util.function.BiConsumer}<javax.swing.JTabbedPane, Integer>
+ * that received the tabbed pane and the tab index as parameters:
+ * {@code
+ * myTabbedPane.putClientProperty( "JTabbedPane.tabCloseCallback",
+ * (BiConsumer) (tabbedPane, tabIndex) -> {
+ * // close tab here
+ * } );
+ * }
+ * If you need to check whether a modifier key (e.g. Alt or Shift) was pressed
+ * while the user clicked the tab close button, use {@link java.awt.EventQueue#getCurrentEvent}
+ * to get current event, check whether it is a {@link java.awt.event.MouseEvent}
+ * and invoke its methods. E.g.
+ * {@code
+ * AWTEvent e = EventQueue.getCurrentEvent();
+ * boolean shift = (e instanceof MouseEvent) ? ((MouseEvent)e).isShiftDown() : false;
+ * }
+ *
+ * Component {@link javax.swing.JTabbedPane}
+ * or tab content components (see {@link javax.swing.JTabbedPane#setComponentAt(int, java.awt.Component)})
+ * Value type {@link java.util.function.IntConsumer}
+ * or {@link java.util.function.BiConsumer}<javax.swing.JTabbedPane, Integer>
+ *
+ * @see #TABBED_PANE_TAB_CLOSABLE
+ */
+ String TABBED_PANE_TAB_CLOSE_CALLBACK = "JTabbedPane.tabCloseCallback";
+
/**
* Specifies how to navigate to hidden tabs.
*
@@ -280,6 +366,8 @@ public interface FlatClientProperties */ String TABBED_PANE_TRAILING_COMPONENT = "JTabbedPane.trailingComponent"; + //---- JTextField --------------------------------------------------------- + /** * Specifies whether all text is selected when the text component gains focus. *
@@ -322,6 +410,8 @@ public interface FlatClientProperties */ String PLACEHOLDER_TEXT = "JTextField.placeholderText"; + //---- JToggleButton ------------------------------------------------------ + /** * Height of underline if toggle button type is {@link #BUTTON_TYPE_TAB}. *
@@ -346,6 +436,8 @@ public interface FlatClientProperties
*/
String TAB_BUTTON_SELECTED_BACKGROUND = "JToggleButton.tab.selectedBackground";
+ //---- helper methods -----------------------------------------------------
+
/**
* Checks whether a client property of a component has the given value.
*/
diff --git a/flatlaf-core/src/main/java/com/formdev/flatlaf/icons/FlatTabbedPaneCloseIcon.java b/flatlaf-core/src/main/java/com/formdev/flatlaf/icons/FlatTabbedPaneCloseIcon.java
new file mode 100644
index 00000000..fa5f5114
--- /dev/null
+++ b/flatlaf-core/src/main/java/com/formdev/flatlaf/icons/FlatTabbedPaneCloseIcon.java
@@ -0,0 +1,90 @@
+/*
+ * Copyright 2020 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
+ *
+ * https://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.icons;
+
+import java.awt.BasicStroke;
+import java.awt.Color;
+import java.awt.Component;
+import java.awt.Dimension;
+import java.awt.Graphics2D;
+import java.awt.geom.Line2D;
+import java.awt.geom.Path2D;
+import javax.swing.UIManager;
+import com.formdev.flatlaf.ui.FlatButtonUI;
+import com.formdev.flatlaf.ui.FlatUIUtils;
+
+/**
+ * "close" icon for closable tabs in {@link javax.swing.JTabbedPane}.
+ *
+ * @uiDefault TabbedPane.closeSize Dimension
+ * @uiDefault TabbedPane.closeArc int
+ * @uiDefault TabbedPane.closeCrossPlainSize float
+ * @uiDefault TabbedPane.closeCrossFilledSize float
+ * @uiDefault TabbedPane.closeCrossLineWidth float
+ * @uiDefault TabbedPane.closeBackground Color
+ * @uiDefault TabbedPane.closeForeground Color
+ * @uiDefault TabbedPane.closeHoverBackground Color
+ * @uiDefault TabbedPane.closeHoverForeground Color
+ * @uiDefault TabbedPane.closePressedBackground Color
+ * @uiDefault TabbedPane.closePressedForeground Color
+ *
+ * @author Karl Tauber
+ */
+public class FlatTabbedPaneCloseIcon
+ extends FlatAbstractIcon
+{
+ protected final Dimension size = UIManager.getDimension( "TabbedPane.closeSize" );
+ protected final int arc = UIManager.getInt( "TabbedPane.closeArc" );
+ protected final float crossPlainSize = FlatUIUtils.getUIFloat( "TabbedPane.closeCrossPlainSize", 7.5f );
+ protected final float crossFilledSize = FlatUIUtils.getUIFloat( "TabbedPane.closeCrossFilledSize", crossPlainSize );
+ protected final float closeCrossLineWidth = FlatUIUtils.getUIFloat( "TabbedPane.closeCrossLineWidth", 1f );
+ protected final Color background = UIManager.getColor( "TabbedPane.closeBackground" );
+ protected final Color foreground = UIManager.getColor( "TabbedPane.closeForeground" );
+ protected final Color hoverBackground = UIManager.getColor( "TabbedPane.closeHoverBackground" );
+ protected final Color hoverForeground = UIManager.getColor( "TabbedPane.closeHoverForeground" );
+ protected final Color pressedBackground = UIManager.getColor( "TabbedPane.closePressedBackground" );
+ protected final Color pressedForeground = UIManager.getColor( "TabbedPane.closePressedForeground" );
+
+ public FlatTabbedPaneCloseIcon() {
+ super( 16, 16, null );
+ }
+
+ @Override
+ protected void paintIcon( Component c, Graphics2D g ) {
+ // paint background
+ Color bg = FlatButtonUI.buttonStateColor( c, background, null, null, hoverBackground, pressedBackground );
+ if( bg != null ) {
+ g.setColor( bg );
+ g.fillRoundRect( (width - size.width) / 2, (height - size.height) / 2,
+ size.width, size.height, arc, arc );
+ }
+
+ // set cross color
+ g.setColor( FlatButtonUI.buttonStateColor( c, foreground, null, null, hoverForeground, pressedForeground ) );
+
+ float mx = width / 2;
+ float my = height / 2;
+ float r = ((bg != null) ? crossFilledSize : crossPlainSize) / 2;
+
+ // paint cross
+ Path2D path = new Path2D.Float( Path2D.WIND_EVEN_ODD );
+ path.append( new Line2D.Float( mx - r, my - r, mx + r, my + r ), false );
+ path.append( new Line2D.Float( mx - r, my + r, mx + r, my - r ), false );
+ g.setStroke( new BasicStroke( closeCrossLineWidth ) );
+ g.draw( path );
+ }
+}
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 b3d83fab..d24e3f90 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
@@ -38,11 +38,14 @@ 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;
@@ -51,6 +54,10 @@ 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.swing.ButtonModel;
+import javax.swing.Icon;
import javax.swing.JButton;
import javax.swing.JComponent;
import javax.swing.JMenuItem;
@@ -90,7 +97,7 @@ import com.formdev.flatlaf.util.UIScale;
* @uiDefault TabbedPane.shadow Color used for scroll arrows and cropped line
* @uiDefault TabbedPane.textIconGap int
* @uiDefault TabbedPane.tabInsets Insets
- * @uiDefault TabbedPane.selectedTabPadInsets Insets
+ * @uiDefault TabbedPane.selectedTabPadInsets Insets unused
* @uiDefault TabbedPane.tabAreaInsets Insets
* @uiDefault TabbedPane.tabsOverlapBorder boolean
* @uiDefault TabbedPane.tabRunOverlay int
@@ -119,6 +126,7 @@ import com.formdev.flatlaf.util.UIScale;
* @uiDefault TabbedPane.hasFullBorder boolean
* @uiDefault TabbedPane.hiddenTabsNavigation String moreTabsButton (default) or arrowButtons
* @uiDefault ScrollPane.smoothScrolling boolean
+ * @uiDefault TabbedPane.closeIcon Icon
*
* @uiDefault TabbedPane.moreTabsButtonToolTipText String
*
@@ -144,6 +152,7 @@ public class FlatTabbedPaneUI
protected Color tabSeparatorColor;
protected Color contentAreaColor;
+ private int textIconGapUnscaled;
protected int tabHeight;
protected int tabSelectionHeight;
protected int contentSeparatorHeight;
@@ -152,18 +161,22 @@ public class FlatTabbedPaneUI
protected boolean hasFullBorder;
private String hiddenTabsNavigationStr;
+ protected Icon closeIcon;
protected String moreTabsButtonToolTipText;
protected JViewport tabViewport;
protected FlatWheelTabScroller wheelTabScroller;
+ private JButton tabCloseButton;
private JButton moreTabsButton;
private Container leadingComponent;
private Container trailingComponent;
private Handler handler;
private boolean blockRollover;
+ private boolean rolloverTabClose;
+ private boolean pressedTabClose;
public static ComponentUI createUI( JComponent c ) {
return new FlatTabbedPaneUI();
@@ -203,6 +216,7 @@ public class FlatTabbedPaneUI
tabSeparatorColor = UIManager.getColor( "TabbedPane.tabSeparatorColor" );
contentAreaColor = UIManager.getColor( "TabbedPane.contentAreaColor" );
+ textIconGapUnscaled = UIManager.getInt( "TabbedPane.textIconGap" );
tabHeight = UIManager.getInt( "TabbedPane.tabHeight" );
tabSelectionHeight = UIManager.getInt( "TabbedPane.tabSelectionHeight" );
contentSeparatorHeight = UIManager.getInt( "TabbedPane.contentSeparatorHeight" );
@@ -210,17 +224,13 @@ public class FlatTabbedPaneUI
tabSeparatorsFullHeight = UIManager.getBoolean( "TabbedPane.tabSeparatorsFullHeight" );
hasFullBorder = UIManager.getBoolean( "TabbedPane.hasFullBorder" );
hiddenTabsNavigationStr = UIManager.getString( "TabbedPane.hiddenTabsNavigation" );
+ closeIcon = UIManager.getIcon( "TabbedPane.closeIcon" );
Locale l = tabPane.getLocale();
moreTabsButtonToolTipText = UIManager.getString( "TabbedPane.moreTabsButtonToolTipText", l );
// scale
- textIconGap = scale( textIconGap );
- tabInsets = scale( tabInsets );
- selectedTabPadInsets = scale( selectedTabPadInsets );
- tabAreaInsets = scale( tabAreaInsets );
- tabHeight = scale( tabHeight );
- tabSelectionHeight = scale( tabSelectionHeight );
+ 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
@@ -255,6 +265,8 @@ public class FlatTabbedPaneUI
tabSeparatorColor = null;
contentAreaColor = null;
+ closeIcon = null;
+
MigLayoutVisualPadding.uninstall( tabPane );
}
@@ -262,6 +274,11 @@ public class FlatTabbedPaneUI
protected void installComponents() {
super.installComponents();
+ // create tab close button
+ tabCloseButton = new TabCloseButton();
+ tabCloseButton.setVisible( false );
+ tabPane.add( tabCloseButton );
+
// find scrollable tab viewport
tabViewport = null;
if( isScrollTabLayout() ) {
@@ -288,6 +305,11 @@ public class FlatTabbedPaneUI
super.uninstallComponents();
+ if( tabCloseButton != null ) {
+ tabPane.remove( tabCloseButton );
+ tabCloseButton = null;
+ }
+
tabViewport = null;
}
@@ -354,9 +376,7 @@ public class FlatTabbedPaneUI
protected void installListeners() {
super.installListeners();
- tabPane.addMouseListener( getHandler() );
- tabPane.addMouseMotionListener( getHandler() );
- tabPane.addComponentListener( getHandler() );
+ getHandler().installListeners();
if( tabViewport != null && (wheelTabScroller = createWheelTabScroller()) != null ) {
// ideally we would add the mouse listeners to the viewport, but then the
@@ -373,9 +393,7 @@ public class FlatTabbedPaneUI
super.uninstallListeners();
if( handler != null ) {
- tabPane.removeMouseListener( handler );
- tabPane.removeMouseMotionListener( handler );
- tabPane.removeComponentListener( handler );
+ handler.uninstallListeners();
handler = null;
}
@@ -399,6 +417,13 @@ public class FlatTabbedPaneUI
return new FlatWheelTabScroller();
}
+ @Override
+ protected MouseListener createMouseListener() {
+ Handler handler = getHandler();
+ handler.mouseDelegate = super.createMouseListener();
+ return handler;
+ }
+
@Override
protected PropertyChangeListener createPropertyChangeListener() {
Handler handler = getHandler();
@@ -454,6 +479,30 @@ public class FlatTabbedPaneUI
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;
@@ -465,15 +514,34 @@ public class FlatTabbedPaneUI
@Override
protected int calculateTabWidth( int tabPlacement, int tabIndex, FontMetrics metrics ) {
- return super.calculateTabWidth( tabPlacement, tabIndex, metrics ) - 3 /* was added by superclass */;
+ // update textIconGap before used in super class
+ textIconGap = scale( textIconGapUnscaled );
+
+ int tabWidth = super.calculateTabWidth( tabPlacement, tabIndex, metrics ) - 3 /* was added by superclass */;
+ if( isTabClosable( tabIndex ) )
+ tabWidth += closeIcon.getIconWidth();
+ return tabWidth;
}
@Override
protected int calculateTabHeight( int tabPlacement, int tabIndex, int fontHeight ) {
- int tabHeight = clientPropertyInt( tabPane, TABBED_PANE_TAB_HEIGHT, this.tabHeight );
+ int tabHeight = scale( clientPropertyInt( tabPane, TABBED_PANE_TAB_HEIGHT, this.tabHeight ) );
return Math.max( tabHeight, super.calculateTabHeight( tabPlacement, tabIndex, fontHeight ) - 2 /* was added by superclass */ );
}
+ @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 );
+ }
+
@Override
protected Insets getTabAreaInsets( int tabPlacement ) {
Insets currentTabAreaInsets = super.getTabAreaInsets( tabPlacement );
@@ -486,6 +554,9 @@ public class FlatTabbedPaneUI
// 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 );
+
// increase insets for wrap layout if using leading/trailing components
if( tabPane.getTabLayoutPolicy() == JTabbedPane.WRAP_TAB_LAYOUT ) {
if( leadingComponent != null ) {
@@ -528,6 +599,10 @@ public class FlatTabbedPaneUI
@Override
protected int getTabLabelShiftX( int tabPlacement, int tabIndex, boolean isSelected ) {
+ if( isTabClosable( tabIndex ) ) {
+ int shift = closeIcon.getIconWidth() / 2;
+ return isLeftToRight() ? -shift : shift;
+ }
return 0;
}
@@ -619,6 +694,10 @@ public class FlatTabbedPaneUI
protected void paintTabBorder( Graphics g, int tabPlacement, int tabIndex,
int x, int y, int w, int h, boolean isSelected )
{
+ // paint tab close button
+ if( isTabClosable( tabIndex ) )
+ paintTabCloseButton( g, tabIndex, x, y, w, h );
+
// paint tab separators
if( clientPropertyBoolean( tabPane, TABBED_PANE_SHOW_TAB_SEPARATORS, showTabSeparators ) &&
!isLastInRun( tabIndex ) )
@@ -628,6 +707,18 @@ public class FlatTabbedPaneUI
paintTabSelection( g, tabPlacement, x, y, w, h );
}
+ protected void paintTabCloseButton( Graphics g, int tabIndex, int x, int y, int w, int h ) {
+ // update state of tab close button
+ boolean rollover = (tabIndex == getRolloverTab());
+ ButtonModel bm = tabCloseButton.getModel();
+ bm.setRollover( rollover && isRolloverTabClose() );
+ bm.setPressed( rollover && isPressedTabClose() );
+
+ // paint tab close icon
+ Rectangle tabCloseRect = getTabCloseBounds( 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 );
@@ -636,7 +727,7 @@ public class FlatTabbedPaneUI
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( tabPane.getComponentOrientation().isLeftToRight() ) {
+ } 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 {
@@ -682,6 +773,7 @@ public class FlatTabbedPaneUI
Insets contentInsets = getContentBorderInsets( tabPlacement );
// paint underline selection
+ int tabSelectionHeight = scale( this.tabSelectionHeight );
switch( tabPlacement ) {
case TOP:
default:
@@ -788,6 +880,16 @@ public class FlatTabbedPaneUI
{
}
+ @Override
+ protected void layoutLabel( int tabPlacement, FontMetrics metrics, int tabIndex, String title, Icon icon,
+ Rectangle tabRect, Rectangle iconRect, Rectangle textRect, boolean isSelected )
+ {
+ // update textIconGap before used in super class
+ textIconGap = scale( textIconGapUnscaled );
+
+ super.layoutLabel( tabPlacement, metrics, tabIndex, title, icon, tabRect, iconRect, textRect, isSelected );
+ }
+
@Override
public int tabForCoordinate( JTabbedPane pane, int x, int y ) {
if( moreTabsButton != null ) {
@@ -819,6 +921,55 @@ public class FlatTabbedPaneUI
return super.getTabBounds( tabIndex, dest );
}
+ protected Rectangle getTabCloseBounds( int x, int y, int w, int h, Rectangle dest ) {
+ dest.width = closeIcon.getIconWidth();
+ dest.height = closeIcon.getIconHeight();
+ int xoffset = (h - dest.width) / 2;
+ int yoffset = (h - dest.height) / 2;
+ dest.x = isLeftToRight() ? (x + w - xoffset - dest.width) : (x + xoffset);
+ dest.y = y + yoffset;
+ return dest;
+ }
+
+ protected Rectangle getTabCloseHitArea( int tabIndex ) {
+ Rectangle tabRect = getTabBounds( tabPane, tabIndex );
+ Rectangle tabCloseRect = getTabCloseBounds( 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
* Although this class delegates all methods to the original layout manager
- * {@link BasicTabbedPaneUI.TabbedPaneScrollLayout}, which extends
+ * {@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().
@@ -1603,7 +1902,7 @@ public class FlatTabbedPaneUI
// 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 = tabPane.getComponentOrientation().isLeftToRight();
+ boolean leftToRight = isLeftToRight();
if( !leftToRight && !useMoreButton && isHorizontalTabPlacement() )
useMoreButton = true;
@@ -1623,6 +1922,13 @@ public class FlatTabbedPaneUI
if( !useMoreButton && (backwardButton == null || forwardButton == null) )
return; // should never occur
+ if( rects.length == 0 ) {
+ moreTabsButton.setVisible( false );
+ backwardButton.setVisible( false );
+ forwardButton.setVisible( false );
+ return;
+ }
+
Rectangle bounds = tabPane.getBounds();
Insets insets = tabPane.getInsets();
int tabPlacement = tabPane.getTabPlacement();
diff --git a/flatlaf-core/src/main/resources/com/formdev/flatlaf/FlatDarkLaf.properties b/flatlaf-core/src/main/resources/com/formdev/flatlaf/FlatDarkLaf.properties
index a334cd0c..85e6d7c2 100644
--- a/flatlaf-core/src/main/resources/com/formdev/flatlaf/FlatDarkLaf.properties
+++ b/flatlaf-core/src/main/resources/com/formdev/flatlaf/FlatDarkLaf.properties
@@ -258,6 +258,13 @@ TabbedPane.hoverColor=#2e3133
TabbedPane.focusColor=#3d4b5c
TabbedPane.contentAreaColor=#646464
+TabbedPane.closeBackground=null
+TabbedPane.closeForeground=@disabledText
+TabbedPane.closeHoverBackground=lighten($TabbedPane.hoverColor,10%)
+TabbedPane.closeHoverForeground=@foreground
+TabbedPane.closePressedBackground=lighten($TabbedPane.hoverColor,15%)
+TabbedPane.closePressedForeground=$TabbedPane.closeHoverForeground
+
#---- Table ----
diff --git a/flatlaf-core/src/main/resources/com/formdev/flatlaf/FlatLaf.properties b/flatlaf-core/src/main/resources/com/formdev/flatlaf/FlatLaf.properties
index 8559cc12..6a05180c 100644
--- a/flatlaf-core/src/main/resources/com/formdev/flatlaf/FlatLaf.properties
+++ b/flatlaf-core/src/main/resources/com/formdev/flatlaf/FlatLaf.properties
@@ -552,6 +552,13 @@ TabbedPane.shadow=@background
TabbedPane.contentBorderInsets=null
TabbedPane.hiddenTabsNavigation=moreTabsButton
+TabbedPane.closeIcon=com.formdev.flatlaf.icons.FlatTabbedPaneCloseIcon
+TabbedPane.closeSize=16,16
+TabbedPane.closeArc=4
+TabbedPane.closeCrossPlainSize={float}7.5
+TabbedPane.closeCrossFilledSize=$TabbedPane.closeCrossPlainSize
+TabbedPane.closeCrossLineWidth={float}1
+
#---- Table ----
diff --git a/flatlaf-core/src/main/resources/com/formdev/flatlaf/FlatLightLaf.properties b/flatlaf-core/src/main/resources/com/formdev/flatlaf/FlatLightLaf.properties
index 8870bdb8..9bc0e28c 100644
--- a/flatlaf-core/src/main/resources/com/formdev/flatlaf/FlatLightLaf.properties
+++ b/flatlaf-core/src/main/resources/com/formdev/flatlaf/FlatLightLaf.properties
@@ -270,6 +270,13 @@ TabbedPane.hoverColor=#d9d9d9
TabbedPane.focusColor=#dae4ed
TabbedPane.contentAreaColor=#bfbfbf
+TabbedPane.closeBackground=null
+TabbedPane.closeForeground=@disabledText
+TabbedPane.closeHoverBackground=darken($TabbedPane.hoverColor,10%)
+TabbedPane.closeHoverForeground=@foreground
+TabbedPane.closePressedBackground=darken($TabbedPane.hoverColor,15%)
+TabbedPane.closePressedForeground=$TabbedPane.closeHoverForeground
+
#---- Table ----
diff --git a/flatlaf-extras/src/main/java/com/formdev/flatlaf/extras/TriStateCheckBox.java b/flatlaf-extras/src/main/java/com/formdev/flatlaf/extras/TriStateCheckBox.java
index 4a9d28d2..a60c45ef 100644
--- a/flatlaf-extras/src/main/java/com/formdev/flatlaf/extras/TriStateCheckBox.java
+++ b/flatlaf-extras/src/main/java/com/formdev/flatlaf/extras/TriStateCheckBox.java
@@ -95,6 +95,19 @@ public class TriStateCheckBox
repaint();
}
+ public Boolean getValue() {
+ switch( state ) {
+ default:
+ case INDETERMINATE: return null;
+ case SELECTED: return true;
+ case UNSELECTED: return false;
+ }
+ }
+
+ public void setValue( Boolean value ) {
+ setState( value == null ? State.INDETERMINATE : (value ? State.SELECTED : State.UNSELECTED) );
+ }
+
public boolean isThirdStateEnabled() {
return thirdStateEnabled;
}
diff --git a/flatlaf-testing/dumps/uidefaults/FlatDarkLaf_1.8.0_202.txt b/flatlaf-testing/dumps/uidefaults/FlatDarkLaf_1.8.0_202.txt
index cd66c85b..5308a7a0 100644
--- a/flatlaf-testing/dumps/uidefaults/FlatDarkLaf_1.8.0_202.txt
+++ b/flatlaf-testing/dumps/uidefaults/FlatDarkLaf_1.8.0_202.txt
@@ -905,6 +905,17 @@ SplitPaneUI com.formdev.flatlaf.ui.FlatSplitPaneUI
#---- TabbedPane ----
TabbedPane.background #3c3f41 javax.swing.plaf.ColorUIResource [UI]
+TabbedPane.closeArc 4
+TabbedPane.closeCrossFilledSize 7.5
+TabbedPane.closeCrossLineWidth 1.0
+TabbedPane.closeCrossPlainSize 7.5
+TabbedPane.closeForeground #888888 javax.swing.plaf.ColorUIResource [UI]
+TabbedPane.closeHoverBackground #464b4e javax.swing.plaf.ColorUIResource [UI]
+TabbedPane.closeHoverForeground #bbbbbb javax.swing.plaf.ColorUIResource [UI]
+TabbedPane.closeIcon [lazy] 16,16 com.formdev.flatlaf.icons.FlatTabbedPaneCloseIcon [UI]
+TabbedPane.closePressedBackground #52585b javax.swing.plaf.ColorUIResource [UI]
+TabbedPane.closePressedForeground #bbbbbb javax.swing.plaf.ColorUIResource [UI]
+TabbedPane.closeSize 16,16 javax.swing.plaf.DimensionUIResource [UI]
TabbedPane.contentAreaColor #646464 javax.swing.plaf.ColorUIResource [UI]
TabbedPane.contentOpaque true
TabbedPane.contentSeparatorHeight 1
diff --git a/flatlaf-testing/dumps/uidefaults/FlatLightLaf_1.8.0_202.txt b/flatlaf-testing/dumps/uidefaults/FlatLightLaf_1.8.0_202.txt
index e179a14e..df5e628a 100644
--- a/flatlaf-testing/dumps/uidefaults/FlatLightLaf_1.8.0_202.txt
+++ b/flatlaf-testing/dumps/uidefaults/FlatLightLaf_1.8.0_202.txt
@@ -910,6 +910,17 @@ SplitPaneUI com.formdev.flatlaf.ui.FlatSplitPaneUI
#---- TabbedPane ----
TabbedPane.background #f2f2f2 javax.swing.plaf.ColorUIResource [UI]
+TabbedPane.closeArc 4
+TabbedPane.closeCrossFilledSize 7.5
+TabbedPane.closeCrossLineWidth 1.0
+TabbedPane.closeCrossPlainSize 7.5
+TabbedPane.closeForeground #8c8c8c javax.swing.plaf.ColorUIResource [UI]
+TabbedPane.closeHoverBackground #c0c0c0 javax.swing.plaf.ColorUIResource [UI]
+TabbedPane.closeHoverForeground #000000 javax.swing.plaf.ColorUIResource [UI]
+TabbedPane.closeIcon [lazy] 16,16 com.formdev.flatlaf.icons.FlatTabbedPaneCloseIcon [UI]
+TabbedPane.closePressedBackground #b3b3b3 javax.swing.plaf.ColorUIResource [UI]
+TabbedPane.closePressedForeground #000000 javax.swing.plaf.ColorUIResource [UI]
+TabbedPane.closeSize 16,16 javax.swing.plaf.DimensionUIResource [UI]
TabbedPane.contentAreaColor #bfbfbf javax.swing.plaf.ColorUIResource [UI]
TabbedPane.contentOpaque true
TabbedPane.contentSeparatorHeight 1
diff --git a/flatlaf-testing/dumps/uidefaults/FlatTestLaf_1.8.0_202.txt b/flatlaf-testing/dumps/uidefaults/FlatTestLaf_1.8.0_202.txt
index d49664a1..aab155aa 100644
--- a/flatlaf-testing/dumps/uidefaults/FlatTestLaf_1.8.0_202.txt
+++ b/flatlaf-testing/dumps/uidefaults/FlatTestLaf_1.8.0_202.txt
@@ -898,6 +898,17 @@ SplitPaneUI com.formdev.flatlaf.ui.FlatSplitPaneUI
#---- TabbedPane ----
TabbedPane.background #ccffcc javax.swing.plaf.ColorUIResource [UI]
+TabbedPane.closeArc 999
+TabbedPane.closeCrossFilledSize 6.5
+TabbedPane.closeCrossLineWidth 2.0
+TabbedPane.closeCrossPlainSize 12.0
+TabbedPane.closeForeground #ff0000 javax.swing.plaf.ColorUIResource [UI]
+TabbedPane.closeHoverBackground #aa0000 javax.swing.plaf.ColorUIResource [UI]
+TabbedPane.closeHoverForeground #ffffff javax.swing.plaf.ColorUIResource [UI]
+TabbedPane.closeIcon [lazy] 16,16 com.formdev.flatlaf.icons.FlatTabbedPaneCloseIcon [UI]
+TabbedPane.closePressedBackground #00ff00 javax.swing.plaf.ColorUIResource [UI]
+TabbedPane.closePressedForeground #000000 javax.swing.plaf.ColorUIResource [UI]
+TabbedPane.closeSize 16,16 javax.swing.plaf.DimensionUIResource [UI]
TabbedPane.contentAreaColor #bbbbbb javax.swing.plaf.ColorUIResource [UI]
TabbedPane.contentOpaque true
TabbedPane.contentSeparatorHeight 1
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 d7386af4..4a295902 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
@@ -18,9 +18,12 @@ package com.formdev.flatlaf.testing;
import static com.formdev.flatlaf.FlatClientProperties.*;
import java.awt.*;
+import java.awt.event.MouseEvent;
+import java.util.function.BiConsumer;
import javax.swing.*;
import javax.swing.border.*;
import com.formdev.flatlaf.FlatLaf;
+import com.formdev.flatlaf.extras.TriStateCheckBox;
import com.formdev.flatlaf.icons.FlatInternalFrameCloseIcon;
import com.formdev.flatlaf.util.ScaledImageIcon;
import com.jgoodies.forms.layout.*;
@@ -49,6 +52,10 @@ public class FlatContainerTest
addInitialTabs( tabbedPane1, tabbedPane2, tabbedPane3, tabbedPane4 );
initialTabCount = tabbedPane1.getTabCount();
+ tabsClosableCheckBox.setSelected( true );
+ tabsClosableChanged();
+ putTabbedPanesClientProperty( TABBED_PANE_TAB_CLOSE_TOOLTIPTEXT, "Close" );
+
tabScrollCheckBox.setSelected( true );
tabScrollChanged();
}
@@ -123,14 +130,15 @@ public class FlatContainerTest
private void addInitialTabs( JTabbedPane... tabbedPanes ) {
for( JTabbedPane tabbedPane : tabbedPanes ) {
- tabbedPane.addTab( "Tab 1", new Panel1() );
+ tabbedPane.addTab( "Tab 1", null, new Panel1(), "First tab." );
JComponent tab2 = new Panel2();
tab2.setBorder( new LineBorder( Color.magenta ) );
- tabbedPane.addTab( "Second Tab", tab2 );
+ tabbedPane.addTab( "Second Tab", null, tab2, "This is the second tab." );
addTab( tabbedPane, "Disabled", "tab content 3" );
tabbedPane.setEnabledAt( 2, false );
+ tabbedPane.setToolTipTextAt( 2, "Disabled tab." );
tabbedPane.addTab( "Tab 4", new JLabel( "non-opaque content", SwingConstants.CENTER ) );
}
@@ -236,10 +244,7 @@ public class FlatContainerTest
case "arrowButtons": value = TABBED_PANE_HIDDEN_TABS_NAVIGATION_ARROW_BUTTONS; break;
}
- tabbedPane1.putClientProperty( TABBED_PANE_HIDDEN_TABS_NAVIGATION, value );
- tabbedPane2.putClientProperty( TABBED_PANE_HIDDEN_TABS_NAVIGATION, value );
- tabbedPane3.putClientProperty( TABBED_PANE_HIDDEN_TABS_NAVIGATION, value );
- tabbedPane4.putClientProperty( TABBED_PANE_HIDDEN_TABS_NAVIGATION, value );
+ putTabbedPanesClientProperty( TABBED_PANE_HIDDEN_TABS_NAVIGATION, value );
}
private void tabBackForegroundChanged() {
@@ -270,6 +275,52 @@ public class FlatContainerTest
}
}
+ private void tabsClosableChanged() {
+ boolean closable = tabsClosableCheckBox.isSelected();
+ putTabbedPanesClientProperty( TABBED_PANE_TAB_CLOSABLE, closable ? true : null );
+
+ if( closable ) {
+ putTabbedPanesClientProperty( TABBED_PANE_TAB_CLOSE_CALLBACK,
+ (BiConsumer