package com.explodingpixels.widgets.plaf; import java.awt.Color; import java.awt.FontMetrics; import java.awt.Graphics2D; import java.awt.Point; import java.awt.Rectangle; import java.awt.Shape; import java.awt.geom.GeneralPath; import javax.swing.Icon; import javax.swing.ImageIcon; import javax.swing.JTabbedPane; import javax.swing.SwingUtilities; import javax.swing.plaf.basic.BasicGraphicsUtils; public class EPTabPainter { private CloseButtonLocation fCloseButtonLocation = CloseButtonLocation.LEFT; private CloseButtonIcon fCloseButtonIcon = new DefaultCloseButtonIcon(); private static final int CONTENT_DISTANCE_FROM_EDGE = 5; private static final int CLOSE_BUTTON_DISTANCE_FROM_EDGE = 5; private static final int CLOSE_BUTTON_DISTANCE_FROM_CONTENT = 3; // create single variable to reuse when layout the tab. this reuse of rectangle is also seen in BasicLabelUI and // was used there for performance gains -- we're following that example, though it's unclear whether or not there // are still performance gains to be had by doing this. private static final Rectangle ADJUSTED_TAB_BOUNDS = new Rectangle(); private static final Rectangle TEXT_BOUNDS = new Rectangle(); private static final Rectangle ICON_BOUNDS = new Rectangle(); // the colors used to draw the tab backround and border. static final Color SELECTED_BORDER_COLOR = new Color(0x666666); private static final Color UNSELECTED_BORDER_COLOR = new Color(0x888888); private static final Color SELECTED_BACKGROUND_COLOR = Color.WHITE; private static final Color UNSELECTED_BACKGROUND_COLOR = new Color(0xcccccc); private static final int CORNER_ARC_DIAMETER = 6; public void setCloseButtonLocation(CloseButtonLocation closeButtonLocation) { fCloseButtonLocation = closeButtonLocation; } public void paintTab(Graphics2D graphics, JTabbedPane tabPane, Rectangle tabBounds, String tabText, Icon tabIcon, boolean isSelected, boolean isMouseOverCloseButton, boolean isMousePressedOverCloseButton) { paintTabBackgroundAndBorder(graphics, tabBounds, isSelected); paintCloseButton(graphics, tabBounds, isSelected, isMouseOverCloseButton, isMousePressedOverCloseButton); FontMetrics fontMetrics = graphics.getFontMetrics(); int closeButtonWidth = fCloseButtonIcon.getWidth(); int textWidth = fontMetrics.stringWidth(tabText); int widthRequiredForCloseButton = fCloseButtonLocation.calculateWidthRequiredForCloseButton(closeButtonWidth); boolean tooWide = textWidth > tabBounds.width - widthRequiredForCloseButton - CONTENT_DISTANCE_FROM_EDGE; Rectangle adjustedTabRect = new Rectangle(); adjustedTabRect.x = tooWide ? fCloseButtonLocation.calculateContentX(tabBounds, closeButtonWidth) : tabBounds.x; adjustedTabRect.y = tabBounds.y; adjustedTabRect.width = tooWide ? tabBounds.width - widthRequiredForCloseButton - CONTENT_DISTANCE_FROM_EDGE : tabBounds.width; adjustedTabRect.height = tabBounds.height; ICON_BOUNDS.x = 0; ICON_BOUNDS.y = 0; String clippedText = SwingUtilities.layoutCompoundLabel(tabPane, fontMetrics, tabText, tabIcon, SwingUtilities.CENTER, SwingUtilities.CENTER, SwingUtilities.CENTER, SwingUtilities.TRAILING, adjustedTabRect, ICON_BOUNDS, TEXT_BOUNDS, 4); int textX = fCloseButtonLocation.adjustXToPreventEncroachment(tabBounds, closeButtonWidth, TEXT_BOUNDS); int textY = TEXT_BOUNDS.y + fontMetrics.getAscent(); graphics.setColor(tabPane.getForeground()); BasicGraphicsUtils.drawString(graphics, clippedText, -1, textX, textY); } private void paintTabBackgroundAndBorder(Graphics2D graphics, Rectangle tabBounds, boolean isSelected) { int extendedHeight = tabBounds.height + CORNER_ARC_DIAMETER / 2; // paint the background. graphics.setColor(isSelected ? SELECTED_BACKGROUND_COLOR : UNSELECTED_BACKGROUND_COLOR); graphics.fillRoundRect(tabBounds.x, tabBounds.y, tabBounds.width, extendedHeight, CORNER_ARC_DIAMETER, CORNER_ARC_DIAMETER); // paint the border. graphics.setColor(isSelected ? SELECTED_BORDER_COLOR : UNSELECTED_BORDER_COLOR); graphics.drawRoundRect(tabBounds.x, tabBounds.y, tabBounds.width, extendedHeight, CORNER_ARC_DIAMETER, CORNER_ARC_DIAMETER); } private void paintCloseButton(Graphics2D graphics, Rectangle tabBounds, boolean isSelected, boolean isMouseOverCloseButton, boolean isMousePressedOverCloseButton) { int x = fCloseButtonLocation.calculateCloseButtonX(tabBounds, fCloseButtonIcon.getWidth()); int y = fCloseButtonLocation.calculateCloseButtonY(tabBounds, fCloseButtonIcon.getHeight()); ImageIcon closeImageIcon = fCloseButtonIcon.getImageIcon(isSelected, isMouseOverCloseButton, isMousePressedOverCloseButton); graphics.drawImage(closeImageIcon.getImage(), x, y, null); } public boolean isPointOverCloseButton(Rectangle tabBounds, Point point) { int closeButtonWidth = fCloseButtonIcon.getWidth(); int closeButtonHeight = fCloseButtonIcon.getHeight(); int closeButtonX = fCloseButtonLocation.calculateCloseButtonX(tabBounds, closeButtonWidth); int closeButtonY = fCloseButtonLocation.calculateCloseButtonY(tabBounds, closeButtonHeight); boolean overHorizontally = closeButtonX <= point.x && point.x <= closeButtonX + closeButtonWidth; boolean overVertically = closeButtonY <= point.y && point.y <= closeButtonY + closeButtonHeight; return overHorizontally && overVertically; } private Shape createTabShape(Rectangle tabBounds) { int topIndent = 3; int topCurveRadius = 3; int bottomCurveRadius = 4; int topLeftX = tabBounds.x + topIndent; int topRightX = tabBounds.x + tabBounds.width - topIndent; int bottomLeftX = tabBounds.x; int bottomRightX = tabBounds.x + tabBounds.width; GeneralPath path = new GeneralPath(); path.moveTo(topLeftX + topCurveRadius, tabBounds.y); path.quadTo(topLeftX, tabBounds.y, topLeftX, tabBounds.y + topCurveRadius); // path.lineTo(topLeftX, tabBounds.y + tabBounds.height - bottomCurveRadius); path.quadTo(topLeftX, tabBounds.y + tabBounds.height, bottomLeftX, tabBounds.y + tabBounds.height); // path.moveTo(tabBounds.x + curveRadius, tabBounds.y); // path.quadTo(topLeftX, tabBounds.y + tabBounds.height, bottomLeftX, tabBounds.y + tabBounds.height); // path.lineTo(tabBounds.x, tabBounds.y + tabBounds.height); // path.lineTo(tabBounds.x + tabBounds.width, tabBounds.y + tabBounds.height); // path.lineTo(tabBounds.x + tabBounds.width, tabBounds.y + topCurveRadius); // path.quadTo(tabBounds.x + tabBounds.width, tabBounds.y, tabBounds.x + tabBounds.width - topCurveRadius, tabBounds.y); path.lineTo(bottomRightX, tabBounds.y + tabBounds.height); path.quadTo(topRightX, tabBounds.y + tabBounds.height, topRightX, tabBounds.y + topCurveRadius); path.quadTo(topRightX, tabBounds.y, topRightX - topCurveRadius, tabBounds.y); path.closePath(); return path; } // CloseButton enumeration implementation. //////////////////////////////////////////////////////////////////////// public enum CloseButtonLocation { LEFT { int calculateCloseButtonX(Rectangle tabBounds, int closeButtonWidth) { return tabBounds.x + CLOSE_BUTTON_DISTANCE_FROM_EDGE; } int calculateContentX(Rectangle tabBounds, int closeButtonWidth) { return tabBounds.x + calculateWidthRequiredForCloseButton(closeButtonWidth); } int adjustXToPreventEncroachment(Rectangle tabBounds, int closeButtonWidth, Rectangle contentBounds) { int closeButtonX = calculateCloseButtonX(tabBounds, closeButtonWidth); int closeButtonXWithPad = closeButtonX + closeButtonWidth + CLOSE_BUTTON_DISTANCE_FROM_CONTENT; return Math.max(closeButtonXWithPad, contentBounds.x); } }, RIGHT { int calculateCloseButtonX(Rectangle tabBounds, int closeButtonWidth) { return tabBounds.x + tabBounds.width - CLOSE_BUTTON_DISTANCE_FROM_CONTENT - closeButtonWidth; } int calculateContentX(Rectangle tabBounds, int closeButtonWidth) { return tabBounds.x + CONTENT_DISTANCE_FROM_EDGE; } int adjustXToPreventEncroachment(Rectangle tabBounds, int closeButtonWidth, Rectangle contentBounds) { int closeButtonX = calculateCloseButtonX(tabBounds, closeButtonWidth); int closeButtonXWithPad = closeButtonX - CLOSE_BUTTON_DISTANCE_FROM_CONTENT; int shiftAmount = (contentBounds.x + contentBounds.width) - closeButtonXWithPad; return shiftAmount > 0 ? contentBounds.x - shiftAmount : contentBounds.x; } }; abstract int calculateCloseButtonX(Rectangle tabBounds, int closeButtonWidth); abstract int calculateContentX(Rectangle tabBounds, int closeButtonWidth); abstract int adjustXToPreventEncroachment(Rectangle tabBounds, int closeButtonWidth, Rectangle contentBounds); private int calculateCloseButtonY(Rectangle tabBounds, int closeButtonHeight) { return tabBounds.y + tabBounds.height / 2 - closeButtonHeight / 2; } int calculateWidthRequiredForCloseButton(int closeButtonWidth) { return closeButtonWidth + CLOSE_BUTTON_DISTANCE_FROM_EDGE + CLOSE_BUTTON_DISTANCE_FROM_CONTENT; } } // CloseButton interface and implementations. ///////////////////////////////////////////////////////////////////// private interface CloseButtonIcon { int getWidth(); int getHeight(); ImageIcon getImageIcon(boolean isSelected, boolean isOver, boolean isPressed); } private static class DefaultCloseButtonIcon implements CloseButtonIcon { private ImageIcon fSelected = createImageIcon("close.png"); private ImageIcon fUnselected = createImageIcon("close_unselected.png"); private ImageIcon fOver = createImageIcon("close_over.png"); private ImageIcon fPressed = createImageIcon("close_pressed.png"); public int getWidth() { return fSelected.getIconWidth(); } public int getHeight() { return fSelected.getIconHeight(); } public ImageIcon getImageIcon(boolean isSelected, boolean isOver, boolean isPressed) { ImageIcon closeImageIcon; if (isOver && isPressed) { closeImageIcon = fPressed; } else if (isOver) { closeImageIcon = fOver; } else if (isSelected) { closeImageIcon = fSelected; } else { closeImageIcon = fUnselected; } return closeImageIcon; } private static ImageIcon createImageIcon(String fileName) { return new ImageIcon(EPTabbedPaneUI.class.getResource("/com/explodingpixels/widgets/images/" + fileName)); } } }