TabbedPane: Added activeTabBorder option to tabbed panes. Changes the active tab design to more closely resemble classic tabbed pane designs.

This commit is contained in:
DUDSS
2021-06-20 02:13:54 +02:00
parent 8a72b30cbc
commit 4c13271a5b
3 changed files with 238 additions and 29 deletions

View File

@@ -14,8 +14,8 @@
* limitations under the License.
*/
package com.formdev.flatlaf;
package com.formdev.flatlaf;
import java.awt.Color;
import java.util.Objects;
import javax.swing.JComponent;
@@ -332,6 +332,15 @@ public interface FlatClientProperties
*/
String TABBED_PANE_SHOW_TAB_SEPARATORS = "JTabbedPane.showTabSeparators";
/**
* Specifies whether a border is painted around the active tab.
* This also changes position of the active tab indicator.
* <p>
* <strong>Component</strong> {@link javax.swing.JTabbedPane}<br>
* <strong>Value type</strong> {@link java.lang.Boolean}
*/
String TABBED_PANE_ACTIVE_TAB_BORDER = "JTabbedPane.activeTabBorder";
/**
* Specifies whether the separator between tabs area and content area should be shown.
* <p>

View File

@@ -40,6 +40,8 @@ import java.awt.event.ComponentEvent;
import java.awt.event.ComponentListener;
import java.awt.event.ContainerEvent;
import java.awt.event.ContainerListener;
import java.awt.event.FocusEvent;
import java.awt.event.FocusListener;
import java.awt.event.InputEvent;
import java.awt.event.KeyEvent;
import java.awt.event.MouseAdapter;
@@ -47,6 +49,7 @@ import java.awt.event.MouseEvent;
import java.awt.event.MouseListener;
import java.awt.event.MouseMotionListener;
import java.awt.event.MouseWheelEvent;
import java.awt.geom.Area;
import java.awt.geom.Path2D;
import java.awt.geom.Rectangle2D;
import java.beans.PropertyChangeEvent;
@@ -197,6 +200,7 @@ public class FlatTabbedPaneUI
protected boolean tabSeparatorsFullHeight;
protected boolean hasFullBorder;
protected boolean tabsOpaque = true;
protected boolean activeTabBorder;
private int tabsPopupPolicy;
private int scrollButtonsPolicy;
@@ -230,6 +234,8 @@ public class FlatTabbedPaneUI
private boolean rolloverTabClose;
private boolean pressedTabClose;
private FocusListener contentBorderFocusListener;
private Object[] oldRenderingHints;
public static ComponentUI createUI( JComponent c ) {
@@ -304,6 +310,7 @@ public class FlatTabbedPaneUI
tabSeparatorsFullHeight = UIManager.getBoolean( "TabbedPane.tabSeparatorsFullHeight" );
hasFullBorder = UIManager.getBoolean( "TabbedPane.hasFullBorder" );
tabsOpaque = UIManager.getBoolean( "TabbedPane.tabsOpaque" );
activeTabBorder = UIManager.getBoolean( "TabbedPane.activeTabBorder" );
tabsPopupPolicy = parseTabsPopupPolicy( UIManager.getString( "TabbedPane.tabsPopupPolicy" ) );
scrollButtonsPolicy = parseScrollButtonsPolicy( UIManager.getString( "TabbedPane.scrollButtonsPolicy" ) );
@@ -471,6 +478,23 @@ public class FlatTabbedPaneUI
tabPane.addMouseMotionListener( wheelTabScroller );
tabPane.addMouseListener( wheelTabScroller );
}
// Fix for content border repainting issue (when activeTabBorder is on) //TODO: Find a better solution perhaps
/* Basically the content separator paints part of itself with the color of the active tab.
* When the active tab has focus, and focus is lost to a different component (outside the tabbed pane)
* the content separator does not get repainted, but the active tab background does. I don't know why.
*/
contentBorderFocusListener = new FocusListener()
{
@Override
public void focusGained( FocusEvent e ) {}
@Override
public void focusLost( FocusEvent e ) {
repaintContentBorder();
}
};
tabPane.addFocusListener( contentBorderFocusListener );
}
@Override
@@ -490,6 +514,8 @@ public class FlatTabbedPaneUI
tabPane.removeMouseListener( wheelTabScroller );
wheelTabScroller = null;
}
tabPane.removeFocusListener( contentBorderFocusListener );
}
@Override
@@ -602,6 +628,35 @@ public class FlatTabbedPaneUI
repaintTab( getRolloverTab() );
}
/**
* Repaints parts of the content border if necessary.
* @see com.formdev.flatlaf.FlatClientProperties#TABBED_PANE_ACTIVE_TAB_BORDER
*/
private void repaintContentBorder() {
if ( clientPropertyBoolean( tabPane, TABBED_PANE_ACTIVE_TAB_BORDER, activeTabBorder ) &&
clientPropertyBoolean( tabPane, TABBED_PANE_SHOW_CONTENT_SEPARATOR, true ))
{
Rectangle bounds = getContentBorderBounds(tabPane.getTabPlacement());
Rectangle r;
switch (tabPane.getTabPlacement()) {
default:
case TOP:
r = new Rectangle( bounds.x, bounds.y, bounds.width, contentSeparatorHeight);
break;
case BOTTOM:
r = new Rectangle( bounds.x, bounds.y + bounds.height - contentSeparatorHeight, bounds.width, contentSeparatorHeight);
break;
case LEFT:
r = new Rectangle( bounds.x, bounds.y, contentSeparatorHeight, bounds.height);
break;
case RIGHT:
r = new Rectangle( bounds.x + bounds.width - contentSeparatorHeight, bounds.y, contentSeparatorHeight, bounds.height);
break;
}
tabPane.repaint(r);
}
}
private void repaintTab( int tabIndex ) {
if( tabIndex < 0 || tabIndex >= tabPane.getTabCount() )
return;
@@ -609,6 +664,8 @@ public class FlatTabbedPaneUI
Rectangle r = getTabBounds( tabPane, tabIndex );
if( r != null )
tabPane.repaint( r );
repaintContentBorder();
}
private boolean inCalculateEqual;
@@ -825,7 +882,7 @@ public class FlatTabbedPaneUI
FlatUIUtils.resetRenderingHints( g, oldRenderingHints );
oldRenderingHints = null;
}
static int cnt = 0;
@Override
public void paint( Graphics g, JComponent c ) {
if( hideTabArea() )
@@ -934,6 +991,12 @@ public class FlatTabbedPaneUI
int x, int y, int w, int h, boolean isSelected )
{
// paint tab background
Color background = determineTabBackgroundColor(tabPlacement, tabIndex, isSelected);
g.setColor( FlatUIUtils.deriveColor( background, tabPane.getBackground() ) );
g.fillRect( x, y, w, h );
}
protected Color determineTabBackgroundColor(int tabPlacement, int tabIndex, boolean isSelected) {
boolean enabled = tabPane.isEnabled();
Color background = enabled && tabPane.isEnabledAt( tabIndex ) && getRolloverTab() == tabIndex
? hoverColor
@@ -942,8 +1005,7 @@ public class FlatTabbedPaneUI
: (selectedBackground != null && enabled && isSelected
? selectedBackground
: tabPane.getBackgroundAt( tabIndex )));
g.setColor( FlatUIUtils.deriveColor( background, tabPane.getBackground() ) );
g.fillRect( x, y, w, h );
return background;
}
@Override
@@ -953,7 +1015,31 @@ public class FlatTabbedPaneUI
// paint tab separators
if( clientPropertyBoolean( tabPane, TABBED_PANE_SHOW_TAB_SEPARATORS, showTabSeparators ) &&
!isLastInRun( tabIndex ) )
paintTabSeparator( g, tabPlacement, x, y, w, h );
paintTabSeparator( g, tabPlacement, x, y, w, h );
// paint tab border
if ( clientPropertyBoolean( tabPane, TABBED_PANE_ACTIVE_TAB_BORDER, activeTabBorder ) && isSelected) {
paintActiveTabBorder(g, tabPlacement, tabIndex, x, y, w, h);
}
}
protected void paintActiveTabBorder( Graphics g, int tabPlacement, int tabIndex, int x, int y, int w, int h) {
float borderWidth = UIScale.scale( 1f );
g.setColor( (tabSeparatorColor != null) ? tabSeparatorColor : contentAreaColor );
switch( tabPlacement ) {
default:
case TOP:
case BOTTOM:
((Graphics2D)g).fill( new Rectangle2D.Float( x, y, borderWidth, h) );
((Graphics2D)g).fill( new Rectangle2D.Float( x + w - borderWidth, y, borderWidth, h) );
break;
case LEFT:
case RIGHT:
((Graphics2D)g).fill( new Rectangle2D.Float( x, y, w, borderWidth) );
((Graphics2D)g).fill( new Rectangle2D.Float( x, y + h - borderWidth, w, borderWidth) );
break;
}
}
protected void paintTabCloseButton( Graphics g, int tabIndex, int x, int y, int w, int h ) {
@@ -1004,40 +1090,44 @@ public class FlatTabbedPaneUI
switch( tabPlacement ) {
case TOP:
default:
int sy = y + h + contentInsets.top - tabSelectionHeight;
int sy;
if ( clientPropertyBoolean( tabPane, TABBED_PANE_ACTIVE_TAB_BORDER, activeTabBorder ))
sy = y;
else
sy = y + h + contentInsets.top - tabSelectionHeight;
g.fillRect( x, sy, w, tabSelectionHeight );
break;
case BOTTOM:
g.fillRect( x, y - contentInsets.bottom, w, tabSelectionHeight );
if ( clientPropertyBoolean( tabPane, TABBED_PANE_ACTIVE_TAB_BORDER, activeTabBorder )) {
g.fillRect( x, y + h + contentInsets.top - tabSelectionHeight, w, tabSelectionHeight );
} else {
g.fillRect( x, y - contentInsets.bottom, w, tabSelectionHeight );
}
break;
case LEFT:
int sx = x + w + contentInsets.left - tabSelectionHeight;
int sx;
if ( clientPropertyBoolean( tabPane, TABBED_PANE_ACTIVE_TAB_BORDER, activeTabBorder )) {
sx = x;
} else {
sx = x + w + contentInsets.left - tabSelectionHeight;
}
g.fillRect( sx, y, tabSelectionHeight, h );
break;
case RIGHT:
g.fillRect( x - contentInsets.right, y, tabSelectionHeight, h );
if ( clientPropertyBoolean( tabPane, TABBED_PANE_ACTIVE_TAB_BORDER, activeTabBorder )) {
g.fillRect( x + w + contentInsets.left - tabSelectionHeight, y, tabSelectionHeight, h );
} else {
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;
protected Rectangle getContentBorderBounds(int tabPlacement) {
Insets insets = tabPane.getInsets();
Insets tabAreaInsets = getTabAreaInsets( tabPlacement );
@@ -1072,6 +1162,31 @@ public class FlatTabbedPaneUI
break;
}
return new Rectangle(x, y, w, h);
}
/**
* 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
* - painting active tab border style
*/
@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;
Rectangle contentBorderBounds = getContentBorderBounds(tabPlacement);
int x = contentBorderBounds.x;
int y = contentBorderBounds.y;
int w = contentBorderBounds.width;
int h = contentBorderBounds.height;
// 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
@@ -1079,12 +1194,95 @@ public class FlatTabbedPaneUI
rotateInsets( hasFullBorder ? new Insets( sh, sh, sh, sh ) : new Insets( sh, 0, 0, 0 ), ci, tabPlacement );
// paint content separator or full border
Area area = new Area();
if ( clientPropertyBoolean( tabPane, TABBED_PANE_ACTIVE_TAB_BORDER, activeTabBorder ) ) {
// Active tab border style
int ax = rects[selectedIndex].x;
int ay = rects[selectedIndex].y;
int aw = rects[selectedIndex].width;
int ah = rects[selectedIndex].height;
int scrollOffsetX = 0;
int scrollOffsetY = 0;
int tabViewportOffsetX = 0;
int tabViewportOffsetY = 0;
if (tabViewport != null && isScrollTabLayout()) {
scrollOffsetX = tabViewport.getViewRect().x;
scrollOffsetY = tabViewport.getViewRect().y;
tabViewportOffsetX = tabViewport.getX();
tabViewportOffsetY = tabViewport.getY();
}
int gapX;
int gapY;
int gapW;
int gapH;
switch( tabPlacement ) {
default:
case TOP:
gapX = x + ax + 1 + (-scrollOffsetX + tabViewportOffsetX);
gapY = y;
gapW = aw - 2;
gapH = contentSeparatorHeight;
break;
case BOTTOM:
gapX = x + ax + 1 + (-scrollOffsetX + tabViewportOffsetX);
gapY = h - contentSeparatorHeight;
gapW = aw - 2;
gapH = contentSeparatorHeight;
break;
case LEFT:
gapX = x;
gapY = y + ay + 1 + (-scrollOffsetY + tabViewportOffsetY);
gapW = contentSeparatorHeight;
gapH = ah - 2;
break;
case RIGHT:
gapX = w - contentSeparatorHeight;
gapY = y + ay + 1 + (-scrollOffsetY + tabViewportOffsetY);
gapW = contentSeparatorHeight;
gapH = ah - 2;
break;
}
area.add( new Area( new Rectangle2D.Float( x, y, w, h ) ) );
area.subtract( new Area( new Rectangle2D.Float( gapX, gapY, gapW, gapH) ) );
Color activeTabAreaBackground = determineTabBackgroundColor(tabPlacement, selectedIndex, true);
g.setColor( activeTabAreaBackground );
((Graphics2D)g).fill( new Rectangle2D.Float( gapX, gapY, gapW, gapH) );
// Ensure that the separator outside the tabViewport is present (doesn't get cutoff by the active tab)
// If left unsolved the active tab is "visible" in the separator (the gap) even when outside the viewport
if (tabViewport != null && isScrollTabLayout()) {
switch( tabPlacement ) {
default:
case TOP:
case BOTTOM:
area.add( new Area( new Rectangle2D.Float( x, y, tabViewport.getX() - x, h ) ) );
area.add( new Area( new Rectangle2D.Float( tabViewport.getX() + tabViewport.getWidth(), y, w - tabViewport.getX() - x, h ) ) );
break;
case LEFT:
case RIGHT:
area.add( new Area( new Rectangle2D.Float( x, y, w, tabViewport.getY() - y ) ) );
area.add( new Area( new Rectangle2D.Float( x, tabViewport.getY() + tabViewport.getHeight(), w, h - tabViewport.getY() - y ) ) );
break;
}
}
// ((Graphics2D)g).fill( new Rectangle2D.Float( x, y, tabViewport.getX() - x, contentSeparatorHeight) );
} else {
area.add( new Area( new Rectangle2D.Float(x, y, w, h) ) );
}
Rectangle2D.Float r = 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) );
area.subtract( new Area( r ) );
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 );
((Graphics2D) g).fill( area );
// repaint selection in scroll-tab-layout because it may be painted before
// the content border was painted (from BasicTabbedPaneUI$ScrollableTabPanel)
@@ -2303,6 +2501,7 @@ public class FlatTabbedPaneUI
break;
case TABBED_PANE_SHOW_TAB_SEPARATORS:
case TABBED_PANE_ACTIVE_TAB_BORDER:
case TABBED_PANE_SHOW_CONTENT_SEPARATOR:
case TABBED_PANE_HAS_FULL_BORDER:
case TABBED_PANE_HIDE_TAB_AREA_WITH_ONE_TAB:

View File

@@ -571,6 +571,7 @@ TabbedPane.tabHeight = 32
TabbedPane.tabSelectionHeight = 3
TabbedPane.contentSeparatorHeight = 1
TabbedPane.showTabSeparators = false
TabbedPane.activeTabBorder = false
TabbedPane.tabSeparatorsFullHeight = false
TabbedPane.hasFullBorder = false
TabbedPane.tabInsets = 4,12,4,12