/**
 * This software is released as part of the Pumpernickel project.
 * 
 * All com.pump resources in the Pumpernickel project are distributed under the
 * MIT License:
 * https://github.com/mickleness/pumpernickel/raw/master/License.txt
 * 
 * More information about the Pumpernickel project is available here:
 * https://mickleness.github.io/pumpernickel/
 */
package com.pump.plaf;

import java.awt.Color;
import java.awt.Component;
import java.awt.Component.BaselineResizeBehavior;
import java.awt.Container;
import java.awt.Dimension;
import java.awt.Font;
import java.awt.GradientPaint;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.GridBagConstraints;
import java.awt.GridBagLayout;
import java.awt.Insets;
import java.awt.LayoutManager;
import java.awt.Paint;
import java.awt.Rectangle;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.ComponentAdapter;
import java.awt.event.ComponentEvent;
import java.awt.event.ComponentListener;
import java.awt.event.ContainerAdapter;
import java.awt.event.ContainerEvent;
import java.awt.event.ContainerListener;
import java.awt.event.MouseEvent;
import java.awt.event.MouseListener;
import java.awt.event.MouseMotionListener;
import java.beans.PropertyChangeEvent;
import java.beans.PropertyChangeListener;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import javax.swing.AbstractButton;
import javax.swing.Icon;
import javax.swing.JButton;
import javax.swing.JComponent;
import javax.swing.JLabel;
import javax.swing.JPanel;
import javax.swing.JTabbedPane;
import javax.swing.JToggleButton.ToggleButtonModel;
import javax.swing.SwingConstants;
import javax.swing.SwingUtilities;
import javax.swing.UIManager;
import javax.swing.border.Border;
import javax.swing.border.CompoundBorder;
import javax.swing.border.EmptyBorder;
import javax.swing.event.ChangeEvent;
import javax.swing.event.ChangeListener;
import javax.swing.plaf.TabbedPaneUI;
import javax.swing.plaf.UIResource;
import javax.swing.plaf.basic.BasicButtonListener;
import javax.swing.plaf.basic.BasicButtonUI;

import com.pump.awt.DescendantListener;
import com.pump.awt.SplayedLayout;
import com.pump.icon.button.MinimalDuoToneCloseIcon;
import com.pump.plaf.button.QButtonUI;
import com.pump.plaf.button.QButtonUI.HorizontalPosition;
import com.pump.plaf.button.QButtonUI.VerticalPosition;
import com.pump.swing.PartialLineBorder;

/**
 * This is modeled after Safari's tab model. There is one row of tabs (and other
 * possible controls) that is always stretch to fill the available width (or
 * height for vertical tabs).
 * 
 * Features include:
 * <ul>
 * <li>The option to hide the tab row when only one tab is present (the content
 * stays visible, but the tab is hidden).
 * <li>The option to automatically include a close button on all tabs.
 * <li>The option to add custom controls before/after the tabs.
 * </ul>
 * 
 * TODO: it would be nice to add drag-and-drop reordering
 * 
 * <p>
 * This UI does not support the tab layout policy of the JTabbedPane; tabs are
 * only presented in a single continuous scrollable row. So this will only
 * support one or zero runs of tabs.<br>
 * <h3>Context / Design</h3>
 * <p>
 * I would suggest this UI is appropriate for tabbed document interfaces (TDIs),
 * which are a specific implementation of multiple document interfaces (MDIs).
 * (This is all in contrast to single document interfaces (SDIs).)
 * <p>
 * There is a lot of related reading on this online and in well-researched books
 * like <em>About Face</em>. To sum up what I've read so far:
 * <ul>
 * <li>MDIs were most popular in the 90s and early 2000s. This is partly because
 * of resource limitations at the time -- there was no alternative. Some UX
 * designers regard them with a stigma and will suggest SDIs are the better way
 * to go.
 * <li>Still TDIs don't appear to be going away anytime soon. Browsers are the
 * most obvious example. Microsoft Excel is another. Apple, who is notorious for
 * UX scrutiny, recently added tabs to Finder windows. But my favorite example
 * is Notepad vs Notepad++. Notepad is a fine tool, and maybe it's the go-to
 * choice for many users, but Notepad++ (or a similar TDI tool like Atom) is
 * where power-users gravitate to.
 * </ul>
 * <p>
 * If you're interested in displaying a fixed set of tabs (such as in a complex
 * properties or preferences dialog): you should try to make sure the tabs are
 * always visible. Depending on the number of tabs you want to display: this UI
 * may not be a good fit in that case. Horizontal scrolling is relatively subtle
 * (even if you include an indicator like "+3" to indicate 3 more tabs are
 * out-of-sight, some users won't register that).
 * <p>
 * <h3>Implementation</h3>
 * <p>
 * This section describes how parts of this class are implemented, which may be
 * useful if you want to customize either a {@link Style} object or extend the
 * {@link DefaultTab} class.
 * <p>
 * (This section is written as if using the default tab placement; the same
 * components are arranged in subtly different ways for other tab placements.)
 * <p>
 * Each BoxTabbedPaneUI has a <code>controlRow</code> panel that stretches the
 * width of the <code>JTabbedPane</code>.
 * <p>
 * If you have defined leading components (see
 * {@link #PROPERTY_LEADING_COMPONENTS}), then they are anchored to the left
 * side of the <code>controlRow</code>. If you have defined trailing components
 * (see {@link #PROPERTY_TRAILING_COMPONENTS}), then they are anchored on the
 * right side of the <code>controlRow</code>. All of these components are given
 * their preferred size, so their preferred size should be modest. Note it is
 * your responsibility to format these controls. (For example, you may want your
 * components to match the border/fill of your BoxTabbedPaneUI.)
 * <p>
 * The <code>tabContainer</code> panel is given the remaining width. This width
 * is divided up into a series of <code>TabContainer</code> panels with one
 * panel per tab. The <code>TabContainer</code> class is actually a
 * <code>AbstractButton</code>: if you click it you are selecting that tab. You
 * can also transfer keyboard focus to it and press the spacebar.
 * <p>
 * The <code>TabContainer</code> is actually blank, and it contains the
 * component used to render/control the actual tab. By default this component
 * will be the <code>DefaultTab</code> object, but if you have defined custom
 * components using {@link JTabbedPane#setTabComponentAt(int, Component)} then
 * that component is used.
 * <p>
 * The <code>DefaultTab</code> contains a close button that is visible when
 * {@link #PROPERTY_CLOSEABLE_TABS} is enabled. (That also means: this property
 * has no automatic effect if you have supplied your own custom tab components.)
 * <p>
 * <img src=
 * "data:image/png;base64, "
 * alt="diagram of BoxTabbedPaneUI components">
 */
public class BoxTabbedPaneUI extends TabbedPaneUI {

	public interface Style {
		public void formatControlRow(JTabbedPane tabs, JPanel tabsContainer);

		/**
		 * 
		 * @param tabs
		 * @param tabContainer
		 * @param tabIndex
		 *            the tab index being formatted, or -1 if this button is
		 *            part of the control row but does not affect a tab.
		 */
		public void formatControlRowButton(JTabbedPane tabs,
				AbstractButton tabContainer, int tabIndex);

		public void formatCloseButton(JTabbedPane tabs, JButton closeButton);

		public void formatTabContent(JTabbedPane tabs, JComponent c);
	}

	public static DefaultStyle STYLE_DEFAULT = new DefaultStyle();

	public static Style getStyle(JTabbedPane tabs) {
		Style style = (Style) tabs.getClientProperty(PROPERTY_STYLE);
		if (style == null)
			style = STYLE_DEFAULT;
		return style;
	}

	public static class DefaultStyle implements Style {

		public class TabButtonUI extends BasicButtonUI {

			@Override
			public void paint(Graphics g0, JComponent c) {
				Graphics2D g = (Graphics2D) g0.create();
				boolean isSelected = ((AbstractButton) c).isSelected();
				g.setPaint(createContentGradient(c, isSelected));
				g.fillRect(0, 0, c.getWidth(), c.getHeight());
				if (c.isFocusOwner()) {
					int x = 0;
					int y = 0;
					int w = c.getWidth() - 1;
					int h = c.getHeight() - 1;
					Border b = c.getBorder();
					if (b != null) {
						Insets i = b.getBorderInsets(c);
						x += i.left;
						y += i.top;
						w -= i.left + i.right;
						h -= i.top + i.bottom;
					}
					Rectangle r = new Rectangle(x, y, w, h);
					PlafPaintUtils.paintFocus(g, r, 1,
							PlafPaintUtils.getFocusRingColor());
				}
				g.dispose();
				super.paint(g0, c);
			}

		}

		protected Color borderNormalLight = new Color(0xa1a0a1);
		protected Color borderNormalDark = new Color(0x9e9c9e);
		protected Color borderOuterSelected = new Color(0xbab9ba);
		protected Color borderOuterUnselected = new Color(0xa1a0a1);
		protected Color contentOuterNormal = new Color(0xbdbbbd);
		protected Color contentInnerNormal = new Color(0xb5b3b5);
		protected Color contentOuterSelected = new Color(0xd9d7d9);
		protected Color contentInnerSelected = new Color(0xd1cfd1);
		private boolean outerBorder, contentBorder;

		protected GradientPaint createContentGradient(JComponent c,
				boolean isSelected) {
			JTabbedPane tabs = getTabbedPaneParent(c);
			int placement = tabs == null ? SwingConstants.TOP : tabs
					.getTabPlacement();
			Color outer = isSelected ? contentOuterSelected
					: contentOuterNormal;
			Color inner = isSelected ? contentInnerSelected
					: contentInnerNormal;
			if (placement == SwingConstants.LEFT) {
				return (new GradientPaint(0, 0, outer, c.getWidth(), 0, inner));
			} else if (placement == SwingConstants.RIGHT) {
				return (new GradientPaint(0, 0, inner, c.getWidth(), 0, outer));
			} else if (placement == SwingConstants.BOTTOM) {
				return (new GradientPaint(0, 0, inner, 0, c.getHeight(), outer));
			} else {
				return (new GradientPaint(0, 0, outer, 0, c.getHeight(), inner));
			}
		}

		private JTabbedPane getTabbedPaneParent(Container c) {
			while (c != null) {
				if (c instanceof JTabbedPane)
					return (JTabbedPane) c;
				c = c.getParent();
			}
			return null;
		}

		public DefaultStyle() {
			this(true, true);
		}

		public DefaultStyle(boolean outerBorder, boolean contentBorder) {
			this.outerBorder = outerBorder;
			this.contentBorder = contentBorder;
		}

		private GradientPaint createBorderGradient(int tabPlacement) {
			if (tabPlacement == SwingConstants.BOTTOM) {
				return new GradientPaint(0, 0, borderNormalDark, 0, 25,
						borderNormalLight);
			} else if (tabPlacement == SwingConstants.LEFT) {
				return new GradientPaint(0, 0, borderNormalLight, 25, 0,
						borderNormalDark);
			} else if (tabPlacement == SwingConstants.RIGHT) {
				return new GradientPaint(0, 0, borderNormalDark, 25, 0,
						borderNormalLight);
			} else {
				return new GradientPaint(0, 0, borderNormalLight, 0, 25,
						borderNormalDark);
			}
		}

		@Override
		public void formatControlRow(JTabbedPane tabs, JPanel tabsContainer) {

			Border border;
			Paint paint = createBorderGradient(tabs.getTabPlacement());
			if (tabs.getTabPlacement() == SwingConstants.BOTTOM) {
				border = new PartialLineBorder(paint, true, true,
						tabs.getTabCount() == 0, true);
			} else if (tabs.getTabPlacement() == SwingConstants.LEFT) {
				border = new PartialLineBorder(paint, true,
						tabs.getTabCount() == 0, true, true);
			} else if (tabs.getTabPlacement() == SwingConstants.RIGHT) {
				border = new PartialLineBorder(paint, true, true, true,
						tabs.getTabCount() == 0);
			} else {
				border = new PartialLineBorder(paint, tabs.getTabCount() == 0,
						true, true, true);
			}

			tabsContainer.setUI(new GradientPanelUI(createContentGradient(tabs,
					false)));
			tabsContainer.setBorder(border);
		}

		@Override
		public void formatControlRowButton(JTabbedPane tabs,
				AbstractButton tabContainer, int tabIndex) {
			if (tabIndex >= 0) {
				Border border;
				int p = tabs.getTabPlacement();
				if (p == SwingConstants.LEFT) {
					border = new PartialLineBorder(createBorderGradient(p),
							tabIndex != 0, false, false, false);
					if (isOuterBorder()) {
						Paint paint = tabIndex == tabs.getSelectedIndex() ? borderOuterSelected
								: borderOuterUnselected;
						Border inner = new PartialLineBorder(paint, false,
								true, false, false);
						border = new CompoundBorder(border, inner);
					}
				} else if (p == SwingConstants.RIGHT) {
					border = new PartialLineBorder(createBorderGradient(p),
							tabIndex != 0, false, false, false);
					if (isOuterBorder()) {
						Paint paint = tabIndex == tabs.getSelectedIndex() ? borderOuterSelected
								: borderOuterUnselected;
						Border inner = new PartialLineBorder(paint, false,
								false, false, true);
						border = new CompoundBorder(border, inner);
					}
				} else {
					border = new PartialLineBorder(createBorderGradient(p),
							false, tabIndex != 0, false, false);
					if (isOuterBorder()) {
						Paint paint = tabIndex == tabs.getSelectedIndex() ? borderOuterSelected
								: borderOuterUnselected;
						Border inner = new PartialLineBorder(paint,
								p == SwingConstants.TOP, false,
								p == SwingConstants.BOTTOM, false);
						border = new CompoundBorder(border, inner);
					}
				}
				tabContainer.setBorder(border);
			}

			if (!(tabContainer.getUI() instanceof TabButtonUI)) {
				tabContainer.setUI(new TabButtonUI());
				tabContainer.putClientProperty(
						QButtonUI.PROPERTY_HORIZONTAL_POSITION,
						HorizontalPosition.MIDDLE);
				tabContainer.putClientProperty(
						QButtonUI.PROPERTY_VERTICAL_POSITION,
						VerticalPosition.MIDDLE);
				tabContainer.putClientProperty(
						QButtonUI.PROPERTY_STROKE_PAINTED, Boolean.FALSE);
			}
		}

		public boolean isOuterBorder() {
			return outerBorder;
		}

		@Override
		public void formatCloseButton(JTabbedPane tabs, JButton closeButton) {
			if (!(closeButton.getIcon() instanceof MinimalDuoToneCloseIcon)) {
				MinimalDuoToneCloseIcon icon = new MinimalDuoToneCloseIcon(
						closeButton);
				closeButton.setIcon(icon);
				DescendantMouseListener.installForParentOf(closeButton,
						icon.getParentRollover());
			}
			closeButton.setMargin(new Insets(0, 0, 0, 0));
			closeButton.setBorder(new EmptyBorder(0, 0, 0, 0));
		}

		@Override
		public void formatTabContent(JTabbedPane tabs, JComponent c) {
			if (contentBorder) {
				Border border;
				if (tabs.getTabPlacement() == SwingConstants.LEFT) {
					border = new PartialLineBorder(borderNormalDark, true,
							false, true, true);
				} else if (tabs.getTabPlacement() == SwingConstants.BOTTOM) {
					border = new PartialLineBorder(borderNormalDark, true,
							true, false, true);
				} else if (tabs.getTabPlacement() == SwingConstants.RIGHT) {
					border = new PartialLineBorder(borderNormalDark, true,
							true, true, false);
				} else {
					border = new PartialLineBorder(borderNormalDark, false,
							true, true, true);
				}
				c.setBorder(border);
			}
		}
	}

	/**
	 * An optional client property for a <code>java.util.List</code> of
	 * JComponents to always place on the trailing (right) side of the tabs.
	 */
	public static final String PROPERTY_TRAILING_COMPONENTS = BoxTabbedPaneUI.class
			.getName() + "#trailingComponents";

	/**
	 * An optional client property for a <code>java.util.List</code> of
	 * JComponents to always place on the leading (left) side of the tabs.
	 */
	public static final String PROPERTY_LEADING_COMPONENTS = BoxTabbedPaneUI.class
			.getName() + "#leadingComponents";

	/**
	 * This client property on tabs maps to the Integer index of that tab
	 * relative to the JTabbedPane.
	 */
	private static final String PROPERTY_TAB_INDEX = BoxTabbedPaneUI.class
			.getName() + "#tabIndex";

	/**
	 * This client property on JTabbedPane resolves to an internal Data object.
	 */
	private static final String PROPERTY_DATA = BoxTabbedPaneUI.class.getName()
			+ "#data";

	/**
	 * This optional property on JTabbedPane resolves to a Boolean used to
	 * indicate whether tabs should be closeable by default. Note if you have
	 * defined a custom tab component using
	 * {@link JTabbedPane#setTabComponentAt(int, Component)} then that component
	 * is used and this property is not automatically consulted. This value is
	 * assumed to be false by default.
	 */
	public static final String PROPERTY_CLOSEABLE_TABS = BoxTabbedPaneUI.class
			.getName() + "#closeableTabs";

	/**
	 * This optional property on JTabbedPane resolves to a Style object used to
	 * help format colors, borders, inner component UIs.
	 */
	public static final String PROPERTY_STYLE = BoxTabbedPaneUI.class.getName()
			+ "#style";

	/**
	 * This optional property on JTabbedPane resolves to a Boolean used to
	 * indicate whether the tab UI controls should be hidden if there is only
	 * one tab. This value is assumed to be false by default.
	 */
	public static final String PROPERTY_HIDE_SINGLE_TAB = BoxTabbedPaneUI.class
			.getName() + "#hideSingleTab";

	/**
	 * This caches a TabContainer on each Tab object. If we recreate the
	 * TabContainer with every refresh: the icons/animations may flicker a
	 * little.
	 */
	private static final String PROPERTY_TAB_CONTAINER = BoxTabbedPaneUI.class
			.getName() + "#tabContainer";

	private static class UIResourcePanel extends JPanel implements UIResource {
		private static final long serialVersionUID = 1L;

		UIResourcePanel(LayoutManager layoutManager) {
			super(layoutManager);
		}
	}

	private static class BoxTabbedPaneUILayoutManager implements LayoutManager {

		@Override
		public void addLayoutComponent(String name, Component comp) {
		}

		@Override
		public void removeLayoutComponent(Component comp) {
		}

		@Override
		public Dimension preferredLayoutSize(Container parent) {
			return getLayoutSize(parent, true);
		}

		@Override
		public Dimension minimumLayoutSize(Container parent) {
			return getLayoutSize(parent, false);
		}

		private Dimension getLayoutSize(Container parent, boolean preferred) {
			int tabPlacement = ((JTabbedPane) parent).getTabPlacement();
			boolean verticalPlacement = tabPlacement == JTabbedPane.TOP
					|| tabPlacement == JTabbedPane.BOTTOM;
			Dimension additional = new Dimension(0, 0);
			Dimension max = new Dimension(0, 0);
			for (int a = 0; a < parent.getComponentCount(); a++) {
				Component c = parent.getComponent(a);
				Dimension d = preferred ? c.getPreferredSize() : c
						.getMinimumSize();
				if (c instanceof UIResourcePanel) {
					if (verticalPlacement) {
						additional.height += d.height;
						additional.width = Math.max(additional.width, d.width);
					} else {
						additional.width += d.width;
						additional.height = Math.max(additional.height,
								d.height);
					}
				} else {
					max.width = Math.max(max.width, d.width);
					max.height = Math.max(max.height, d.height);
				}
			}
			if (verticalPlacement) {
				return new Dimension(Math.max(additional.width, max.width),
						additional.height + max.height);
			}
			return new Dimension(additional.width + max.width, Math.max(
					additional.height, max.height));
		}

		@Override
		public void layoutContainer(Container parent) {
			int y = 0;
			int x = 0;
			int width = parent.getWidth();
			int height = parent.getHeight();

			int tabPlacement = ((JTabbedPane) parent).getTabPlacement();
			for (int a = 0; a < parent.getComponentCount(); a++) {
				Component c = parent.getComponent(a);
				if (c instanceof UIResourcePanel && c.isVisible()) {
					Dimension d = c.getPreferredSize();
					if (tabPlacement == JTabbedPane.TOP) {
						c.setBounds(x, y, width, d.height);
						y += d.height;
						height -= d.height;
					} else if (tabPlacement == JTabbedPane.BOTTOM) {
						c.setBounds(x, height - d.height, width, d.height);
						height -= d.height;
					} else if (tabPlacement == JTabbedPane.LEFT) {
						c.setBounds(x, y, d.width, height);
						x += d.width;
						width -= d.width;
					} else if (tabPlacement == JTabbedPane.RIGHT) {
						c.setBounds(width - d.width, y, d.width, height);
						width -= d.width;
					}
				}
			}
			for (int a = 0; a < parent.getComponentCount(); a++) {
				Component c = parent.getComponent(a);
				if (!(c instanceof UIResourcePanel)) {
					c.setBounds(x, y, width, height);
				}
			}
		}

	}

	private static final String[] REFRESH_PROPERTIES = new String[] {
			"mnemonicAt", "displayedMnemonicIndexAt", "indexForTitle",
			"tabLayoutPolicy", "opaque", "background", "indexForTabComponent",
			"indexForNullComponent", "font", PROPERTY_HIDE_SINGLE_TAB,
			PROPERTY_CLOSEABLE_TABS };

	/**
	 * This is a cluster of data related to a specific JTabbedPane.
	 * <p>
	 * In theory a TabbedPaneUI can be applied to multiple JTabbedPanes. If that
	 * happens each pane gets its own Data object to control it.
	 */
	class Data {

		JPanel controlRow = new UIResourcePanel(new GridBagLayout());
		JPanel leadingComponents = new JPanel(new GridBagLayout());
		JPanel tabsContainer = new JPanel();
		JPanel trailingComponents = new JPanel(new GridBagLayout());
		JTabbedPane tabs;

		ChangeListener refreshChangeListener = new ChangeListener() {

			@Override
			public void stateChanged(ChangeEvent e) {
				refreshTabStates();
			}
		};

		PropertyChangeListener refreshPropertyListener = new PropertyChangeListener() {

			@Override
			public void propertyChange(PropertyChangeEvent evt) {
				refreshTabStates();
			}

		};

		PropertyChangeListener refreshExtraComponentsListener = new PropertyChangeListener() {

			@Override
			public void propertyChange(PropertyChangeEvent evt) {
				refreshExtraComponents();
			}

		};

		PropertyChangeListener refreshStyleListener = new PropertyChangeListener() {

			@Override
			public void propertyChange(PropertyChangeEvent evt) {
				refreshStyle();
				refreshTabStates();
			}

		};

		PropertyChangeListener refreshContentBorderListener = new PropertyChangeListener() {

			@Override
			public void propertyChange(PropertyChangeEvent evt) {
				refreshContentBorder();
			}

		};

		PropertyChangeListener tabPlacementPropertyListener = new PropertyChangeListener() {

			@Override
			public void propertyChange(PropertyChangeEvent evt) {
				relayoutControlRow();
				refreshExtraComponents();
				refreshTabStates();
				refreshStyle();
				refreshContentBorder();
			}

		};

		ContainerListener containerListener = new ContainerAdapter() {

			@Override
			public void componentAdded(ContainerEvent e) {
				refreshTabStates();
				refreshContentBorder();
				refreshStyle();
			}

			@Override
			public void componentRemoved(ContainerEvent e) {
				refreshTabStates();
				refreshContentBorder();
				refreshStyle();
			}

		};

		public Data(JTabbedPane tabs) {
			this.tabs = tabs;
			controlRow.setOpaque(false);
			leadingComponents.setOpaque(false);
			tabsContainer.setOpaque(false);
			trailingComponents.setOpaque(false);
		}

		public void install() {
			tabs.setLayout(new BoxTabbedPaneUILayoutManager());
			removeNonUIResources(tabs);
			tabs.add(controlRow);

			relayoutControlRow();

			for (String property : new String[] { PROPERTY_STYLE }) {
				tabs.addPropertyChangeListener(property, refreshStyleListener);
			}

			for (String property : new String[] { PROPERTY_TRAILING_COMPONENTS,
					PROPERTY_LEADING_COMPONENTS }) {
				tabs.addPropertyChangeListener(property,
						refreshExtraComponentsListener);
			}

			tabs.addPropertyChangeListener("tabPlacement",
					tabPlacementPropertyListener);

			for (String property : REFRESH_PROPERTIES) {
				tabs.addPropertyChangeListener(property,
						refreshPropertyListener);
			}
			tabs.addContainerListener(containerListener);
			tabs.getModel().addChangeListener(refreshChangeListener);

			refreshExtraComponents();
			refreshTabStates();
			refreshContentBorder();
			refreshStyle();
		}

		private void refreshContentBorder() {
			for (int a = 0; a < tabs.getComponentCount(); a++) {
				Component c = tabs.getComponent(a);
				if (!(c instanceof UIResource) && (c instanceof JComponent)) {
					getStyle(tabs).formatTabContent(tabs, (JComponent) c);
				}
			}
		}

		private void refreshStyle() {
			getStyle(tabs).formatControlRow(tabs, tabsContainer);
		}

		private void relayoutControlRow() {
			controlRow.removeAll();
			GridBagConstraints c = new GridBagConstraints();
			c.gridx = 0;
			c.gridy = 0;
			c.weightx = 0;
			c.weighty = 0;
			c.fill = GridBagConstraints.BOTH;
			if (tabs.getTabPlacement() == SwingConstants.LEFT) {
				controlRow.add(trailingComponents, c);
				c.gridy++;
				c.weighty = 1;
				controlRow.add(tabsContainer, c);
				c.gridy++;
				c.weighty = 0;
				controlRow.add(leadingComponents, c);
			} else if (tabs.getTabPlacement() == SwingConstants.RIGHT) {
				controlRow.add(leadingComponents, c);
				c.gridy++;
				c.weighty = 1;
				controlRow.add(tabsContainer, c);
				c.gridy++;
				c.weighty = 0;
				controlRow.add(trailingComponents, c);
			} else {
				controlRow.add(leadingComponents, c);
				c.gridx++;
				c.weightx = 1;
				controlRow.add(tabsContainer, c);
				c.gridx++;
				c.weightx = 0;
				controlRow.add(trailingComponents, c);
			}

			controlRow.revalidate();
		}

		int lastTabPlacement = -1;

		@SuppressWarnings("unchecked")
		private void refreshExtraComponents() {
			List<JComponent> newLeadingComponents = (List<JComponent>) tabs
					.getClientProperty(PROPERTY_LEADING_COMPONENTS);
			List<JComponent> newTrailingComponents = (List<JComponent>) tabs
					.getClientProperty(PROPERTY_TRAILING_COMPONENTS);
			int tabPlacement = tabs.getTabPlacement();
			boolean forceReinstall = tabPlacement != lastTabPlacement;
			lastTabPlacement = tabPlacement;
			installExtraComponents(leadingComponents, newLeadingComponents,
					forceReinstall);
			installExtraComponents(trailingComponents, newTrailingComponents,
					forceReinstall);
		}

		ComponentListener extraComponentListener = new ComponentAdapter() {

			@Override
			public void componentShown(ComponentEvent e) {
				refreshExtraContainerVisibility();
			}

			@Override
			public void componentHidden(ComponentEvent e) {
				refreshExtraContainerVisibility();
			}

		};

		private void installExtraComponents(Container container,
				List<JComponent> components, boolean forceReinstall) {
			if (components == null)
				components = new ArrayList<>();
			Component[] oldComponents = container.getComponents();
			if (!Arrays.asList(oldComponents).equals(components)) {
				forceReinstall = true;
			}

			if (forceReinstall) {
				container.removeAll();
				GridBagConstraints c = new GridBagConstraints();
				c.gridx = 0;
				c.gridy = 100;
				c.weightx = 1;
				c.weighty = 1;
				c.fill = GridBagConstraints.BOTH;
				for (JComponent jc : components) {
					container.add(jc, c);
					if (tabs.getTabPlacement() == SwingConstants.LEFT) {
						c.gridy--;
					} else if (tabs.getTabPlacement() == SwingConstants.RIGHT) {
						c.gridy++;
					} else {
						c.gridx++;
					}
					jc.removeComponentListener(extraComponentListener);
					jc.addComponentListener(extraComponentListener);

					for (Component oldComponent : oldComponents) {
						if (components.contains(oldComponent)) {
							oldComponent
									.removeComponentListener(extraComponentListener);
						}
					}
				}
				container.revalidate();
			}
			refreshExtraContainerVisibility();
		}

		private void refreshExtraContainerVisibility() {
			leadingComponents
					.setVisible(containsVisibleComponent(leadingComponents));
			trailingComponents
					.setVisible(containsVisibleComponent(trailingComponents));
		}

		private boolean containsVisibleComponent(Container container) {
			for (Component c : container.getComponents()) {
				if (c.isVisible())
					return true;
			}
			return false;
		}

		public void uninstall() {
			tabs.remove(controlRow);

			for (String property : new String[] { PROPERTY_STYLE }) {
				tabs.removePropertyChangeListener(property,
						refreshStyleListener);
			}

			for (String property : new String[] { PROPERTY_TRAILING_COMPONENTS,
					PROPERTY_LEADING_COMPONENTS }) {
				tabs.removePropertyChangeListener(property,
						refreshExtraComponentsListener);
			}

			tabs.removePropertyChangeListener("tabPlacement",
					tabPlacementPropertyListener);

			for (String property : REFRESH_PROPERTIES) {
				tabs.removePropertyChangeListener(property,
						refreshPropertyListener);
			}
			tabs.removeContainerListener(containerListener);
			tabs.getModel().removeChangeListener(refreshChangeListener);
		}

		private void removeNonUIResources(Container c) {
			for (Component comp : c.getComponents()) {
				if (!(comp instanceof UIResource)) {
					c.remove(comp);
				}
			}
		}

		protected void refreshTabStates() {
			List<Component> newTabs = new ArrayList<>();
			for (int a = 0; a < tabs.getTabCount(); a++) {
				JComponent tab = (JComponent) tabs.getTabComponentAt(a);
				if (tab == null)
					tab = getDefaultTab(a);

				TabContainer tabContainer = (TabContainer) tab
						.getClientProperty(PROPERTY_TAB_CONTAINER);
				if (tabContainer == null) {
					tabContainer = new TabContainer(tabs, a, tab);
					tab.putClientProperty(PROPERTY_TAB_CONTAINER, tabContainer);
				}

				newTabs.add(tabContainer);
				tab.putClientProperty(PROPERTY_TAB_INDEX, a);
				getStyle(tabs).formatControlRowButton(tabs, tabContainer, a);
			}

			Boolean hideSingleTab = (Boolean) tabs
					.getClientProperty(PROPERTY_HIDE_SINGLE_TAB);
			if (hideSingleTab == null)
				hideSingleTab = Boolean.FALSE;
			controlRow.setVisible(!(tabs.getTabCount() <= 1 && hideSingleTab));

			if (!(tabsContainer.getLayout() instanceof SplayedLayout)) {
				SplayedLayout l = new SplayedLayout(true) {

					@Override
					protected Collection<JComponent> getEmphasizedComponents(
							JComponent container) {
						Collection<JComponent> returnValue = super
								.getEmphasizedComponents(container);

						for (Component c : container.getComponents()) {
							if (c instanceof AbstractButton) {
								AbstractButton ab = (AbstractButton) c;
								if (ab.isSelected())
									returnValue.add(ab);
							}
						}
						return returnValue;
					}

				};
				tabsContainer.setLayout(l);
			}
			int orientation = tabs.getTabPlacement() == SwingConstants.LEFT
					|| tabs.getTabPlacement() == SwingConstants.RIGHT ? SwingConstants.VERTICAL
					: SwingConstants.HORIZONTAL;
			((SplayedLayout) tabsContainer.getLayout()).setOrientation(null,
					orientation);

			setComponents(tabsContainer, newTabs);

			tabsContainer.revalidate();
		}

		/**
		 * This is basically
		 * <code>java.awt.Container.setComponents(Component[])</code>. This is
		 * functionally the same as removing all of a container's children and
		 * then adding them back again, but this method makes individual changes
		 * to minimize the number of container/hierarchy changes that listeners
		 * hear.
		 */
		private void setComponents(Container container,
				List<Component> components) {
			Component[] oldComponents = container.getComponents();
			for (int a = 0; a < oldComponents.length; a++) {
				if (!components.contains(oldComponents[a])) {
					container.remove(oldComponents[a]);
				}
			}
			oldComponents = container.getComponents();
			int oldCtr = 0;
			int newCtr = 0;
			while (newCtr < components.size()) {
				Component oldComponent = oldCtr < oldComponents.length ? oldComponents[oldCtr]
						: null;
				if (oldComponent != components.get(newCtr)) {
					container.add(components.get(newCtr), oldCtr);
				} else {
					oldCtr++;
				}
				newCtr++;
			}
		}

		Map<Integer, DefaultTab> tabMap = new HashMap<>();

		private DefaultTab getDefaultTab(int a) {
			DefaultTab returnValue = tabMap.get(a);
			if (returnValue == null) {
				returnValue = createDefaultTab(tabs, a);
				tabMap.put(a, returnValue);
			} else {
				returnValue.decorateLabel(a);
			}
			return returnValue;
		}

		public int getTabRunCount() {
			return 1;
		}

		public Rectangle getTabBounds(int index) {
			for (int a = 0; a < controlRow.getComponentCount(); a++) {
				Component c = controlRow.getComponent(a);
				if (c instanceof JComponent) {
					JComponent jc = (JComponent) c;
					Integer i = (Integer) jc
							.getClientProperty(PROPERTY_TAB_INDEX);
					if (i != null && i == index)
						return SwingUtilities.convertRectangle(controlRow,
								jc.getBounds(), tabs);
				}
			}
			return null;
		}

		public int getTabForCoordinate(int x, int y) {
			Component c = SwingUtilities.getDeepestComponentAt(tabs, x, y);
			while (c != tabs) {
				if (c instanceof JComponent) {
					JComponent jc = (JComponent) c;
					Integer i = (Integer) jc
							.getClientProperty(PROPERTY_TAB_INDEX);
					if (i != null)
						return i;
				}
				c = c.getParent();
			}
			return -1;
		}
	}

	static class TabContainer extends AbstractButton {
		/**
		 * This converts MouseEvent sources to an AbstractButton.
		 */
		private static class MyBasicButtonListener extends BasicButtonListener {

			AbstractButton button;

			public MyBasicButtonListener(AbstractButton b) {
				super(b);
				button = b;
			}

			@Override
			public void mouseMoved(MouseEvent e) {
				super.mouseMoved(convert(e));
			}

			@Override
			public void mouseDragged(MouseEvent e) {
				super.mouseDragged(convert(e));
			}

			@Override
			public void mouseClicked(MouseEvent e) {
				super.mouseClicked(convert(e));
			}

			@Override
			public void mousePressed(MouseEvent e) {
				super.mousePressed(convert(e));
			}

			@Override
			public void mouseReleased(MouseEvent e) {
				super.mouseReleased(convert(e));
			}

			@Override
			public void mouseEntered(MouseEvent e) {
				super.mouseEntered(convert(e));
			}

			@Override
			public void mouseExited(MouseEvent e) {
				super.mouseExited(convert(e));
			}

			private MouseEvent convert(MouseEvent e) {
				if (e.getSource() == button)
					return e;
				return SwingUtilities.convertMouseEvent(e.getComponent(), e,
						button);
			}
		}

		JTabbedPane tabs;
		int tabIndex;

		public TabContainer(JTabbedPane tabs, int tabIndex, JComponent tab) {
			this.tabs = tabs;
			this.tabIndex = tabIndex;

			setLayout(new GridBagLayout());
			GridBagConstraints c = new GridBagConstraints();
			c.gridx = 0;
			c.gridy = 0;
			c.weightx = 1;
			c.weighty = 1;
			c.fill = GridBagConstraints.BOTH;
			add(tab, c);
			setFocusable(true);
			setFocusPainted(true);
			setModel(new ToggleButtonModel());
			refreshSelectedState();
			getModel().addChangeListener(new ChangeListener() {
				@Override
				public void stateChanged(ChangeEvent e) {
					refreshSelectedState();
				}
			});

			tabs.addChangeListener(new ChangeListener() {
				@Override
				public void stateChanged(ChangeEvent e) {
					refreshSelectedState();
				}
			});

			BasicButtonListener buttonListener = new MyBasicButtonListener(this);
			buttonListener.installKeyboardActions(this);
			addFocusListener(buttonListener);
			DescendantListener
					.addMouseListener(this, (MouseListener) buttonListener,
							false, AbstractButton.class);
			DescendantListener.addMouseListener(this,
					(MouseMotionListener) buttonListener, false,
					AbstractButton.class);

			addActionListener(new ActionListener() {

				@Override
				public void actionPerformed(ActionEvent e) {
					TabContainer.this.tabs
							.setSelectedIndex(TabContainer.this.tabIndex);
				}

			});

			setFocusable(true);
			setRequestFocusEnabled(false);
		}

		private void refreshSelectedState() {
			getModel().setSelected(tabs.getSelectedIndex() == tabIndex);
		}

		private static final long serialVersionUID = 1L;

	}

	public static class DefaultTab extends JPanel {
		private static final long serialVersionUID = 1L;

		protected final JTabbedPane tabs;
		private JLabel label = new JLabel();
		private JButton closeButton = new JButton();
		private int tabIndex = -1;

		private PropertyChangeListener closeableTabsListener = new PropertyChangeListener() {

			@Override
			public void propertyChange(PropertyChangeEvent evt) {
				refreshCloseableButton();
			}

		};

		public DefaultTab(JTabbedPane tabs, int tabIndex) {
			setLayout(new GridBagLayout());
			this.tabs = tabs;

			closeButton.setContentAreaFilled(false);
			closeButton.setBorderPainted(false);

			label.setHorizontalAlignment(SwingConstants.CENTER);

			Font font = UIManager.getFont("TabbedPane.smallFont");
			if (font != null)
				label.setFont(font);

			setOpaque(false);
			decorateLabel(tabIndex);

			closeButton.addActionListener(new ActionListener() {

				@Override
				public void actionPerformed(ActionEvent e) {
					doCloseTab(DefaultTab.this.tabIndex);
				}

			});

			tabs.addPropertyChangeListener(PROPERTY_CLOSEABLE_TABS,
					closeableTabsListener);
			refreshCloseableButton();
		}

		/**
		 * Remove a tab at a given index.
		 * <p>
		 * The default implementation simply calls
		 * <code>tabs.removeTabAt(tabIndex)</code>, but subclasses can override
		 * this as needed. For example, you may need to prompt the user to
		 * confirm discarding unsaved changes.
		 * 
		 * @param tabIndex
		 *            the index of the tab to close.
		 */
		protected void doCloseTab(int tabIndex) {
			tabs.removeTabAt(tabIndex);
		}

		private void refreshCloseableButton() {
			Boolean b = (Boolean) tabs
					.getClientProperty(PROPERTY_CLOSEABLE_TABS);
			if (b == null)
				b = Boolean.FALSE;
			closeButton.setVisible(b);
			int i;
			if (b) {
				Dimension d = closeButton.getPreferredSize();
				i = Math.max(d.width, d.height);
			} else {
				i = 0;
			}
			i += 2;
			label.setBorder(new EmptyBorder(0, i, 0, i));

			getStyle(tabs).formatCloseButton(tabs, closeButton);
		}

		protected JLabel getLabel() {
			return label;
		}

		protected JButton getCloseButton() {
			return closeButton;
		}

		protected GridBagConstraints createLabelConstraints() {
			GridBagConstraints labelConstraints = new GridBagConstraints();
			labelConstraints.gridx = 0;
			labelConstraints.gridy = 0;
			labelConstraints.weightx = 1;
			labelConstraints.weighty = 1;
			labelConstraints.fill = GridBagConstraints.BOTH;
			labelConstraints.insets = new Insets(3, 3, 3, 3);
			return labelConstraints;
		}

		protected GridBagConstraints createCloseButtonConstraints() {
			GridBagConstraints closeButtonConstraints = new GridBagConstraints();
			closeButtonConstraints.gridx = 0;
			closeButtonConstraints.gridy = 0;
			closeButtonConstraints.weightx = 1;
			closeButtonConstraints.weighty = 1;
			closeButtonConstraints.fill = GridBagConstraints.NONE;
			if (tabs.getTabPlacement() == SwingConstants.LEFT) {
				closeButtonConstraints.anchor = GridBagConstraints.SOUTH;
			} else if (tabs.getTabPlacement() == SwingConstants.RIGHT) {
				closeButtonConstraints.anchor = GridBagConstraints.NORTH;
			} else {
				closeButtonConstraints.anchor = GridBagConstraints.WEST;
			}
			if (tabs.getTabPlacement() == SwingConstants.LEFT) {
				closeButtonConstraints.insets = new Insets(0, 0, 3, 0);
			} else if (tabs.getTabPlacement() == SwingConstants.RIGHT) {
				closeButtonConstraints.insets = new Insets(3, 0, 0, 0);
			} else {
				closeButtonConstraints.insets = new Insets(0, 3, 0, 0);
			}
			return closeButtonConstraints;
		}

		public void decorateLabel(int index) {
			tabIndex = index;
			Color background = tabs.getBackgroundAt(index);
			setBackground(background);

			Icon disabledIcon = tabs.getDisabledIconAt(index);
			Icon icon = tabs.getIconAt(index);

			if (!tabs.isEnabledAt(index) || !tabs.isEnabled()) {
				if (disabledIcon != null)
					icon = disabledIcon;
			}
			getLabel().setIcon(icon);

			// TODO: we don't current support mnemonics
			tabs.getDisplayedMnemonicIndexAt(index);
			tabs.getMnemonicAt(index);

			Color foreground = tabs.getForegroundAt(index);
			if (foreground == null)
				foreground = UIManager.getColor("Label.foreground");
			getLabel().setForeground(foreground);

			setToolTipText(tabs.getToolTipTextAt(index));

			String title = tabs.getTitleAt(index);
			if (title == null)
				title = "";
			getLabel().setText(title);

			getStyle(tabs).formatCloseButton(tabs, closeButton);

			addChild(tabs.getTabPlacement());
		}

		/**
		 * Add the label to this DefaultTab; if necessary wrap it inside a
		 * RotatedComponent.
		 * 
		 * @param tabPlacement
		 *            the tab placement (SwingConstants.TOP,
		 *            SwingConstants.LEFT, SwingConstants.BOTTOM,
		 *            SwingConstants.RIGHT)
		 */
		protected void addChild(int tabPlacement) {
			RotatedPanel.Rotation rotation = RotatedPanel.Rotation.NONE;
			if (tabPlacement == SwingConstants.LEFT) {
				rotation = RotatedPanel.Rotation.COUNTER_CLOCKWISE;
			} else if (tabPlacement == SwingConstants.RIGHT) {
				rotation = RotatedPanel.Rotation.CLOCKWISE;
			}

			Component child = getComponentCount() == 0 ? null : getComponent(0);
			if (rotation == RotatedPanel.Rotation.NONE) {
				if (child == getLabel()) {
					return;
				} else {
					removeAll();
					add(getCloseButton(), createCloseButtonConstraints());
					add(getLabel(), createLabelConstraints());
					revalidate();
				}
			} else {
				if (child instanceof RotatedPanel) {
					RotatedPanel rc = (RotatedPanel) child;
					rc.setRotation(rotation);
				} else {
					removeAll();
					add(getCloseButton(), createCloseButtonConstraints());
					add(new RotatedPanel(getLabel(), rotation),
							createLabelConstraints());
					revalidate();
				}
			}
		}

	}

	protected Data getData(JTabbedPane tabs) {
		Data d = (Data) tabs.getClientProperty(PROPERTY_DATA);
		if (d == null) {
			d = new Data(tabs);
			tabs.putClientProperty(PROPERTY_DATA, d);
		}
		return d;
	}

	public DefaultTab createDefaultTab(JTabbedPane tabs, int a) {
		return new DefaultTab(tabs, a);
	}

	@Override
	public void installUI(JComponent c) {
		super.installUI(c);
		Data data = getData((JTabbedPane) c);
		data.install();
	}

	@Override
	public void uninstallUI(JComponent c) {
		super.uninstallUI(c);
		Data data = getData((JTabbedPane) c);
		data.uninstall();
	}

	@Override
	public boolean contains(JComponent c, int x, int y) {
		return x >= 0 && x < c.getWidth() && y >= 0 && y < c.getHeight();
	}

	@Override
	public int getBaseline(JComponent c, int width, int height) {
		// TODO
		return -1;
	}

	@Override
	public BaselineResizeBehavior getBaselineResizeBehavior(JComponent c) {
		return BaselineResizeBehavior.OTHER;
	}

	@Override
	public int tabForCoordinate(JTabbedPane pane, int x, int y) {
		return getData(pane).getTabForCoordinate(x, y);
	}

	@Override
	public Rectangle getTabBounds(JTabbedPane pane, int index) {
		return getData(pane).getTabBounds(index);
	}

	@Override
	public int getTabRunCount(JTabbedPane pane) {
		return getData(pane).getTabRunCount();
	}

}