From 72797a420141dadfd23d28f62cfdc7c521008959 Mon Sep 17 00:00:00 2001 From: boa Date: Fri, 13 Feb 2026 13:27:10 +0200 Subject: [PATCH] Add card tab type support for JideTabbedPane FlatLaf supports the "card" tab type for standard JTabbedPane via UIManager.put("TabbedPane.tabType", "card"), but this style was not available for JideTabbedPane. This commit adds card tab rendering directly into FlatJideTabbedPaneUI. Changes: - Read TabbedPane.tabType, cardTabSelectionHeight, and cardTabArc UI defaults - Support per-component override via JTabbedPane.tabType client property - Paint rounded card tab background when cardTabArc > 0 - Paint card tab border frame around the selected tab - Paint card tab selection stripe at the outer edge of the selected tab - Paint content separator gap under the selected card tab - Skip tab separators adjacent to the selected card tab --- .../jideoss/ui/FlatJideTabbedPaneUI.java | 173 +++++++++++++++++- 1 file changed, 166 insertions(+), 7 deletions(-) diff --git a/flatlaf-jide-oss/src/main/java/com/formdev/flatlaf/jideoss/ui/FlatJideTabbedPaneUI.java b/flatlaf-jide-oss/src/main/java/com/formdev/flatlaf/jideoss/ui/FlatJideTabbedPaneUI.java index f34589419..312988708 100644 --- a/flatlaf-jide-oss/src/main/java/com/formdev/flatlaf/jideoss/ui/FlatJideTabbedPaneUI.java +++ b/flatlaf-jide-oss/src/main/java/com/formdev/flatlaf/jideoss/ui/FlatJideTabbedPaneUI.java @@ -36,6 +36,7 @@ import java.awt.Shape; import java.awt.event.MouseListener; import java.awt.event.MouseMotionListener; +import java.awt.geom.Area; import java.awt.geom.Path2D; import java.awt.geom.Rectangle2D; import java.beans.PropertyChangeListener; @@ -81,6 +82,13 @@ public class FlatJideTabbedPaneUI protected boolean hasFullBorder; protected boolean tabsOverlapBorder; + private static final int TAB_TYPE_UNDERLINED = 0; + private static final int TAB_TYPE_CARD = 1; + + private int tabType; + private int cardTabSelectionHeight; + private int cardTabArc; + protected Icon closeIcon; protected Icon arrowIcon; @@ -152,6 +160,10 @@ protected void installDefaults() { buttonHoverBackground = UIManager.getColor( "TabbedPane.buttonHoverBackground" ); buttonPressedBackground = UIManager.getColor( "TabbedPane.buttonPressedBackground" ); + tabType = parseTabType( UIManager.getString( "TabbedPane.tabType" ) ); + cardTabSelectionHeight = UIManager.getInt( "TabbedPane.cardTabSelectionHeight" ); + cardTabArc = UIManager.getInt( "TabbedPane.cardTabArc" ); + closeButtonLeftMarginUnscaled = _closeButtonLeftMargin; closeButtonRightMarginUnscaled = _closeButtonRightMargin; @@ -356,15 +368,23 @@ protected void paintTabBackground( Graphics g, int tabPlacement, int tabIndex, { // paint tab background boolean enabled = _tabPane.isEnabled(); - g.setColor( enabled && _tabPane.isEnabledAt( tabIndex ) && - (_indexMouseOver == tabIndex || (_closeButtons != null && ((JideTabbedPane.NoFocusButton)_closeButtons[tabIndex]).isMouseOver())) + boolean hovering = enabled && _tabPane.isEnabledAt( tabIndex ) && + (_indexMouseOver == tabIndex || (_closeButtons != null && + _closeButtons[tabIndex] instanceof JideTabbedPane.NoFocusButton && + ((JideTabbedPane.NoFocusButton)_closeButtons[tabIndex]).isMouseOver())); + + g.setColor( hovering ? hoverColor : (enabled && isSelected && FlatUIUtils.isPermanentFocusOwner( _tabPane ) ? focusColor : (selectedBackground != null && enabled && isSelected ? selectedBackground : _tabPane.getBackgroundAt( tabIndex ))) ); - g.fillRect( x, y, w, h ); + + if( isCardTabType() && cardTabArc > 0 ) + ((Graphics2D) g).fill( createCardTabOuterPath( tabPlacement, x, y, w, h ) ); + else + g.fillRect( x, y, w, h ); } @Override @@ -412,11 +432,19 @@ protected void paintTabBorder( Graphics g, int tabPlacement, int tabIndex, int x { // paint tab separators if( clientPropertyBoolean( _tabPane, TABBED_PANE_SHOW_TAB_SEPARATORS, showTabSeparators ) && - !isLastInRun( tabIndex ) ) - paintTabSeparator( g, tabPlacement, x, y, w, h ); + !isLastInRun( tabIndex ) ) { + if( !isCardTabType() || !isSelected ) { + int selectedIndex = _tabPane.getSelectedIndex(); + if( !isCardTabType() || (tabIndex != selectedIndex - 1 && tabIndex != selectedIndex) ) + paintTabSeparator( g, tabPlacement, x, y, w, h ); + } + } - if( isSelected ) + if( isSelected ) { + if( isCardTabType() ) + paintCardTabBorder( g, tabPlacement, x, y, w, h ); paintTabSelection( g, tabPlacement, x, y, w, h ); + } } protected void paintTabSeparator( Graphics g, int tabPlacement, int x, int y, int w, int h ) { @@ -440,6 +468,11 @@ protected void paintTabSelection( Graphics g, int tabPlacement, int x, int y, i if( !_tabPane.isTabShown() ) return; + if( isCardTabType() ) { + paintCardTabSelection( g, tabPlacement, x, y, w, h ); + return; + } + // increase clip bounds in scroll-tab-layout to paint over the separator line Rectangle clipBounds = scrollableTabLayoutEnabled() ? g.getClipBounds() : null; if( clipBounds != null ) { @@ -554,11 +587,43 @@ protected void paintContentBorder( Graphics g, int tabPlacement, int selectedInd rotateInsets( hasFullBorder ? new Insets( sh, sh, sh, sh ) : new Insets( sh, 0, 0, 0 ), ci, tabPlacement ); // paint content area - 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 ); + + // card tab gap for the selected tab + if( isCardTabType() && selectedIndex >= 0 ) { + float csh = scale( (float) contentSeparatorHeight ); + Rectangle tabRect = getTabBounds( _tabPane, selectedIndex ); + Rectangle2D.Float inner = new Rectangle2D.Float( + tabRect.x + csh, tabRect.y + csh, + tabRect.width - csh * 2, tabRect.height - csh * 2 ); + + if( scrollableTabLayoutEnabled() && _tabScroller != null && _tabScroller.viewport != null ) + Rectangle2D.intersect( _tabScroller.viewport.getBounds(), inner, inner ); + + Rectangle2D.Float gap = null; + boolean horiz = (tabPlacement == TOP || tabPlacement == BOTTOM); + if( horiz && inner.width > 0 ) { + float gy = (tabPlacement == TOP) ? y : y + h - csh; + gap = new Rectangle2D.Float( inner.x, gy, inner.width, csh ); + } else if( !horiz && inner.height > 0 ) { + float gx = (tabPlacement == LEFT) ? x : x + w - csh; + gap = new Rectangle2D.Float( gx, inner.y, csh, inner.height ); + } + + if( gap != null ) { + path.append( gap, false ); + + // fill gap with the tab's background colour + Color bg = getSelectedTabBackground( tabPlacement, selectedIndex ); + g.setColor( FlatUIUtils.deriveColor( bg, _tabPane.getBackground() ) ); + ((Graphics2D) g).fill( gap ); + } + } + + g.setColor( contentAreaColor ); ((Graphics2D)g).fill( path ); // repaint selection in scroll-tab-layout because it may be painted before @@ -627,6 +692,100 @@ private boolean isHorizontalTabPlacement() { return tabPlacement == TOP || tabPlacement == BOTTOM; } + //---- card tab type helpers ----------------------------------------------- + + private static int parseTabType( String str ) { + return "card".equals( str ) ? TAB_TYPE_CARD : TAB_TYPE_UNDERLINED; + } + + private int getTabType() { + Object value = _tabPane.getClientProperty( "JTabbedPane.tabType" ); + if( value instanceof String ) + return parseTabType( (String) value ); + return tabType; + } + + private boolean isCardTabType() { + return getTabType() == TAB_TYPE_CARD; + } + + private Shape createCardTabOuterPath( int tabPlacement, int x, int y, int w, int h ) { + float arc = scale( (float) cardTabArc ) / 2f; + switch( tabPlacement ) { + default: + case TOP: return FlatUIUtils.createRoundRectanglePath( x, y, w, h, arc, arc, 0, 0 ); + case BOTTOM: return FlatUIUtils.createRoundRectanglePath( x, y, w, h, 0, 0, arc, arc ); + case LEFT: return FlatUIUtils.createRoundRectanglePath( x, y, w, h, arc, 0, arc, 0 ); + case RIGHT: return FlatUIUtils.createRoundRectanglePath( x, y, w, h, 0, arc, 0, arc ); + } + } + + private Shape createCardTabInnerPath( int tabPlacement, int x, int y, int w, int h ) { + float bw = scale( (float) contentSeparatorHeight ); + float arc = (scale( (float) cardTabArc ) / 2f) - bw; + switch( tabPlacement ) { + default: + case TOP: return FlatUIUtils.createRoundRectanglePath( x + bw, y + bw, w - bw * 2, h - bw, arc, arc, 0, 0 ); + case BOTTOM: return FlatUIUtils.createRoundRectanglePath( x + bw, y, w - bw * 2, h - bw, 0, 0, arc, arc ); + case LEFT: return FlatUIUtils.createRoundRectanglePath( x + bw, y + bw, w - bw, h - bw * 2, arc, 0, arc, 0 ); + case RIGHT: return FlatUIUtils.createRoundRectanglePath( x, y + bw, w - bw, h - bw * 2, 0, arc, 0, arc ); + } + } + + private void paintCardTabBorder( Graphics g, int tabPlacement, int x, int y, int w, int h ) { + Path2D path = new Path2D.Float( Path2D.WIND_EVEN_ODD ); + path.append( createCardTabOuterPath( tabPlacement, x, y, w, h ), false ); + path.append( createCardTabInnerPath( tabPlacement, x, y, w, h ), false ); + + g.setColor( (tabSeparatorColor != null) ? tabSeparatorColor : contentAreaColor ); + ((Graphics2D) g).fill( path ); + } + + private void paintCardTabSelection( Graphics g, int tabPlacement, int x, int y, int w, int h ) { + g.setColor( _tabPane.isEnabled() ? underlineColor : disabledUnderlineColor ); + + int selH = scale( cardTabSelectionHeight ); + float arc = scale( (float) cardTabArc ) / 2f; + int sx = x, sy = y, sw = w, sh = h; + + switch( tabPlacement ) { + case TOP: + default: + sy = y; + sh = selH; + break; + case BOTTOM: + sy = y + h - selH; + sh = selH; + break; + case LEFT: + sx = x; + sw = selH; + break; + case RIGHT: + sx = x + w - selH; + sw = selH; + break; + } + + if( arc <= 0 ) + g.fillRect( sx, sy, sw, sh ); + else { + Area area = new Area( createCardTabOuterPath( tabPlacement, x, y, w, h ) ); + area.intersect( new Area( new Rectangle2D.Float( sx, sy, sw, sh ) ) ); + ((Graphics2D) g).fill( area ); + } + } + + private Color getSelectedTabBackground( int tabPlacement, int tabIndex ) { + boolean enabled = _tabPane.isEnabled(); + if( enabled && FlatUIUtils.isPermanentFocusOwner( _tabPane ) && focusColor != null ) + return focusColor; + if( selectedBackground != null && enabled ) + return selectedBackground; + return _tabPane.getBackgroundAt( tabIndex ); + } + @Override protected void ensureCurrentRects( int leftMargin, int tabCount ) { int oldFitStyleBoundSize = _fitStyleBoundSize;