/*
* (c) Copyright 2010-2011 AgileBirds
*
* This file is part of OpenFlexo.
*
* OpenFlexo is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* OpenFlexo is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with OpenFlexo. If not, see <http://www.gnu.org/licenses/>.
*
*/
package org.openflexo.swing;
import java.awt.BorderLayout;
import java.awt.Color;
import java.awt.Component;
import java.awt.Container;
import java.awt.Cursor;
import java.awt.Dimension;
import java.awt.GradientPaint;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.Insets;
import java.awt.Point;
import java.awt.RenderingHints;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.ComponentAdapter;
import java.awt.event.ComponentEvent;
import java.awt.event.MouseEvent;
import java.awt.event.MouseListener;
import java.awt.geom.Point2D;
import java.net.MalformedURLException;
import java.net.URL;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import javax.annotation.Nonnull;
import javax.annotation.Nullable;
import javax.swing.BorderFactory;
import javax.swing.Icon;
import javax.swing.ImageIcon;
import javax.swing.JButton;
import javax.swing.JComponent;
import javax.swing.JFrame;
import javax.swing.JLabel;
import javax.swing.JMenuBar;
import javax.swing.JPanel;
import javax.swing.JPopupMenu;
import javax.swing.SwingUtilities;
import javax.swing.UIManager;
import javax.swing.UnsupportedLookAndFeelException;
import javax.swing.border.AbstractBorder;
import javax.swing.border.Border;
import org.openflexo.icon.UtilsIconLibrary;
public class TabbedPane<J> {
public static interface TabHeaderRenderer<J> {
public boolean isTabHeaderVisible(J tab);
public Icon getTabHeaderIcon(J tab);
public String getTabHeaderTitle(J tab);
public String getTabHeaderTooltip(J tab);
}
public static interface TabListener<J> {
public void tabSelected(@Nullable J tab);
public void tabClosed(@Nonnull J tab);
}
private static final Color TRANSPARENT = new Color(1.0f, 1.0f, 1.0f, 0.3f);
private static final Color LIGHT_BLUE = new Color(150, 183, 255, 255);
private static final int TAB_SPACING = 2;
private class TabHeaders extends JPanel implements ActionListener {
private class TabHeader extends JMenuBar implements ActionListener, MouseListener {
private static final int ROUNDED_CORNER_SIZE = 8;
private class TabHeaderBorder implements Border {
@Override
public void paintBorder(Component c, Graphics g, int x, int y, int width, int height) {
((Graphics2D) g).setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
g.setColor(Color.LIGHT_GRAY);
g.drawLine(0, ROUNDED_CORNER_SIZE - 3, 0, height);
g.drawLine(width - 1, ROUNDED_CORNER_SIZE - 3, width - 1, height);
g.drawLine(ROUNDED_CORNER_SIZE - 3, 0, width - ROUNDED_CORNER_SIZE + 3, 0);
g.drawArc(0, 0, ROUNDED_CORNER_SIZE, ROUNDED_CORNER_SIZE, 90, 90);
g.drawArc(width - ROUNDED_CORNER_SIZE - 1, 0, ROUNDED_CORNER_SIZE, ROUNDED_CORNER_SIZE, 0, 90);
}
@Override
public Insets getBorderInsets(Component c) {
return new Insets(5, ROUNDED_CORNER_SIZE + 1, 5, ROUNDED_CORNER_SIZE + 1);
}
@Override
public boolean isBorderOpaque() {
return false;
}
}
private final J tab;
private JLabel title;
private JButton close;
private Color defaultBackground;
private Color defaultForeground;
private boolean hovered;
private java.awt.geom.Path2D.Double clip;
public TabHeader(J tab) {
super();
setLayout(new BorderLayout());
setBackground(UIManager.getDefaults().getColor("ToolBar.floatingForeground"));
setOpaque(false);
this.tab = tab;
add(title = new JLabel());
title.setOpaque(false);
title.setBorder(null);
addMouseListener(this);
title.addMouseListener(this);
add(close = new JButton(UtilsIconLibrary.CLOSE_ICON), BorderLayout.EAST);
close.setContentAreaFilled(false);
close.setOpaque(false);
close.setBorderPainted(false);
close.setRolloverIcon(UtilsIconLibrary.CLOSE_HOVER_ICON);
close.setPressedIcon(UtilsIconLibrary.CLOSE_PRESSED_ICON);
close.addActionListener(this);
close.setFocusable(false);
close.setBorder(BorderFactory.createEmptyBorder(0, 5, 0, 0));
close.setPreferredSize(new Dimension(close.getIcon().getIconWidth() + close.getInsets().left + close.getInsets().right,
close.getIcon().getIconHeight() + close.getInsets().top + close.getInsets().bottom));
setBorder(new TabHeaderBorder());
addComponentListener(new ComponentAdapter() {
@Override
public void componentResized(ComponentEvent e) {
clip = null;
}
});
refresh();
}
@Override
public String toString() {
return title.getText() + " visible=" + isVisible() + " bounds=" + getBounds();
}
@Override
public boolean isVisible() {
return (tabHeaderRenderer == null || tabHeaderRenderer.isTabHeaderVisible(tab)) && super.isVisible();
}
public void refresh() {
if (tabHeaderRenderer != null) {
title.setIcon(tabHeaderRenderer.getTabHeaderIcon(tab));
title.setText(tabHeaderRenderer.getTabHeaderTitle(tab));
title.setToolTipText(tabHeaderRenderer.getTabHeaderTooltip(tab));
} else {
title.setIcon(null);
if (tab instanceof JComponent) {
title.setText(((JComponent) tab).getName());
title.setToolTipText(((JComponent) tab).getToolTipText());
}
}
TabHeaders.this.revalidate();
}
@Override
protected void paintComponent(Graphics g) {
if (getParent() == TabHeaders.this) {
Graphics clippedG = g.create();
((Graphics2D) clippedG).clip(getClip());
super.paintComponent(clippedG);
clippedG.dispose();
if (tab == selectedTab) {
((Graphics2D) g).setPaint(new GradientPaint(new Point2D.Double(0, 0), TRANSPARENT, new Point2D.Double(0,
getHeight() / 2), LIGHT_BLUE));
g.fillRect(0, 0, getWidth(), getHeight());
}
} else {
if (isOpaque()) {
g.setColor(getBackground());
g.fillRect(0, 0, getWidth(), getHeight());
}
}
}
public java.awt.geom.Path2D.Double getClip() {
if (clip == null) {
clip = new java.awt.geom.Path2D.Double();
clip.moveTo(ROUNDED_CORNER_SIZE - 3, 0);
// Top border
clip.lineTo(getWidth() - ROUNDED_CORNER_SIZE + 3, 0);
// Top right rounded corner
clip.quadTo(getWidth(), 0, getWidth(), ROUNDED_CORNER_SIZE - 3);
// Right border
clip.lineTo(getWidth(), getHeight() - 1);
// Bottom border
clip.lineTo(0, getHeight() - 1);
// Left border
clip.lineTo(0, ROUNDED_CORNER_SIZE - 3);
// Top left rounder border
clip.quadTo(0, 0, ROUNDED_CORNER_SIZE - 3, 0);
clip.closePath();
}
return clip;
}
@Override
protected void paintBorder(Graphics g) {
if (getParent() == TabHeaders.this) {
super.paintBorder(g);
}
}
@Override
public void actionPerformed(ActionEvent e) {
if (e.getSource() == close) {
TabbedPane.this.removeTab(tab);
}
}
public void delete() {
if (getParent() != null) {
super.getParent().remove(this);
}
}
@Override
public void mouseClicked(MouseEvent e) {
if (e.getSource() != close) {
dropHoveringEffect();
TabHeaders.this.hidePopup();
if (getParent() != TabHeaders.this) {
// Move the tab at the beginning
tabs.remove(tab);
tabs.add(0, tab);
}
TabbedPane.this.selectTab(tab);
}
}
@Override
public void mousePressed(MouseEvent e) {
// TODO Auto-generated method stub
}
@Override
public void mouseReleased(MouseEvent e) {
// TODO Auto-generated method stub
}
@Override
public void mouseEntered(MouseEvent e) {
if (getParent() == extraTabsPopup) {
hovered = true;
setOpaque(true);
defaultBackground = getBackground();
defaultForeground = title.getForeground();
setBackground(UIManager.getColor("List.selectionBackground"));
title.setForeground(UIManager.getColor("List.selectionForeground"));
setCursor(Cursor.getPredefinedCursor(Cursor.HAND_CURSOR));
repaint();
}
}
@Override
public void mouseExited(MouseEvent e) {
dropHoveringEffect();
}
public void dropHoveringEffect() {
if (hovered) {
setOpaque(false);
setBackground(defaultBackground);
title.setForeground(defaultForeground);
defaultBackground = null;
defaultForeground = null;
setCursor(Cursor.getDefaultCursor());
repaint();
hovered = false;
}
}
}
private Map<J, TabHeader> headerComponents = new HashMap<J, TabHeader>();
private JButton extraTabsButton;
private JPopupMenu extraTabsPopup;
private int xBorderStart = 0;
private int xBorderEnd = 0;
public TabHeaders() {
setOpaque(false);
extraTabsButton = new BarButton(UtilsIconLibrary.ARROW_DOWN);
extraTabsButton.setSize(new Dimension(extraTabsButton.getIcon().getIconWidth(), extraTabsButton.getIcon().getIconHeight()));
extraTabsButton.addActionListener(this);
extraTabsPopup = new JPopupMenu();
extraTabsPopup.setInvoker(extraTabsButton);
// extraTabsButton.setComponentPopupMenu(extraTabsPopup);
setBorder(new AbstractBorder() {
@Override
public void paintBorder(Component c, Graphics g, int x, int y, int width, int height) {
g.setColor(Color.LIGHT_GRAY);
g.drawLine(0, height - 1, xBorderEnd, height - 1);
g.drawLine(xBorderStart, height - 1, width, height - 1);
}
});
}
public void hidePopup() {
if (extraTabsPopup.isVisible()) {
extraTabsPopup.setVisible(false);
}
}
@Override
public void actionPerformed(ActionEvent e) {
if (e.getSource() == extraTabsButton) {
if (extraTabsPopup != null) {
if (extraTabsPopup.isVisible()) {
extraTabsPopup.setVisible(false);
} else {
extraTabsPopup.setVisible(true);
Point location = new Point(extraTabsButton.getWidth() - extraTabsPopup.getWidth(), extraTabsButton.getHeight());
SwingUtilities.convertPointToScreen(location, extraTabsButton);
extraTabsPopup.setLocation(location);
}
}
}
}
@Override
public void doLayout() {
extraTabsPopup.removeAll();
xBorderEnd = 0;
xBorderStart = getWidth();
boolean moveToPopup = false;
if (tabs.size() > 0) {
TabHeader selectedHeader = selectedTab != null ? headerComponents.get(selectedTab) : null;
boolean selectedHeaderDone = selectedTab == null;
int x = 0;
int availableWidth = getWidth();
for (int i = 0; i < tabs.size(); i++) {
J tab = tabs.get(i);
TabHeader tabHeader = headerComponents.get(tab);
if (!tabHeader.isVisible()) {
tabHeader.setBounds(0, 0, 0, 0);
selectedHeaderDone |= tabHeader == selectedHeader;
continue;
}
if (!moveToPopup) {
if (!selectedHeaderDone) {
if (tab != selectedTab) {
if (i + 2 == tabs.size()) { // in this case, we only need to put the current tab and the selected tab
moveToPopup = availableWidth
- (tabHeader.getPreferredSize().width + selectedHeader.getPreferredSize().width) < 0;
} else {
moveToPopup = availableWidth
- (tabHeader.getPreferredSize().width + selectedHeader.getPreferredSize().width + extraTabsButton
.getWidth()) < 0;
}
}
if (moveToPopup) {
// There is not enough room to put the current tab header, the selected header and the extraTabs button
if (selectedHeader.getParent() != this) {
add(selectedHeader);
}
tabs.remove(selectedTab);
tabs.add(i, selectedTab);
i++;
selectedHeader.setBounds(x, 0, selectedHeader.getPreferredSize().width, getHeight());
xBorderEnd = x;
xBorderStart = x + selectedHeader.getWidth();
selectedHeaderDone = true;
}
} else {
if (i + 1 == tabs.size()) {
moveToPopup = availableWidth - tabHeader.getWidth() < 0;
} else {
moveToPopup = availableWidth - (tabHeader.getWidth() + extraTabsButton.getWidth()) < 0;
}
}
}
if (moveToPopup) {
if (extraTabsButton.getParent() != this) {
add(extraTabsButton);
}
extraTabsButton.setSize(extraTabsButton.getWidth(), getHeight());
extraTabsButton.setLocation(getWidth() - extraTabsButton.getWidth(), 0);
extraTabsPopup.add(tabHeader);
extraTabsPopup.revalidate();
} else {
if (tabHeader.getParent() != this) {
add(tabHeader);
}
if (x > 0) {
x += TAB_SPACING;
}
tabHeader.setBounds(x, 0, tabHeader.getPreferredSize().width, getHeight());
x += tabHeader.getWidth();
}
availableWidth = getWidth() - x;
selectedHeaderDone |= tab == selectedTab;
}
if (selectedHeader != null) {
xBorderEnd = selectedHeader.getX();
xBorderStart = xBorderEnd + selectedHeader.getWidth();
}
}
if (!moveToPopup) {
if (extraTabsButton.getParent() == this) {
remove(extraTabsButton);
}
}
}
@Override
public Dimension getPreferredSize() {
Dimension prefSize = new Dimension();
for (TabHeader header : headerComponents.values()) {
Dimension headerPrefSize = header.getPreferredSize();
prefSize.width += headerPrefSize.getWidth();
prefSize.height = Math.max(prefSize.height, headerPrefSize.height);
}
return prefSize;
}
@Override
public Dimension getMinimumSize() {
if (selectedTab != null) {
return headerComponents.get(selectedTab).getPreferredSize();
}
return super.getMinimumSize();
}
public void selectTab(J tab) {
doLayout();
repaint();
}
public void refresh() {
for (Entry<J, TabHeader> e : headerComponents.entrySet()) {
e.getValue().refresh();
}
}
public void addTab(J tab) {
headerComponents.put(tab, new TabHeader(tab));
doLayout();
repaint();
}
public void removeTab(J tab) {
TabHeader tabHeader = headerComponents.remove(tab);
if (tabHeader != null) {
Container parent = tabHeader.getParent();
tabHeader.delete();
if (parent == this) {
doLayout();
repaint();
} else if (parent == extraTabsPopup) {
extraTabsPopup.revalidate();
extraTabsPopup.pack();
}
}
}
}
protected TabHeaderRenderer<J> tabHeaderRenderer;
private List<TabListener<J>> tabListeners;
protected TabHeaders tabHeaders;
private JPanel tabBody;
private boolean useTabBody = true;
protected List<J> tabs;
protected J selectedTab;
public TabbedPane() {
tabs = new ArrayList<J>();
tabListeners = new ArrayList<TabbedPane.TabListener<J>>();
tabHeaders = new TabHeaders();
tabBody = new JPanel(new BorderLayout());
tabBody.setBorder(BorderFactory.createMatteBorder(0, 1, 1, 1, Color.LIGHT_GRAY));
}
public TabbedPane(TabHeaderRenderer<J> tabHeaderRenderer) {
this();
this.tabHeaderRenderer = tabHeaderRenderer;
}
public TabHeaders getTabHeaders() {
return tabHeaders;
}
public JPanel getTabBody() {
return tabBody;
}
public boolean isUseTabBody() {
return useTabBody;
}
public void setUseTabBody(boolean useTabBody) {
this.useTabBody = useTabBody;
if (useTabBody) {
for (J tab : tabs) {
if (tab != null && !JComponent.class.isAssignableFrom(tab.getClass())) {
throw new IllegalArgumentException("Cannot use tab body because " + tab + " is not a JComponent");
}
}
}
}
public TabHeaderRenderer<J> getTabHeaderRenderer() {
return tabHeaderRenderer;
}
public void setTabHeaderRenderer(TabHeaderRenderer<J> tabHeaderRenderer) {
this.tabHeaderRenderer = tabHeaderRenderer;
}
public void addToTabListeners(TabListener<J> listener) {
tabListeners.add(listener);
}
public void removeToTabListeners(TabListener<J> listener) {
tabListeners.remove(listener);
}
public J getSelectedTab() {
return selectedTab;
}
public boolean hasTabs() {
return tabs.size() > 0;
}
public boolean containsTab(J tab) {
return tabs.contains(tab);
}
public void addTab(J tab) {
if (tab != null && useTabBody && !JComponent.class.isAssignableFrom(tab.getClass())) {
throw new IllegalArgumentException("Tab must be an instanceof JComponent but received a " + tab.getClass().getName());
}
if (!tabs.contains(tab)) {
tabs.add(tab);
tabHeaders.addTab(tab);
if (selectedTab == null && (tabHeaderRenderer == null || tabHeaderRenderer.isTabHeaderVisible(tab))) {
selectTab(tab);
}
}
}
public void removeTab(J tab) {
int indexOf = tabs.indexOf(tab);
if (tabs.remove(tab)) {
if (selectedTab == tab) {
// TODO: Handle removal of selected tab
if (tabs.size() > 0) {
if (tabHeaderRenderer == null) {
if (indexOf >= tabs.size()) {
selectTab(tabs.get(tabs.size() - 1));
} else {
selectTab(tabs.get(indexOf));
}
} else {
J tabToSelect = null;
for (int i = indexOf - 1; i > -1; i--) {
if (tabHeaderRenderer.isTabHeaderVisible(tabs.get(i))) {
tabToSelect = tabs.get(i);
break;
}
}
if (tabToSelect == null) {
for (int i = indexOf + 1; i < tabs.size(); i++) {
if (tabHeaderRenderer.isTabHeaderVisible(tabs.get(i))) {
tabToSelect = tabs.get(i);
break;
}
}
}
selectTab(tabToSelect);
}
} else {
selectTab(null);
}
}
tabHeaders.removeTab(tab);
fireTabClosed(tab);
}
}
public void selectTab(J tab) {
if (selectedTab == tab) {
return;
}
if (tab != null && !tabs.contains(tab)) {
throw new IllegalArgumentException("Tab must be added to the content pane first.");
}
if (useTabBody && selectedTab != null) {
tabBody.remove((JComponent) selectedTab);
}
selectedTab = tab;
if (useTabBody) {
if (tab != null) {
tabBody.add((JComponent) tab, 0);
} else {
tabBody.add(new JPanel(), 0);
}
tabBody.revalidate();
tabBody.repaint();
}
tabHeaders.selectTab(tab);
fireTabSelected(tab);
}
public void refreshTabHeaders() {
tabHeaders.refresh();
}
protected void fireTabClosed(J tab) {
for (TabListener<J> tabListener : tabListeners) {
tabListener.tabClosed(tab);
}
}
protected void fireTabSelected(J tab) {
for (TabListener<J> tabListener : tabListeners) {
tabListener.tabSelected(tab);
}
}
public static void main(String[] args) {
try {
UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName());
} catch (ClassNotFoundException e1) {
// TODO Auto-generated catch block
e1.printStackTrace();
} catch (InstantiationException e1) {
// TODO Auto-generated catch block
e1.printStackTrace();
} catch (IllegalAccessException e1) {
// TODO Auto-generated catch block
e1.printStackTrace();
} catch (UnsupportedLookAndFeelException e1) {
// TODO Auto-generated catch block
e1.printStackTrace();
}
JFrame frame = new JFrame("Test tabbed panes");
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
TabbedPane<JLabel> tabbedPane = new TabbedPane<JLabel>();
tabbedPane.setTabHeaderRenderer(new TabHeaderRenderer<JLabel>() {
@Override
public boolean isTabHeaderVisible(JLabel tab) {
return true;
}
@Override
public String getTabHeaderTooltip(JLabel tab) {
return "Some tooltip for " + tab.getText();
}
@Override
public String getTabHeaderTitle(JLabel tab) {
return tab.getText();
}
@Override
public Icon getTabHeaderIcon(JLabel tab) {
try {
return new ImageIcon(new URL("http://cdn1.iconfinder.com/data/icons/CuteMonstersPNG/16/blue_monster.png"));
} catch (MalformedURLException e) {
e.printStackTrace();
return null;
}
}
});
for (int i = 0; i < 20; i++) {
JLabel label = new JLabel("Some label " + (i + 1));
label.setHorizontalAlignment(JLabel.CENTER);
tabbedPane.addTab(label);
}
tabbedPane.getTabHeaders().doLayout();
frame.add(tabbedPane.getTabHeaders(), BorderLayout.NORTH);
frame.add(tabbedPane.getTabBody());
frame.setSize(800, 600);
frame.setVisible(true);
}
}