package com.explodingpixels.widgets.plaf;
import java.awt.Color;
import java.awt.Component;
import java.awt.Font;
import java.awt.GradientPaint;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.Insets;
import java.awt.LayoutManager;
import java.awt.Paint;
import java.awt.Point;
import java.awt.Rectangle;
import java.awt.RenderingHints;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.ContainerAdapter;
import java.awt.event.ContainerEvent;
import java.awt.event.ContainerListener;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
import java.awt.event.MouseListener;
import java.awt.event.MouseMotionAdapter;
import java.awt.event.MouseMotionListener;
import java.beans.PropertyChangeEvent;
import java.beans.PropertyChangeListener;
import java.util.HashMap;
import java.util.Map;
import javax.swing.BorderFactory;
import javax.swing.Icon;
import javax.swing.JComponent;
import javax.swing.Timer;
import javax.swing.plaf.basic.BasicTabbedPaneUI;
import com.explodingpixels.painter.MacWidgetsPainter;
import com.explodingpixels.widgets.TabCloseListener;
public class EPTabbedPaneUI extends BasicTabbedPaneUI {
public static final String TAB_CLOSE_LISTENER_KEY = "TabbedPane.closeListener";
public static final String CLOSE_BUTTON_LOCATION_KEY = "TabbedPane.closeButtonLocation";
public static final Object CLOSE_BUTTON_LOCATION_VALUE_LEFT = EPTabPainter.CloseButtonLocation.LEFT;
public static final Object CLOSE_BUTTON_LOCATION_VALUE_RIGHT = EPTabPainter.CloseButtonLocation.RIGHT;
private EPTabPainter fTabPainter = new EPTabPainter();
private MacWidgetsPainter<Component> fContentBorderTopEdgeBackgroundPainter = createContentBorderTopEdgeBackgroundPainter();
private boolean fPaintFullContentBorder = true;
private int fCurrentDefaultTabWidth = DEFAULT_TAB_WIDTH;
private int fMouseOverCloseButtonTabIndex = NO_TAB;
private int fMousePressedCloseButtonTabIndex = NO_TAB;
private TabCloseListener fTabCloseListener = new DefaultTabCloseListener();
private Timer fTabCloseTimer = new Timer(10, null);
private CustomLayoutManager fLayoutManager = new CustomLayoutManager();
private static final Insets FULL_CONTENT_BORDER_INSETS = new Insets(6, 0, 0, 0);
private static final Insets HAIRLINE_BORDER_INSETS = new Insets(2, 0, 0, 0);
private static final int DEFAULT_TAB_WIDTH = 100;
private static final int NO_TAB = -1;
private static final int SMALLEST_TAB_WIDTH = 35;
private static final int TAB_ANIMATION_DELTA = 7;
private static final int OVERFLOW_BUTTON_AREA_WIDTH = 25;
@Override
protected void installDefaults() {
super.installDefaults();
Font oldFont = tabPane.getFont();
tabPane.setFont(oldFont.deriveFont(oldFont.getSize() - 2.0f));
tabPane.setBorder(BorderFactory.createEmptyBorder());
tabInsets = new Insets(2, 10, 2, 10);
selectedTabPadInsets = new Insets(2, 0, 2, 0);
doExtractTabCloseProperty();
doExtractCloseButtonLocationProperty();
}
@Override
protected void installListeners() {
super.installListeners();
tabPane.addMouseMotionListener(createCloseButtonMouseMotionListener());
tabPane.addMouseListener(createCloseButtonMouseListener());
tabPane.addContainerListener(createContainerListener());
tabPane.addPropertyChangeListener(TAB_CLOSE_LISTENER_KEY,
createTabCloseListenerPropertyChangeListener());
tabPane.addPropertyChangeListener(CLOSE_BUTTON_LOCATION_KEY,
createCloseButtonLocationPropertyChangeListener());
}
private MouseMotionListener createCloseButtonMouseMotionListener() {
return new MouseMotionAdapter() {
@Override
public void mouseMoved(MouseEvent e) {
doMouseMoved(e.getPoint());
}
@Override
public void mouseDragged(MouseEvent e) {
doMousePressed(e.getPoint());
doMouseMoved(e.getPoint());
}
};
}
private void doMouseMoved(Point point) {
int tabIndex = tabForCoordinate(tabPane, point.x, point.y);
int oldMouseOverCloseButtonTabIndex = fMouseOverCloseButtonTabIndex;
if (isTabIndexValid(tabIndex)) {
Rectangle tabBounds = getTabBounds(tabPane, tabIndex);
fMouseOverCloseButtonTabIndex = fTabPainter.isPointOverCloseButton(tabBounds, point)
? tabIndex : NO_TAB;
repaintTab(fMouseOverCloseButtonTabIndex);
}
repaintTab(oldMouseOverCloseButtonTabIndex);
}
private MouseListener createCloseButtonMouseListener() {
return new MouseAdapter() {
@Override
public void mouseExited(MouseEvent e) {
int oldMouseOverCloseButtonTabIndex = fMouseOverCloseButtonTabIndex;
fMouseOverCloseButtonTabIndex = NO_TAB;
repaintTab(oldMouseOverCloseButtonTabIndex);
}
@Override
public void mousePressed(MouseEvent e) {
doMousePressed(e.getPoint());
}
@Override
public void mouseReleased(MouseEvent e) {
closeTabUsingAnimationIfValid(fMousePressedCloseButtonTabIndex);
int oldMousePressedOverCloseButtonTabIndex = fMouseOverCloseButtonTabIndex;
fMousePressedCloseButtonTabIndex = NO_TAB;
repaintTab(oldMousePressedOverCloseButtonTabIndex);
}
};
}
private void doMousePressed(Point point) {
int tabIndex = tabForCoordinate(tabPane, point.x, point.y);
int oldMousePressedCloseButtonIndex = fMousePressedCloseButtonTabIndex;
if (isTabIndexValid(tabIndex)) {
Rectangle tabBounds = getTabBounds(tabPane, tabIndex);
fMousePressedCloseButtonTabIndex = fTabPainter.isPointOverCloseButton(tabBounds, point) ? tabIndex : NO_TAB;
repaintTab(fMousePressedCloseButtonTabIndex);
}
repaintTab(oldMousePressedCloseButtonIndex);
}
private ContainerListener createContainerListener() {
return new ContainerAdapter() {
public void componentAdded(ContainerEvent e) {
Component componentAdded = e.getChild();
fLayoutManager.forceTabWidth(componentAdded, SMALLEST_TAB_WIDTH);
animateTabBeingAdded(componentAdded);
}
};
}
private PropertyChangeListener createTabCloseListenerPropertyChangeListener() {
return new PropertyChangeListener() {
public void propertyChange(PropertyChangeEvent evt) {
doExtractTabCloseProperty();
}
};
}
private void doExtractTabCloseProperty() {
Object closeListenerValue = tabPane.getClientProperty(TAB_CLOSE_LISTENER_KEY);
if (closeListenerValue instanceof TabCloseListener) {
fTabCloseListener = (TabCloseListener) closeListenerValue;
}
}
private PropertyChangeListener createCloseButtonLocationPropertyChangeListener() {
return new PropertyChangeListener() {
public void propertyChange(PropertyChangeEvent evt) {
doExtractCloseButtonLocationProperty();
}
};
}
private void doExtractCloseButtonLocationProperty() {
Object closeButtonLocationValue = tabPane.getClientProperty(CLOSE_BUTTON_LOCATION_KEY);
if (closeButtonLocationValue instanceof EPTabPainter.CloseButtonLocation) {
setCloseButtonLocation((EPTabPainter.CloseButtonLocation) closeButtonLocationValue);
}
}
@Override
protected LayoutManager createLayoutManager() {
return fLayoutManager;
}
@Override
protected Insets getContentBorderInsets(int tabPlacement) {
return fPaintFullContentBorder ? FULL_CONTENT_BORDER_INSETS : HAIRLINE_BORDER_INSETS;
}
@Override
public void paint(Graphics g, JComponent c) {
((Graphics2D) g).setRenderingHint(
RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
super.paint(g, c);
paintContentBorder(g, tabPane.getTabPlacement(), tabPane.getSelectedIndex());
}
@Override
protected void paintTab(Graphics g, int tabPlacement, Rectangle[] rects, int tabIndex,
Rectangle iconRect, Rectangle textRect) {
Rectangle tabRect = rects[tabIndex];
boolean isSelected = tabIndex == tabPane.getSelectedIndex();
String title = tabPane.getTitleAt(tabIndex);
Icon icon = getIconForTab(tabIndex);
Graphics2D graphics = (Graphics2D) g;
boolean isMouseOverCloseButton = fMouseOverCloseButtonTabIndex == tabIndex;
boolean isMousePressedOverCloseButton = fMousePressedCloseButtonTabIndex == tabIndex;
fTabPainter.paintTab(graphics, tabPane, tabRect, title, icon, isSelected, isMouseOverCloseButton,
isMousePressedOverCloseButton);
}
@Override
protected void paintContentBorderTopEdge(Graphics g, int tabPlacement, int selectedIndex,
int x, int y, int width, int height) {
Graphics2D graphics = (Graphics2D) g;
graphics.translate(x, y);
int borderHeight = getContentBorderInsets(tabPane.getTabPlacement()).top;
fContentBorderTopEdgeBackgroundPainter.paint(graphics, tabPane, width, borderHeight);
graphics.translate(-x, -y);
if (tabPane.getSelectedIndex() >= 0) {
graphics.setColor(Color.WHITE);
Rectangle boundsOfSelectedTab = getTabBounds(tabPane, tabPane.getSelectedIndex());
graphics.drawLine(boundsOfSelectedTab.x, y, boundsOfSelectedTab.x + boundsOfSelectedTab.width, y);
}
graphics.setColor(Color.RED);
g.fillRect(x, y + height, x + width, y + height + 2);
}
private MacWidgetsPainter<Component> createContentBorderTopEdgeBackgroundPainter() {
return new MacWidgetsPainter<Component>() {
public void paint(Graphics2D graphics, Component objectToPaint, int width, int height) {
Paint paint = new GradientPaint(0, 0, Color.WHITE, 0, height - 1, new Color(0xf8f8f8));
graphics.setPaint(paint);
graphics.fillRect(0, 0, width, height - 1);
graphics.setColor(EPTabPainter.SELECTED_BORDER_COLOR);
graphics.drawLine(0, 0, width, 0);
graphics.setColor(new Color(0x999999));
// TODO figure out why we need to subtract off another extra pixel here -- doesn't make sense.
graphics.drawLine(0, height - 2, width, height - 2);
}
};
}
@Override
protected void paintContentBorderLeftEdge(Graphics g, int tabPlacement, int selectedIndex,
int x, int y, int w, int h) {
// do nothing.
}
@Override
protected void paintContentBorderRightEdge(Graphics g, int tabPlacement, int selectedIndex,
int x, int y, int w, int h) {
// do nothing.
}
@Override
protected void paintContentBorderBottomEdge(Graphics g, int tabPlacement, int selectedIndex,
int x, int y, int w, int h) {
// do nothing.
}
@Override
protected int getTabLabelShiftX(int tabPlacement, int tabIndex, boolean isSelected) {
return 0;
}
@Override
protected int getTabLabelShiftY(int tabPlacement, int tabIndex, boolean isSelected) {
return 0;
}
// Public API methods. ////////////////////////////////////////////////////////////////////////////////////////////
public void setPaintsFullContentBorder(boolean paintsFullContentBorder) {
fPaintFullContentBorder = paintsFullContentBorder;
tabPane.repaint();
}
public void setCloseButtonLocation(EPTabPainter.CloseButtonLocation closeButtonLocation) {
fTabPainter.setCloseButtonLocation(closeButtonLocation);
}
// Helper methods. ////////////////////////////////////////////////////////////////////////////
private void repaintTab(int tabIndex) {
if (isTabIndexValid(tabIndex)) {
Rectangle tabBounds = getTabBounds(tabPane, tabIndex);
tabPane.repaint(tabBounds);
}
}
private boolean isTabIndexValid(int tabIndex) {
return tabIndex >= 0 && tabIndex < tabPane.getTabCount();
}
private void animateTabBeingAdded(Component tabComponent) {
fTabCloseTimer.addActionListener(createTabAddedAnimation(tabComponent));
fTabCloseTimer.start();
}
private void closeTabUsingAnimationIfValid(int tabIndex) {
if (isTabIndexValid(tabIndex) && fTabCloseListener.tabAboutToBeClosed(tabIndex)) {
Component tabComponentToClose = tabPane.getComponent(tabIndex);
fTabCloseTimer.addActionListener(createTabRemovedAnimation(tabComponentToClose));
fTabCloseTimer.start();
}
}
private void closeTab(int tabIndex) {
assert isTabIndexValid(tabIndex) : "The tab index should be valid.";
String title = tabPane.getTitleAt(tabIndex);
Component component = tabPane.getComponentAt(tabIndex);
tabPane.removeTabAt(tabIndex);
fTabCloseListener.tabClosed(title, component);
}
// Tab animation helper methods. //////////////////////////////////////////////////////////////
private ActionListener createTabAddedAnimation(final Component tabComponentAdded) {
return new ActionListener() {
public void actionPerformed(ActionEvent e) {
int currentTabWidth = fLayoutManager.getTabWidth(tabComponentAdded);
int newTabWidth = Math.min(currentTabWidth + TAB_ANIMATION_DELTA, fCurrentDefaultTabWidth);
fLayoutManager.forceTabWidth(tabComponentAdded, newTabWidth);
if (newTabWidth == fCurrentDefaultTabWidth) {
animationFinished(this, tabComponentAdded);
}
tabPane.doLayout();
tabPane.repaint();
}
};
}
private ActionListener createTabRemovedAnimation(final Component tabComponentToClose) {
return new ActionListener() {
public void actionPerformed(ActionEvent e) {
int currentTabWidth = fLayoutManager.getTabWidth(tabComponentToClose);
int newTabWidth = Math.max(currentTabWidth - TAB_ANIMATION_DELTA, SMALLEST_TAB_WIDTH);
fLayoutManager.forceTabWidth(tabComponentToClose, newTabWidth);
if (newTabWidth == SMALLEST_TAB_WIDTH) {
animationFinished(this, tabComponentToClose);
int tabIndex = tabPane.indexOfComponent(tabComponentToClose);
closeTab(tabIndex);
}
tabPane.doLayout();
tabPane.repaint();
}
};
}
private void animationFinished(ActionListener actionListenerToRemove, Component tabComponent) {
fTabCloseTimer.removeActionListener(actionListenerToRemove);
fLayoutManager.useDefaultTabWidth(tabComponent);
if (fTabCloseTimer.getActionListeners().length == 0) {
fTabCloseTimer.stop();
}
}
// CustomLayoutManager implementation. ////////////////////////////////////////////////////////
private class CustomLayoutManager extends TabbedPaneLayout {
private Map<Component, Integer> fTabsBeingAnimatedToWidths = new HashMap<Component, Integer>();
private void forceTabWidth(Component tabComponent, int width) {
fTabsBeingAnimatedToWidths.put(tabComponent, width);
}
private void useDefaultTabWidth(Component tabComponent) {
fTabsBeingAnimatedToWidths.remove(tabComponent);
}
private int getTabWidth(Component tabComponent) {
Integer forcedTabWidth = fTabsBeingAnimatedToWidths.get(tabComponent);
return forcedTabWidth == null ? fCurrentDefaultTabWidth : forcedTabWidth;
}
protected void calculateTabRects(int tabPlacement, int tabCount) {
Insets tabAreaInsets = getTabAreaInsets(tabPlacement);
int currentX = tabAreaInsets.left;
int y = tabAreaInsets.top;
int tabAreaWidth = tabPane.getWidth() - tabAreaInsets.left - tabAreaInsets.right;
int requiredWidth = calculateRequiredWidth();
boolean notEnoughRoom = requiredWidth > tabAreaWidth;
int extraSpace = tabAreaWidth - requiredWidth;
// int extraSpace = notEnoughRoom
// ? tabAreaWidth - requiredWidth - OVERFLOW_BUTTON_AREA_WIDTH
// : tabAreaWidth - requiredWidth;
int numDefaultWidthTabs = getNumDefaultWidthTabs();
System.out.println("tabbed pane width " + tabAreaWidth + ", extra space " + extraSpace);
if (numDefaultWidthTabs > 0) {
int extraSpacePerTab = extraSpace / numDefaultWidthTabs;
int newDefaultTabWidth = fCurrentDefaultTabWidth + extraSpacePerTab;
fCurrentDefaultTabWidth = Math.min(newDefaultTabWidth, DEFAULT_TAB_WIDTH);
fCurrentDefaultTabWidth = Math.max(SMALLEST_TAB_WIDTH, fCurrentDefaultTabWidth);
}
maxTabWidth = 0;
maxTabHeight = calculateMaxTabHeight(tabPlacement);
// inidicate that there is one "run" of tabs. this value is used during the actual
// laying out of the container and must be set here.
runCount = 1;
// iterate through tabs and lay them out in a single row (run).
for (int i = 0; i < tabCount; i++) {
Rectangle rect = rects[i];
rect.width = isTabBeingAnimated(i) ? getForcedTabWidth(i) : fCurrentDefaultTabWidth;
maxTabWidth = Math.max(maxTabWidth, rect.width);
rect.x = currentX;
// move the currentX variable over to the right edge of this tab, which is the
// beginning of the next tab.
currentX += rect.width;
rect.y = y;
rect.height = maxTabHeight;
}
}
private boolean isTabBeingAnimated(int tabIndex) {
Component tabComponent = tabPane.getComponentAt(tabIndex);
return fTabsBeingAnimatedToWidths.get(tabComponent) != null;
}
private int getForcedTabWidth(int tabIndex) {
Component tabComponent = tabPane.getComponentAt(tabIndex);
return fTabsBeingAnimatedToWidths.get(tabComponent);
}
private int getNumDefaultWidthTabs() {
return tabPane.getTabCount() - fTabsBeingAnimatedToWidths.size();
}
private int sumOfForcedTabWidths() {
int sum = 0;
for (int width : fTabsBeingAnimatedToWidths.values()) {
sum += width;
}
return sum;
}
private int calculateRequiredWidth() {
int totalDefaultWidthTabsWidth = getNumDefaultWidthTabs() * fCurrentDefaultTabWidth;
return totalDefaultWidthTabsWidth + sumOfForcedTabWidths();
}
}
// DefaultTabCloseListener implementation. ////////////////////////////////////////////////////
private static class DefaultTabCloseListener implements TabCloseListener {
public boolean tabAboutToBeClosed(int tabIndex) {
return true;
}
public void tabClosed(String title, Component component) {
// no implementation.
}
}
}