package org.limewire.ui.swing.components;
import java.awt.Component;
import java.awt.Dimension;
import java.awt.Insets;
import java.awt.event.ActionEvent;
import java.awt.event.ComponentAdapter;
import java.awt.event.ComponentEvent;
import java.util.ArrayList;
import java.util.List;
import javax.swing.AbstractAction;
import javax.swing.Action;
import javax.swing.Icon;
import javax.swing.JComponent;
import javax.swing.JPopupMenu;
import net.miginfocom.swing.MigLayout;
import org.jdesktop.application.Resource;
import org.limewire.ui.swing.components.decorators.ComboBoxDecorator;
import org.limewire.ui.swing.util.GuiUtils;
import com.google.inject.Inject;
/**
* A horizontal container for {@link FancyTab FancyTab} objects.
* FlexibleTabList adjusts the number of visible tabs depending on the container
* size, and displays a "more" button when the actual tab count exceeds the
* visible count. New tabs may be added to the container by calling the
* {@link #addTabActionMapAt(TabActionMap, int) addTabActionMapAt()} method.
*
* <p>FlexibleTabList is used to display the search tabs at the top of the main
* window.</p>
*/
public class FlexibleTabList extends AbstractTabList {
private static final int MAX_TAB_WIDTH = 205;
private static final int MIN_TAB_WIDTH = 115;
private static final int RIGHT_INSET = 3;
@Resource private Icon moreDefaultIcon;
@Resource private Icon morePressedIcon;
@Resource private Icon moreRolloverIcon;
private final ComboBoxDecorator comboBoxDecorator;
private final Action closeOtherAction;
private final Action closeAllAction;
private int maxVisibleTabs;
private int vizStartIdx = -1;
/**
* Constructs a FlexibleTabList with the specified combobox decorator.
*/
@Inject
FlexibleTabList(ComboBoxDecorator comboBoxDecorator) {
this.comboBoxDecorator = comboBoxDecorator;
GuiUtils.assignResources(this);
setOpaque(false);
setLayout(new MigLayout("insets 0 0 0 3, gap 0, filly, hidemode 2"));
setMinimumSize(new Dimension(0, getMinimumSize().height));
closeOtherAction = new CloseOther();
closeAllAction = new CloseAll();
maxVisibleTabs = Integer.MAX_VALUE;
// Add listener to adjust tab layout when container is resized.
addComponentListener(new ComponentAdapter() {
@Override
public void componentResized(ComponentEvent e) {
// Redo tab layout when number of tabs changes.
if (calculateVisibleTabCount() != maxVisibleTabs) {
layoutTabs();
}
}
});
}
/**
* Adds a new tab using the specified action map at the specified index.
*/
public void addTabActionMapAt(TabActionMap actionMap, int i) {
FancyTab tab = createAndPrepareTab(actionMap);
addTab(tab, i);
}
/**
* Creates a new tab with the specified action map. This calls the
* superclass method to create the tab, sets the minimum and maximum widths,
* and adds close actions.
*/
@Override
protected FancyTab createAndPrepareTab(TabActionMap actionMap) {
// Create tab.
FancyTab tab = super.createAndPrepareTab(actionMap);
// Set minimum and maximum widths.
tab.setMinimumSize(new Dimension(MIN_TAB_WIDTH, tab.getMinimumSize().height));
tab.setMaximumSize(new Dimension(MAX_TAB_WIDTH, tab.getMaximumSize().height));
// Add Close actions.
actionMap.setRemoveAll(closeAllAction);
actionMap.setRemoveOthers(closeOtherAction);
return tab;
}
/**
* Updates the layout to display the visible tabs. This method removes all
* visible tabs, and lays them out again.
*/
@Override
protected void layoutTabs() {
removeAll();
List<FancyTab> visibleTabs = getPendingVisibleTabs();
for (FancyTab tab : visibleTabs) {
add(tab, "growy");
}
// Add "more" button if some tabs not visible.
if (visibleTabs.size() < getTabs().size()) {
FancyTabMoreButton more = new FancyTabMoreButton(getTabs());
comboBoxDecorator.decorateIconComboBox(more);
more.setIcon(moreDefaultIcon);
more.setPressedIcon(morePressedIcon);
more.setRolloverIcon(moreRolloverIcon);
more.setSelectedIcon(morePressedIcon);
add(more, "gapleft 0:" + String.valueOf(MIN_TAB_WIDTH));
}
revalidate();
repaint();
}
/**
* Returns the tabs that *should* be visible, based on the currently visible
* tabs, and the currently selected tab. This keeps state and assumes the
* tabs it returns will become visible.
* <p>
* The goal is to shift the minimum amount of distance possible, while
* still keeping the selected tab in view. If there's no selected tab,
* this bumps everything to the left one.
*/
private List<FancyTab> getPendingVisibleTabs() {
// Calculate maximum visible tabs.
maxVisibleTabs = calculateVisibleTabCount();
// Determine tabs to display.
List<FancyTab> tabs = getTabs();
List<FancyTab> vizTabs;
if (maxVisibleTabs >= tabs.size()) {
vizStartIdx = 0;
vizTabs = tabs;
} else {
// Bump the start down from where it previously was
// if there's now more room to display more tabs,
// so that we display as many tabs as possible.
if (tabs.size() - vizStartIdx < maxVisibleTabs) {
vizStartIdx = tabs.size() - maxVisibleTabs;
}
vizTabs = tabs.subList(vizStartIdx, vizStartIdx + maxVisibleTabs);
// If we had a selection, make sure that we shift in the
// appropriate distance to keep that selection in view.
FancyTab selectedTab = getSelectedTab();
if (selectedTab != null && !vizTabs.contains(selectedTab)) {
int selIdx = tabs.indexOf(selectedTab);
if (vizStartIdx > selIdx) { // We have to shift left
vizStartIdx = selIdx;
} else { // We have to shift right
vizStartIdx = selIdx - maxVisibleTabs + 1;
}
vizTabs = tabs.subList(vizStartIdx, vizStartIdx+maxVisibleTabs);
}
}
return vizTabs;
}
/**
* Calculates the number of visible tabs that will fit in the container.
* This is based on the current container width and the minimum tab width.
*/
private int calculateVisibleTabCount() {
int visibleTabCount;
// Calculate available width and maximum visible tabs.
int totalWidth = getSize().width;
int availWidth = Math.max(totalWidth, MIN_TAB_WIDTH);
visibleTabCount = availWidth / MIN_TAB_WIDTH;
// Adjust maximum tabs including "more" button if necessary.
if (visibleTabCount < getTabs().size()) {
int moreWidth = moreDefaultIcon.getIconWidth();
availWidth = Math.max(totalWidth - moreWidth - RIGHT_INSET, MIN_TAB_WIDTH);
visibleTabCount = availWidth / MIN_TAB_WIDTH;
}
return visibleTabCount;
}
/**
* Recreates the tabs in the container using their action maps.
*/
private void recreateTabs() {
List<FancyTab> tabs = getTabs();
List<TabActionMap> actionMaps = new ArrayList<TabActionMap>(tabs.size());
for (FancyTab tab : tabs) {
actionMaps.add(tab.getTabActionMap());
}
setTabActionMaps(actionMaps);
}
/**
* Sets the text for the Close All tabs action.
*/
public void setCloseAllText(String closeAllText) {
getTabProperties().setCloseAllText(closeAllText);
closeAllAction.putValue(Action.NAME, closeAllText);
}
/**
* Sets the text for the Close tab action.
*/
public void setCloseOneText(String closeOneText) {
getTabProperties().setCloseOneText(closeOneText);
}
/**
* Sets the text for the Close All Other tabs action.
*/
public void setCloseOtherText(String closeOtherText) {
getTabProperties().setCloseOtherText(closeOtherText);
closeOtherAction.putValue(Action.NAME, closeOtherText);
}
/**
* Sets whether or not the tabs should render a 'remove' icon.
*/
public void setRemovable(boolean removable) {
getTabProperties().setRemovable(removable);
recreateTabs();
}
/**
* Sets the insets for all tabs.
*/
public void setTabInsets(Insets insets) {
getTabProperties().setInsets(insets);
revalidate();
}
/**
* Action to remove all tabs from the container.
*/
private class CloseAll extends AbstractAction {
public CloseAll() {
super(getTabProperties().getCloseAllText());
}
@Override
public void actionPerformed(ActionEvent e) {
while (!getTabs().isEmpty()) {
getTabs().get(0).remove();
}
}
}
/**
* Action to remove all tabs except the current one from the container.
*/
private class CloseOther extends AbstractAction {
public CloseOther() {
super(getTabProperties().getCloseOtherText());
}
@Override
public void actionPerformed(ActionEvent e) {
while (getTabs().size() > 1) {
FancyTab tab = getTabs().get(0);
if (isFrom(tab, (Component) e.getSource())) {
tab = getTabs().get(1);
}
tab.remove();
}
}
private boolean isFrom(JComponent parent, Component child) {
while (child.getParent() != null) {
child = child.getParent();
if (child instanceof JPopupMenu) {
child = ((JPopupMenu) child).getInvoker();
}
if (child == parent) {
return true;
}
}
return false;
}
}
}