// Copyright (c) 2006 - 2008, Markus Strauch.
// All rights reserved.
//
// Redistribution and use in source and binary forms, with or without
// modification, are permitted provided that the following conditions are met:
//
// * Redistributions of source code must retain the above copyright notice,
// this list of conditions and the following disclaimer.
// * Redistributions in binary form must reproduce the above copyright notice,
// this list of conditions and the following disclaimer in the documentation
// and/or other materials provided with the distribution.
//
// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
// AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
// IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
// ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
// LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
// CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
// SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
// INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
// CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
// ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF
// THE POSSIBILITY OF SUCH DAMAGE.
package net.sf.sdedit.ui.components;
import java.awt.Component;
import java.awt.Graphics;
import java.awt.Image;
import java.awt.Rectangle;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
import java.util.HashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Set;
import javax.swing.Icon;
import javax.swing.ImageIcon;
import javax.swing.JTabbedPane;
import javax.swing.SwingConstants;
import net.sf.sdedit.util.base64.Base64;
/**
* An <tt>ATabbedPane</tt> is an advanced <tt>JTabbedPane</tt> that allows
* a user to close tabs and that assigns unique names to tabs.
* <p>
* Unique names are generated according to this policy: If a tab should be
* given a name <it>X</it> such that a tab named <it>X</it>
* already exists, we search for the smallest integer number
* <it>i>0</it> such that there is no tab with the name <it>X-i</it>.
* The tab is then named <it>X-i</it>.
*
* @author Markus Strauch
*
*/
public class ATabbedPane extends JTabbedPane {
private final static long serialVersionUID = 0xAB343921;
private static String cleanString = "iVBORw0KGgoAAAANSUhEUgAAABA"
+ "AAAAQCAYAAAAf8/9hAAAABGdBTUEAAK/INwWK6QAAABl0RVh0U29mdHdhcmUAQWRv"
+ "YmUgSW1hZ2VSZWFkeXHJZTwAAAOVSURBVHjaYtTUdGdAA4K/fv3U+Pv3rw+QLfT//"
+ "3+Gf//+vWNiYtjCwsJyAyj2HiT2//8/sGKAAGJB1gkU9Pj581eNnJyctaamMgM/Py"
+ "8DIyMDw+fPXxlu3rxfdfPmjaPMzIwtTEzMO2B6AAKIBaH5fw4LC1tHeHgQt7u7PYO"
+ "OjhIDNzcb2IBfv/4x3LjxiGHr1n3WK1duXPPx45sKJiamKSB9AAHECPIC0GZ3ZmbW"
+ "zQkJkazu7rYMLCyMDD9//gYZCzWcgYGVlRUozsxw9Oh5hv7+Gb8/fXrnC+TvBAggZ"
+ "hERZb7fv3/PdnCwV7C3twT69w+DlpYcw5s3HxkeP34FdP53IPsDg6qqNAMXFxvQIA"
+ "4GoGXMFy9eVgK6eg1AADH9/ftbW0hIxEpFRQms0MBAlYGDg51BQ0OegZ2dneH58zd"
+ "AMRUGKSlhBnFxQYY7dx4CvfSHQVBQyAqkFyCAmIWEFDOlpaVtgQHH8O7dB4aXLz8w"
+ "qKjIMHBysoE1SUqKMCgoSIC90te3lGHNmu0MDx8+Yfjx4xvQmz9eAgQQCzAwhBiBI"
+ "fX69RugwC+GR4+eAl3yliEx0Y+Bl5eDQU5ODBwG3d0LGdau3QH0AjMwLFiBruQEBj"
+ "CTEEAAsYBC+du3HwxPnjxnAMY90JCfoLBlePXqLdAAabDNX778AHvl37+/QP9DYub"
+ "fP0haAAggJlAi+fr1M8Pbt2+Bml4z8PBwMxQURDMoK0uDbf78+QfYJY2N2Qy2thZA"
+ "//8CGsIMtOg70MI/7wACiAkYkluAfmH48+cPMOHwMbS1FTJoaspB/bwYqHE6w4cP3"
+ "xn4+DgYWltzgAGqywCMNbABQBdsAQggJmAsX/3+/esxkPNAoX7jxgNQomKYMWMtw6"
+ "5dRxkuXLjGMHHiEobv338x3Lv3DEhDLAO6+hjQq1cBAohRWdkOqOGvOwcHz2Z1dU1"
+ "WcXEJBgkJYYbbtx+AExIogH/9+s2gra0KDOgPwLTxmOHKlfO/v3z55AtM0jsBAggY"
+ "jfKg0Lz769eP958/f7FnZ2djAyYUBhERQWBUcgLDhItBWFiY4f37j8AYeshw/frVr"
+ "1++fCwFal4O8iZAAIENAKdpRoZTwLg99/Llc8VPnz7JffnyFWQwMAa+Mdy/fw+YmW"
+ "4w3Lp1/eiPH19zgJqXwfIQQACBvQDNiaBsC/K/IDCQNICKfNjYWIVAYQNMH++AIb4"
+ "FGPrg7IycgwECDADIUZC5UWvTuwAAAABJRU5ErkJggg==";
private static String stainedString = "iVBORw0KGgoAAAANSUhEUgAAA"
+ "BAAAAAQCAYAAAAf8/9hAAAABmJLR0QA/wD/AP+gvaeTAAAACXBIWXMAAAsTAAALEw"
+ "EAmpwYAAAAB3RJTUUH1wMbBRAgdDDcCwAAAyVJREFUOMtdk0tsVGUYhp/vP2eml9P"
+ "LzLRAq4MUqKVyS2tIkBZKCCZl0bIgLly68xLcSECjBkx04cIlCy8bFgYXspKaiqZN"
+ "jGkXalACaGPaUrHTmSntdJzpdOac8//nd6FtiO/++fK+yfPJlXQj/0vSGNNrTDQCp"
+ "AAibEEhY0rJDLAGYK0FwH2ctNae0Vq/29a+bXBH127qm1oACKoV8g/m387lc1MK+U"
+ "Ap+WaTcR+DzzuO++Gzp4e9/Wdf4MnBU8SaWxDHwdSq5H+a5u6Xnw/e/nb8RrVafUu"
+ "EqwByJd2ItXZYKXXzudFzsf3nXkS5MUzgbzWLjMGtq0eUYn5inMlrn4ZV3x91RG45"
+ "J5vdliiKPnv6cF9Xz/NnsFFE59HjlB4uUJz/g2phhfVclm2H+om3JlBKIdWKk5mf3"
+ "WPhhrLWHmj0vIHte3soZ5dInziN6zXRefQ4bn0DpaVF0seGaO3eR/OuPTz6/S4mDP"
+ "EaGge05YAz1OS8mki1nUh0PsFGYYXSg1m2H+on1tRMa1c3rTt30XawD4CJCy9zZ/w"
+ "rVrNLhGGIMTrvikhKRKisrqADn7XMX1RW8gy8+T7xRJJk70GsMUxcfIVfvhvHcVyU"
+ "o4jFY1CrplwRwfdrrGUzRMagwxCA0sIc7X1HAAjLJf7OZbHWIiIoUVseKGttIfB9N"
+ "kolysUi9Z7HqUvv0d53BGsMQXGNeCLJyNVr7D3cjzEaEUGHGm0pKGvtmA41URTR4H"
+ "mMfvQxHceGAJi89Bpfv/4S1XyWulQ7Zz+5Trq7B601WodEMKaA+0HgTxtjcGMxlu/"
+ "8jDWGHy6/wczU9yzO/Mbk5QuE5RKr934l9H2iKGIj1NPAfXmnIw4w7LruzY70zlhz"
+ "WzstOzpYnpsF+K9uQOe+Z1hfeUQxnyPz50JY0dGoCLecoSYHYE5rs1bbqJx0HSeul"
+ "MJLthFvaKCu0cNLpKiWihSWMuQyi5WKNhdF+AJg8wAi8mOg9e1ysbi7Vi49FVTWEc"
+ "CvrFNYWiS/+JDs8vJUzUTnRbi+qfnmhH+djyxAMrC21xVGXKVSUWSpWVuwljERtt5"
+ "5M/8AP9V9H5Qdd08AAAAASUVORK5CYII=";
private static Image clean = Base64.decodeBase64EncodedImage(cleanString);
private static Image stain = Base64.decodeBase64EncodedImage(stainedString);
private List<ATabbedPaneListener> listeners;
/**
* Constructor.
*
*/
public ATabbedPane() {
super(SwingConstants.TOP);
listeners = new LinkedList<ATabbedPaneListener>();
addMouseListener(new MouseAdapter() {
@Override
public void mouseClicked(MouseEvent e) {
int tabNumber = getUI().tabForCoordinate(ATabbedPane.this,
e.getX(), e.getY());
if (tabNumber < 0)
return;
Rectangle rect = ((ACloseTabIcon) ATabbedPane.this
.getIconAt(tabNumber)).getBounds();
if (rect.contains(e.getX(), e.getY())) {
ATabbedPane.this.selectAndCloseTab(tabNumber);
}
}
});
}
/**
* Returns the title of the tab currently being selected or
* the empty string, if no tab is selected.
*
* @return the title of the tab currently being selected
*/
public String getCurrentTitle() {
if (getSelectedIndex() == -1) {
return "";
}
return getTitleAt(getSelectedIndex());
}
/**
* Adds an <tt>ATabbedPaneListener</tt> whose responsibility is to
* decide whether a tab will actually be closed on user demand.
*
* @param listener an <tt>ATabbedPaneListener</tt>
*/
public void addListener(ATabbedPaneListener listener) {
listeners.add(listener);
}
private String generateUniqueTabName(String tabName) {
Set<String> titles = new HashSet<String>();
for (int i = 0; i < getTabCount(); i++) {
titles.add(getTitleAt(i));
}
if (!titles.contains(tabName)) {
return tabName;
}
int i = 1;
while (true) {
String trial = tabName + "-" + i;
if (!titles.contains(trial)) {
return trial;
}
i++;
}
}
/**
* Sets the name of the currently selected tab to the given
* <tt>title</tt> or to <tt>title</tt> with an integer number appended,
* as described in the class comment: {@linkplain ATabbedPane}.
*
* @param title the new name of the currently selected tab
*/
public void setTabTitle(String title) {
int i = getSelectedIndex();
setTitleAt(i, "");
title = generateUniqueTabName(title);
setTitleAt(i, title);
}
/**
* Adds a tab to this <tt>ATabbedPane</tt> and assigns the given
* <tt>title</tt> to it, which may be changed as described in the
* class comment ({@linkplain ATabbedPane}) in order to get a unique
* title.
*
* @param tab the tab to be added
* @param title the title of the tab to be added
* @return the unique name of the tab after it has been added
*/
public String addTab(Component tab, String title) {
title = generateUniqueTabName(title);
ACloseTabIcon icon = new ACloseTabIcon(tab, this);
addTab(title, icon, tab);
setSelectedIndex(getTabCount() - 1);
return title;
}
/**
* Adds a tab to this <tt>ATabbedPane</tt> and assigns the given
* <tt>title</tt> to it, which may be changed as described in the
* class comment ({@linkplain ATabbedPane}) in order to get a unique
* title.
*
* @param tab the tab to be added
* @param title the title of the tab to be added
* @param closeIcon the icon for closing the tab
* @return the unique name of the tab after it has been added
*/
public String addTab(Component tab, String title, ImageIcon closeIcon) {
title = generateUniqueTabName(title);
ACloseTabIcon icon = new ACloseTabIcon(tab, this, closeIcon);
addTab(title, icon, tab);
setSelectedIndex(getTabCount() - 1);
return title;
}
/**
* Removes the currently selected tab. If <tt>oneOpen</tt> is true
* and there is only one tab, it will not be removed.
*
* @param oneOpen
* flag denoting if at least one tab must stay open
* @return flag denoting if the current tab has in fact been removed
*/
public boolean removeCurrentTab(boolean oneOpen) {
if (getTabCount() == 0) {
return false;
}
if (oneOpen && getTabCount() == 1) {
return false;
}
int number = getSelectedIndex();
if (number != -1) {
remove(number);
return true;
}
return false;
}
protected void fireCurrentTabClosing() {
for (ATabbedPaneListener listener : listeners) {
listener.currentTabClosing();
}
}
private void selectAndCloseTab(int number) {
setSelectedIndex(number);
if (getTabCount() > 1) {
fireCurrentTabClosing();
}
}
private class ACloseTabIcon implements Icon, StainedListener {
private int x_pos;
private int y_pos;
private Image image;
/**
* Creates a new <tt>CloseTabIcon</tt> for a tab in a
* {@linkplain ATabbedPane}.
*
* @param tab
* a tab
* @param pane
* the tabbed pane
* @param icon
*/
public ACloseTabIcon(Component tab, ATabbedPane pane, ImageIcon icon) {
image = icon.getImage();
}
/**
* Creates a new <tt>CloseTabIcon</tt> for a possibly
* "stainable" tab in a {@linkplain ATabbedPane}. If the
* <tt>tab</tt>'s class implements {@linkplain Stainable}, the
* <tt>CloseTabIcon</tt> registers as a {@linkplain StainedListener}
* and will reflect the stained status of the tab by the color of the
* icon.
*
* @param tab
* a tab
* @param pane
* the tabbed pane
*/
public ACloseTabIcon(Component tab, ATabbedPane pane) {
if (tab instanceof Stainable) {
((Stainable) tab).addStainedListener(this);
}
image = clean;
}
/**
* Changes the icon so it reflects the state of being
* stained or clean.
*
* @see net.sf.sdedit.ui.components.StainedListener#stainedStatusChanged(boolean)
*/
public void stainedStatusChanged(boolean stained) {
image = stained ? stain : clean;
repaint();
}
/**
* @see javax.swing.Icon#paintIcon(java.awt.Component,
* java.awt.Graphics, int, int)
*/
public void paintIcon(Component c, Graphics g, int x, int y) {
g.drawImage(image, x, y, c);
this.x_pos = x;
this.y_pos = y;
}
/**
* @see javax.swing.Icon#getIconWidth()
*/
public int getIconWidth() {
return image.getWidth(null);
}
/**
* @see javax.swing.Icon#getIconHeight()
*/
public int getIconHeight() {
return image.getHeight(null);
}
/**
* Returns a <tt>Rectangle</tt> where this icon is currently being
* displayed.
*
* @return a <tt>Rectangle</tt> where this icon is currently being
* displayed
*/
public Rectangle getBounds() {
return new Rectangle(x_pos, y_pos, getIconWidth(), getIconHeight());
}
}
}