/*
* Copyright (c) 2009, SQL Power Group Inc.
*
* This file is part of Wabit.
*
* Wabit 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.
*
* Wabit 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 this program. If not, see <http://www.gnu.org/licenses/>.
*/
package ca.sqlpower.swingui;
import java.awt.Color;
import java.awt.Component;
import java.awt.Dimension;
import java.awt.GradientPaint;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
import java.awt.event.MouseListener;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.concurrent.Callable;
import javax.swing.BorderFactory;
import javax.swing.Icon;
import javax.swing.ImageIcon;
import javax.swing.JComponent;
import javax.swing.JLabel;
import javax.swing.JPanel;
import javax.swing.JTabbedPane;
import javax.swing.border.Border;
import javax.swing.event.ChangeEvent;
import javax.swing.event.ChangeListener;
import net.miginfocom.swing.MigLayout;
import org.apache.log4j.Logger;
/**
* A custom JComponent to implement the idea of a 'stack' of tabs. The idea is
* similar to the JTabbedPane, except the tabs are stacked on top of each other
* vertically. When a 'tab' is selected, its component is painted in between
* the tab and the next tab (or at the every bottom if it's the last tab). The
* API is only a subset of the {@link JTabbedPane} API, so it's not yet a full
* drop-in replacement for JTabbedPane. It currently only implements the subset
* of methods that are called from {@link WabitSwingSessionContextImpl}, so
* that it can be used in the Wabit.
*/
public class StackedTabComponent extends JComponent {
private static final Logger logger = Logger
.getLogger(StackedTabComponent.class);
/**
* Set of gradient colours for a selected tab
*/
private static final Color SELECTED_TAB_GRADIENT_TOP = new Color(255, 204, 66);
private static final Color SELECTED_TAB_GRADIENT_BOTTOM = new Color(255, 99, 00);
/**
* Set of gradient colours for an unselected tab
*/
private static final Color UNSELECTED_TAB_RADIENT_TOP = new Color(221, 221, 221);
private static final Color UNSELECTED_TAB_GRADIENT_BOTTOM = new Color(204, 204, 204);
/**
* Set of border colours for tabs (unselected and selected)
*/
private final Border UNSELECTED_LABEL_BORDER = BorderFactory.createLineBorder(new Color(187, 187, 187), 1);
private final Border SELECTED_OR_HOVERING_OVER_LABEL_BORDER = BorderFactory.createLineBorder(SELECTED_TAB_GRADIENT_TOP, 1);
private static final Icon closeIcon = new ImageIcon(StackedTab.class.getClassLoader().getResource("ca/sqlpower/swingui/closeWorkspace-12.png"));
/**
* A list of tabs that this component currently contains
*/
private List<StackedTab> tabs = new ArrayList<StackedTab>();
/**
* The tab that is currently 'selected'
*/
private StackedTab selectedTab;
/**
* A list of change listeners that receive state change events from the
* {@link StackedTabComponent}. This is primarily when the selected tab has
* changed.
*/
private List<ChangeListener> changeListeners = new ArrayList<ChangeListener>();
public StackedTabComponent() {
setLayout(new MigLayout("flowy, hidemode 3, fill, ins 0, gap 0 0", "", ""));
}
/**
* Represents an individual 'tab'. It consists of a label component, which
* is the tab itself, and a subcomponent which is the component contained by
* the tab.
*/
public class StackedTab {
/**
* The title of this tab
*/
private String title;
/**
* A JLabel which represents the tab itself
*/
private JLabel titleLabel;
/**
* The {@link Component} contained by this tab
*/
private final Component subComponent;
private final JLabel closeIconComponent;
private final JComponent tabComponent;
private final boolean closeable;
/**
* Creates a new StackedTab with the given title and containing the
* given {@link Component}
*
* @param title
* The title to give to this tab
* @param component
* The {@link Component} that this tab will contain
* @param closeable
* If true, then renders the close button. If false, doesn't.
*/
private StackedTab(String title, Component component, boolean closeable) {
this.title = title;
this.closeable = closeable;
tabComponent = new JPanel(new MigLayout("hidemode 3, fill, ins 0, gap 0 0", "", ""));
if (closeable) {
closeIconComponent = new JLabel(closeIcon) {
@Override
protected void paintComponent(Graphics g) {
Graphics2D g2 = (Graphics2D)g;
Color topColor = (StackedTab.this == selectedTab) ? SELECTED_TAB_GRADIENT_TOP : UNSELECTED_TAB_RADIENT_TOP;
Color bottomColor = (StackedTab.this == selectedTab) ? SELECTED_TAB_GRADIENT_BOTTOM : UNSELECTED_TAB_GRADIENT_BOTTOM;
GradientPaint gp = new GradientPaint(0, 0, topColor, 0, getHeight(), bottomColor);
g2.setPaint(gp);
g2.fillRect(0, 0, getWidth(), tabComponent.getHeight());
super.paintComponent(g);
}
};
closeIconComponent.setVisible(false);
} else {
closeIconComponent = null;
}
titleLabel = new JLabel(" " + title) {
// Override paintComponent to give it a gradient background
@Override
protected void paintComponent(Graphics g) {
Graphics2D g2 = (Graphics2D)g;
Color topColor = (StackedTab.this == selectedTab) ? SELECTED_TAB_GRADIENT_TOP : UNSELECTED_TAB_RADIENT_TOP;
Color bottomColor = (StackedTab.this == selectedTab) ? SELECTED_TAB_GRADIENT_BOTTOM : UNSELECTED_TAB_GRADIENT_BOTTOM;
GradientPaint gp = new GradientPaint(0, 0, topColor, 0, getHeight(), bottomColor);
g2.setPaint(gp);
g2.fillRect(0, 0, StackedTabComponent.this.getWidth(), tabComponent.getHeight());
super.paintComponent(g);
}
};
titleLabel.setMinimumSize(new Dimension(getMinimumSize().width, closeIcon.getIconHeight()));
tabComponent.add(titleLabel, "grow 100 100, push 100 100");
if (closeIconComponent != null) {
tabComponent.add(closeIconComponent, "grow 0 100, push 0 100");
}
tabComponent.addMouseListener(new MouseListener() {
public void mouseClicked(MouseEvent e) {
// Use mousePressed or mouseReleased events instead
}
public void mouseEntered(MouseEvent e) {
if (closeIconComponent != null) {
closeIconComponent.setVisible(true);
}
if (StackedTab.this != selectedTab) {
tabComponent.setBorder(SELECTED_OR_HOVERING_OVER_LABEL_BORDER);
}
}
public void mouseExited(MouseEvent e) {
if (closeIconComponent != null) {
closeIconComponent.setVisible(false);
}
if (StackedTab.this != selectedTab) {
tabComponent.setBorder(UNSELECTED_LABEL_BORDER);
}
}
public void mousePressed(MouseEvent e) {
setSelectedIndex(tabs.indexOf(StackedTab.this));
}
public void mouseReleased(MouseEvent e) {
// do nothing?
}
});
tabComponent.setBorder(UNSELECTED_LABEL_BORDER);
this.subComponent = component;
}
/**
* Returns the Component representing the Tab in the stack of tabs
*/
public Component getTabComponent() {
return tabComponent;
}
public boolean isCloseable() {
return closeable;
}
/**
* Returns true if the close 'button' is contained with the given
* xy co-ordinates. The xy co-ordinates should be relative to the
* StackedTab tab component.
*/
public boolean closeButtonContains(int x, int y) {
int relativeX = x - closeIconComponent.getX();
int relativeY = y - closeIconComponent.getY();
if (closeIconComponent.contains(relativeX, relativeY)) {
return true;
} else {
return false;
}
}
}
/**
* Adds a new tab to the bottom of the stack.
*
* @param title
* The label to display on the tab itself.
* @param comp
* The component to display beneath the tab when the tab is
* selected.
* @param closeable
* Whether or not the new tab will have a functioning close
* button.
* @return The newly created tab object.
*/
public StackedTab addTab(String title, Component comp, boolean closeable) {
final StackedTab tab = new StackedTab(title, comp, closeable);
tab.subComponent.setVisible(false);
tabs.add(tab);
add(tab.tabComponent, "grow 100 0, push 100 0");
add(tab.subComponent, "grow 100 100, push 100 100");
return tab;
}
/**
* Adds a new tab to the bottom of the stack.
*
* @param title
* The label to display on the tab itself.
* @param comp
* The component to display beneath the tab when the tab is
* selected.
* @param closeable
* If not null the tab added will have a close icon displayed
* when it is hovered over and the action will be run when the
* close button is clicked. If the action is performed and
* returns true the tab will be removed from this stacked tab
* component. If the action returns false the tab will not be
* removed.
* @return The newly created tab object.
*/
public StackedTab addTab(String title, Component comp, final Callable<Boolean> closeAction) {
final StackedTab tab = addTab(title, comp, closeAction != null);
tab.getTabComponent().addMouseListener(new MouseAdapter() {
public void mousePressed(MouseEvent e) {
if (tab.isCloseable() && tab.closeButtonContains(e.getX(), e.getY())) {
try {
if (closeAction.call() && indexOfTab(tab) >= 0) {
removeTabAt(indexOfTab(tab));
revalidate();
}
} catch (Exception ex) {
throw new RuntimeException(ex);
}
}
}
});
return tab;
}
/**
* Set the selected tab to the one at the given index. If the index is
* outside the range of the current list of tabs, then selected tab is set
* to null.
*/
public void setSelectedIndex(int i) {
StackedTab oldTab = selectedTab;
if (oldTab != null){
oldTab.subComponent.setVisible(false);
oldTab.tabComponent.setBorder(UNSELECTED_LABEL_BORDER);
}
if (i < 0 || i >= tabs.size()) {
selectedTab = null;
} else {
selectedTab = tabs.get(i);
selectedTab.subComponent.setVisible(true);
selectedTab.tabComponent.setBorder(SELECTED_OR_HOVERING_OVER_LABEL_BORDER);
}
StackedTabComponent.this.repaint();
if (oldTab != selectedTab) {
fireStateChanged();
}
}
/**
* Notifies the list of {@link ChangeListener}s of a state change, usually
* the selected tab being changed
*/
private void fireStateChanged() {
for (ChangeListener listener : changeListeners) {
ChangeEvent e = new ChangeEvent(this);
listener.stateChanged(e);
}
}
/**
* Returns the current number of tabs
*/
public int getTabCount() {
return tabs.size();
}
public List<StackedTab> getTabs() {
return Collections.unmodifiableList(tabs);
}
/**
* Sets the title of a given tab.
*
* @param index
* The index of the tab that we're changing the title for
* @param newValue
* The new title for tab at the given index
*/
public void setTitleAt(int index, String newValue) {
final StackedTab tab = tabs.get(index);
if (tab != null) {
tab.title = newValue;
tab.titleLabel.setText(" " + newValue);
}
}
/**
* Add a {@link ChangeListener} to listen for state changes to the tab
*/
public void addChangeListener(ChangeListener tabChangeListener) {
changeListeners.add(tabChangeListener);
}
/**
* Returns the index of the first tab which has a title that matches the
* given string
*
* @param string
* A string to match with a tab's title
* @return The index of the first tab that has a title that matches the
* given string. If no such tab exists, returns -1.
*/
public int indexOfTab(String string) {
for (int i = 0; i < tabs.size(); i++){
StackedTab tab = tabs.get(i);
if (tab.title.equals(string)) {
return i;
}
}
return -1;
}
/**
* Returns the index of the given tab in this component's tab order.
*
* @param tab
* the tab to find the index of.
* @return The index of the given tab in this component's tab list, which
* matches the visually displayed order of the tabs. If the tab is
* not presently associated with this component, returns -1.
*/
public int indexOfTab(StackedTab tab) {
return tabs.indexOf(tab);
}
/**
* Returns the index of the current selected tab. If no tab is selected,
* then returns -1
*/
public int getSelectedIndex() {
return tabs.indexOf(selectedTab);
}
/**
* Removes the tab at the given index
*
* @param i
* The index of the tab to remove
* @throws IndexOutOfBoundsException
* if i < 0 or i >= to tabs.size()
*/
public void removeTabAt(int i) {
StackedTab removedTab = tabs.get(i);
if (logger.isDebugEnabled()) {
logger.debug("removed tab at " + i + ", removed tab " + removedTab.titleLabel,
new Exception("For stack trace"));
}
if (removedTab != null) {
remove(removedTab.tabComponent);
remove(removedTab.subComponent);
}
tabs.remove(i);
}
/**
* Returns the index of the tab that contains the given set of co-ordinates
*
* @param x
* The x co-ordinate to check
* @param y
* The y co-ordinate to check
* @return The index of the tab containing the x and y co-ordinates
*/
public int indexAtLocation(int x, int y) {
for (int i = 0; i < tabs.size(); i++) {
StackedTab tab = tabs.get(i);
int xRelativeToLabel = x - tab.tabComponent.getX();
int yRelativeToLabel = y - tab.tabComponent.getY();
boolean labelContains = tab.tabComponent.contains(xRelativeToLabel, yRelativeToLabel);
int xRelativeToSubComp = x - tab.tabComponent.getX();
int yRelativeToSubComp = y - tab.tabComponent.getY();
boolean subcompContains = tab.tabComponent.contains(xRelativeToSubComp, yRelativeToSubComp);
if (labelContains || subcompContains) {
return i;
}
}
return -1;
}
/**
* Returns the StackedTab representing the currently selected tab.
*/
public StackedTab getSelectedTab() {
return selectedTab;
}
}