/* Copyright (C) 2006 Christian Schneider
*
* This file is part of Nomad.
*
* Nomad 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 2 of the License, or
* (at your option) any later version.
*
* Nomad 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 Nomad; if not, write to the Free Software
* Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
*/
package net.sf.nmedit.nomad.core.swing.tabs;
import java.awt.event.MouseEvent;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Vector;
import javax.swing.DefaultSingleSelectionModel;
import javax.swing.Icon;
import javax.swing.JComponent;
import javax.swing.SingleSelectionModel;
import javax.swing.ToolTipManager;
import javax.swing.UIManager;
import javax.swing.event.ChangeEvent;
import javax.swing.event.ChangeListener;
import javax.swing.plaf.UIResource;
public class JTabBar<T> extends JComponent
{
/**
*
*/
private static final long serialVersionUID = -5578461150725889787L;
public static String uiClassID = "TabBarUI";
private Vector<Tab> tabs = new Vector<Tab>();
private boolean tipRegistered;
// The default selection model
protected SingleSelectionModel model;
protected ChangeListener changeListener;
// source is always this
protected transient ChangeEvent changeEvent = null;
private boolean closeActionEnabled = true;
private int recentSelectionCounter = 0;
public JTabBar()
{
setOpaque(true);
setUI(FFTabBarUI.createUI(this));
setModel(new DefaultSingleSelectionModel());
}
public void setCloseActionEnabled(boolean enabled)
{
if (this.closeActionEnabled != enabled)
{
this.closeActionEnabled = enabled;
revalidate();
repaint();
}
}
public boolean isCloseActionEnabled()
{
return closeActionEnabled;
}
public String getUIClassID()
{
return uiClassID;
}
public SingleSelectionModel getModel()
{
return model;
}
public void setModel(SingleSelectionModel model) {
SingleSelectionModel oldModel = getModel();
if (oldModel != null) {
oldModel.removeChangeListener(changeListener);
changeListener = null;
}
this.model = model;
if (model != null) {
changeListener = createChangeListener();
model.addChangeListener(changeListener);
}
firePropertyChange("model", oldModel, model);
repaint();
}
protected ChangeListener createChangeListener()
{
return new ModelListener();
}
protected class ModelListener implements ChangeListener, Serializable
{
/**
*
*/
private static final long serialVersionUID = 6525131641051659632L;
public void stateChanged(ChangeEvent e)
{
fireStateChanged();
updateRecentSelectionIndex(getSelectedIndex());
}
}
protected void updateRecentSelectionIndex(int tabIndex)
{
if (tabIndex<0 || tabIndex>=getTabCount())
return;
Tab tab = tabs.get(tabIndex);
tab.recentlySelected = recentSelectionCounter++;
if (recentSelectionCounter == Integer.MAX_VALUE)
{
List<Tab> order = new ArrayList<Tab>(tabs.size());
order.addAll(tabs);
Collections.<Tab>sort(order);
recentSelectionCounter = 0;
for (Tab t: order)
t.recentlySelected = recentSelectionCounter++;
}
}
protected int getPreviousSelection(int tabIndex)
{
if (tabIndex<0 || tabIndex>=getTabCount())
return -1;
Tab tab = tabs.get(tabIndex);
int recentIndex = -1;
int recentValue = -1;
// find tab t with the max(t.recentValue) and
// t.recentValue < tab.recentValue
for (int i=tabs.size()-1;i>=0;i--)
{
if (i != tabIndex)
{
Tab t = tabs.get(i);
if (t.recentlySelected<tab.recentlySelected
&& recentValue < t.recentlySelected)
{
recentIndex = i;
recentValue = t.recentlySelected;
}
}
}
return recentIndex;
}
public void addChangeListener(ChangeListener l) {
listenerList.add(ChangeListener.class, l);
}
public void removeChangeListener(ChangeListener l) {
listenerList.remove(ChangeListener.class, l);
}
public ChangeListener[] getChangeListeners() {
return (ChangeListener[])listenerList.getListeners(
ChangeListener.class);
}
public int getSelectedIndex()
{
return model.getSelectedIndex();
}
public void setSelectedIndex(int index)
{
if (index != -1) checkIndex(index);
setSelectedIndexImpl(index);
}
private void setSelectedIndexImpl(int index)
{
model.setSelectedIndex(index);
}
public int indexOfTab(String title)
{
for(int i = 0; i < getTabCount(); i++)
{
String t = getTitleAt(i);
if (t == title || (t!=null && t.equals(title == null? "" : title))) {
return i;
}
}
return -1;
}
public int indexOfTab(Icon icon) {
for(int i = 0; i < getTabCount(); i++) {
Icon tabIcon = getIconAt(i);
if ((tabIcon != null && tabIcon.equals(icon)) ||
(tabIcon == null && tabIcon == icon)) {
return i;
}
}
return -1;
}
public int indexAtLocation(int x, int y)
{
if (ui != null)
{
return ((TabBarUI)ui).tabForCoordinate(this, x, y);
}
return -1;
}
private void checkIndex(int index) {
if (index < 0 || index >= tabs.size()) {
throw new IndexOutOfBoundsException("Index: "+index+", Tab count: "+tabs.size());
}
}
protected void fireStateChanged()
{
repaint();
Object[] listeners = listenerList.getListenerList();
// Process the listeners last to first, notifying
// those that are interested in this event
for (int i = listeners.length-2; i>=0; i-=2)
{
if (listeners[i]==ChangeListener.class)
{
// Lazily create the event:
if (changeEvent == null)
changeEvent = new ChangeEvent(this);
((ChangeListener)listeners[i+1]).stateChanged(changeEvent);
}
}
}
protected void registerTipComponent(boolean condition)
{
if ((!tipRegistered) && condition)
{
ToolTipManager.sharedInstance().registerComponent(this);
tipRegistered = true;
}
}
public void setUI(TabBarUI ui)
{
super.setUI(ui);
// disabled icons are generated by LF so they should be unset here
for (int i = 0; i < getTabCount(); i++)
{
Icon icon = tabs.get(i).disabledIcon;
if (icon instanceof UIResource)
{
setDisabledIconAt(i, null);
}
}
}
public TabBarUI getUI()
{
return (TabBarUI) ui;
}
public int getTabCount()
{
return tabs.size();
}
public String getToolTipText(MouseEvent event) {
if (ui != null) {
String tip = ((TabBarUI)ui).getToolTipTextAt(this, event.getX(), event.getY());
if (tip != null)
return tip;
}
return super.getToolTipText(event);
}
// Setters for the Tabs
public void setTitleAt(int index, String title) {
Tab tab = tabs.get(index);
String oldTitle = tab.title;
tab.title = title;
if (oldTitle != title) {
firePropertyChange("indexForTitle", -1, index);
}
tab.updateDisplayedMnemonicIndex();
if (title == null || oldTitle == null ||
!title.equals(oldTitle)) {
revalidate();
repaint();
}
}
public void setIconAt(int index, Icon icon) {
Tab tab = tabs.get(index);
Icon oldIcon = tab.icon;
if (icon != oldIcon) {
tab.icon = icon;
/* If the default icon has really changed and we had
* generated the disabled icon for this page, then
* clear the disabledIcon field of the page.
*/
if (tab.disabledIcon instanceof UIResource) {
tab.disabledIcon = null;
}
revalidate();
repaint();
}
}
public void setDisabledIconAt(int index, Icon disabledIcon) {
Tab tab = tabs.get(index);
Icon oldIcon = tab.disabledIcon;
tab.disabledIcon = disabledIcon;
if (disabledIcon != oldIcon && !isEnabledAt(index)) {
revalidate();
repaint();
}
}
public void setToolTipTextAt(int index, String toolTipText) {
Tab tab = tabs.get(index);
//String oldToolTipText = tab.tip;
tab.tip = toolTipText;
registerTipComponent(toolTipText != null);
}
public void setEnabledAt(int index, boolean enabled) {
Tab tab = tabs.get(index);
boolean oldEnabled = tab.enabled;
tab.enabled = enabled;
if (enabled != oldEnabled) {
revalidate();
repaint();
}
}
public void setItemAt(int index, T item) {
Tab tab = tabs.get(index);
T oldItem = tab.item;
tab.item = item;
if (item != oldItem) {
revalidate();
repaint();
}
}
public void setDisplayedMnemonicIndexAt(int index, int mnemonicIndex)
{
Tab tab = tabs.get(index);
int oldMnemonicIndex = tab.mnemonicIndex;
tab.mnemonicIndex = mnemonicIndex;
if (mnemonicIndex != oldMnemonicIndex) {
repaint();
}
}
public void setMnemonicAt(int index, int mnemonic)
{
Tab tab = tabs.get(index);
int oldMnemonic = tab.mnemonic;
tab.mnemonic = mnemonic;
if (mnemonic != oldMnemonic) {
repaint();
}
}
// tabs
public T getSelectedItem()
{
int index = getSelectedIndex();
return index == -1 ? null : tabs.get(index).item;
}
public int indexOfItem(T item)
{
for(int i = 0; i < getTabCount(); i++)
{
T jtem = getItemAt(i);
if ((jtem != null && jtem.equals(item)) ||
(jtem == null && jtem == item)) {
return i;
}
}
return -1;
}
public void setSelectedItem(T item)
{
if (item == null)
{
if (getTabCount()==0)
setSelectedIndexImpl(-1);
return;
}
int index = indexOfItem(item);
if (index != -1)
{
setSelectedIndex(index);
}
else
{
throw new IllegalArgumentException("item not found in tab bar");
}
}
public void moveTab(int target, int source)
{
checkIndex(target);
checkIndex(source);
boolean selected = getSelectedIndex() == source;
if (target == source)
return;
Tab sourceTab = tabs.remove(source);
tabs.add(target, sourceTab);
if (selected)
setSelectedIndex(target);
revalidate();
repaint();
}
public void insertTab(String title, Icon icon, T item, String tip, int index)
{
int newIndex = index;
// If component already exists, remove corresponding
// tab so that new tab gets added correctly
// Note: we are allowing component=null because of compatibility,
// but we really should throw an exception because much of the
// rest of the JTabbedPane implementation isn't designed to deal
// with null components for tabs.
int removeIndex = indexOfItem(item);
if (item != null && removeIndex != -1)
{
removeTabAt(removeIndex);
if (newIndex > removeIndex)
{
newIndex--;
}
}
int selectedIndex = getSelectedIndex();
tabs.insertElementAt(new Tab(this, title != null? title : "", icon, null,
item, tip), newIndex);
if (tabs.size() == 1)
{
setSelectedIndex(0);
}
if (selectedIndex >= newIndex)
{
setSelectedIndexImpl(selectedIndex + 1);
}
registerTipComponent(tip!=null);
revalidate();
repaint();
}
public void addTab(String title, Icon icon, T item, String tip)
{
insertTab(title, icon, item, tip, tabs.size());
}
public void addTab(String title, Icon icon, T item)
{
insertTab(title, icon, item, null, tabs.size());
}
public void addTab(String title, T item)
{
insertTab(title, null, item, null, tabs.size());
}
public void removeTabAt(int index)
{
checkIndex(index);
int selected = getSelectedIndex();
int previouslySelected = getPreviousSelection(selected);
tabs.remove(index);
if (previouslySelected>selected && previouslySelected>0)
{
// !!! important if previouslySelected-1 == index
// then the selection model does not change and
// then it does not fire an event
setSelectedIndexImpl(-1); // set to value(-1)!= previouslySelected-1
setSelectedIndexImpl(previouslySelected-1);
}
else if (previouslySelected>= 0 && previouslySelected<selected)
{
setSelectedIndexImpl(previouslySelected);
}
/* if the selected tab is after the removal */
else if (selected > index) {
setSelectedIndexImpl(selected - 1);
/* if the selected tab is the last tab */
} else if (selected >= getTabCount()) {
setSelectedIndexImpl(selected - 1);
/* selected index hasn't changed, but the associated tab has */
} else if (index == selected) {
fireStateChanged();
}
if (tabs.isEmpty())
recentSelectionCounter = 0;
revalidate();
repaint();
}
// Getters for the Tabs
public String getTitleAt(int index) {
return tabs.get(index).title;
}
public Icon getIconAt(int index) {
return tabs.get(index).icon;
}
public Icon getDisabledIconAt(int index) {
Tab tab = tabs.get(index);
if (tab.disabledIcon == null) {
tab.disabledIcon = UIManager.getLookAndFeel().getDisabledIcon(this, tab.icon);
}
return tab.disabledIcon;
}
/**
* Returns the tab tooltip text at <code>index</code>.
*
* @param index the index of the item being queried
* @return a string containing the tool tip text at <code>index</code>
* @exception IndexOutOfBoundsException if index is out of range
* (index < 0 || index >= tab count)
*
* @see #setToolTipTextAt
* @since 1.3
*/
public String getToolTipTextAt(int index) {
return tabs.get(index).tip;
}
/**
* Returns whether or not the tab at <code>index</code> is
* currently enabled.
*
* @param index the index of the item being queried
* @return true if the tab at <code>index</code> is enabled;
* false otherwise
* @exception IndexOutOfBoundsException if index is out of range
* (index < 0 || index >= tab count)
*
* @see #setEnabledAt
*/
public boolean isEnabledAt(int index) {
return tabs.get(index).enabled;
}
public T getItemAt(int index) {
return tabs.get(index).item;
}
public int getMnemonicAt(int tabIndex) {
return tabs.get(tabIndex).getMnemonic();
}
public int getDisplayedMnemonicIndexAt(int tabIndex) {
return tabs.get(tabIndex).getDisplayedMnemonicIndex();
}
static int findDisplayedMnemonicIndex(String text, int mnemonic) {
if (text == null || mnemonic == '\0') {
return -1;
}
char uc = Character.toUpperCase((char)mnemonic);
char lc = Character.toLowerCase((char)mnemonic);
int uci = text.indexOf(uc);
int lci = text.indexOf(lc);
if (uci == -1) {
return lci;
} else if(lci == -1) {
return uci;
} else {
return (lci < uci) ? lci : uci;
}
}
private class Tab implements Comparable<Tab>
{
String title;
Icon icon;
Icon disabledIcon;
JTabBar parent;
T item;
String tip;
boolean enabled = true;
boolean needsUIUpdate;
int mnemonic = -1;
int mnemonicIndex = -1;
int recentlySelected = 0;
Tab(JTabBar parent,
String title, Icon icon, Icon disabledIcon, T item, String tip) {
this.title = title;
this.icon = icon;
this.disabledIcon = disabledIcon;
this.parent = parent;
this.item = item;
this.tip = tip;
}
void setMnemonic(int mnemonic)
{
this.mnemonic = mnemonic;
updateDisplayedMnemonicIndex();
}
int getMnemonic()
{
return mnemonic;
}
/*
* Sets the page displayed mnemonic index
*/
void setDisplayedMnemonicIndex(int mnemonicIndex) {
if (this.mnemonicIndex != mnemonicIndex) {
if (mnemonicIndex != -1 && (title == null ||
mnemonicIndex < 0 ||
mnemonicIndex >= title.length())) {
throw new IllegalArgumentException(
"Invalid mnemonic index: " + mnemonicIndex);
}
this.mnemonicIndex = mnemonicIndex;
JTabBar.this.firePropertyChange("displayedMnemonicIndexAt",
null, null);
}
}
/*
* Returns the page displayed mnemonic index
*/
int getDisplayedMnemonicIndex() {
return this.mnemonicIndex;
}
void updateDisplayedMnemonicIndex() {
setDisplayedMnemonicIndex(findDisplayedMnemonicIndex(title, mnemonic));
}
public int compareTo(Tab o)
{
if (recentlySelected<o.recentlySelected)
return -1;
else if (recentlySelected==o.recentlySelected)
return 0;
else
return 1;
}
}
public void askRemove(int index)
{
firePropertyChange("ask-remove", -1, index);
}
public void showContextMenuForTab(MouseEvent e, int tabIndex)
{
firePropertyChange("show-context-menu", e, tabIndex);
}
}