package org.limewire.ui.swing.components;
import java.awt.BorderLayout;
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.JPanel;
import javax.swing.JPopupMenu;
import javax.swing.SwingUtilities;
import net.miginfocom.swing.MigLayout;
import org.jdesktop.animation.timing.Animator;
import org.jdesktop.animation.timing.TimingTargetAdapter;
import org.jdesktop.animation.transitions.EffectsManager;
import org.jdesktop.animation.transitions.ScreenTransition;
import org.jdesktop.animation.transitions.TransitionTarget;
import org.jdesktop.animation.transitions.EffectsManager.TransitionType;
import org.jdesktop.application.Resource;
import org.limewire.ui.swing.animate.EffectsUtils;
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 final JComponent parent;
private final LayoutAnimator animator;
private int maxVisibleTabs;
private int vizStartIdx = -1;
private boolean delayedLayout;
private ChangeType pendingChangeType;
/**
* 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;
// Wrap the tab list in a parent component. (JXLayer also works here.)
// This is needed to work around a bug in the Animated Transitions
// library. To work correctly, the library requires the bounds of the
// animated container relative to its parent to start at location (0, 0).
parent = new JPanel(new BorderLayout());
parent.setOpaque(false);
parent.add(this, BorderLayout.CENTER);
// Create layout animation manager.
animator = new LayoutAnimator(this);
// 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(ChangeType.NONE);
}
}
});
}
/**
* Returns the display component.
*/
public JComponent getComponent() {
return parent;
}
/**
* 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;
}
@Override
protected void layoutTabs() {
// Must be called on UI thread.
assert SwingUtilities.isEventDispatchThread();
// Do layout only if not in progress or delayed.
if (!animator.isRunning() && !delayedLayout) {
List<FancyTab> visibleTabs = getPendingVisibleTabs(false);
doTabLayout(visibleTabs);
revalidate();
repaint();
}
}
@Override
protected void layoutTabs(ChangeType changeType) {
// Must be called on UI thread.
assert SwingUtilities.isEventDispatchThread();
// Skip if layout is delayed.
if (delayedLayout) return;
if (animator.isRunning()) {
// Save change type for later use.
pendingChangeType = changeType;
return;
} else {
// Clear pending change type.
pendingChangeType = null;
// Get old index of first visible tab.
int oldStartIdx = vizStartIdx;
// Get new list of visible tabs.
List<FancyTab> visibleTabs = getPendingVisibleTabs(changeType == ChangeType.ADDED);
// Animate layout when tab added, removed or selected.
if (changeType == ChangeType.ADDED ||
changeType == ChangeType.REMOVED ||
changeType == ChangeType.SELECTED) {
// Set up tab effects and start layout animation.
setupTabEffects(changeType, oldStartIdx, visibleTabs);
animator.start(visibleTabs);
} else {
// Layout tabs without animation.
EffectsManager.clearAllEffects();
doTabLayout(visibleTabs);
revalidate();
repaint();
}
}
}
/**
* Adds the specified list of visible tabs to the layout. This method
* removes all previously visible tabs, and adds the specified tabs to
* the layout.
*/
private void doTabLayout(List<FancyTab> visibleTabs) {
// Remove all tabs.
removeAll();
// Add visible tabs to container.
for (FancyTab tab : visibleTabs) {
add(tab, "growy");
}
// Add "more" button if some tabs not visible.
List<FancyTab> allTabs = getTabs();
if (visibleTabs.size() < allTabs.size()) {
FancyTabMoreButton more = new FancyTabMoreButton(allTabs);
comboBoxDecorator.decorateIconComboBox(more);
more.setIcon(moreDefaultIcon);
more.setPressedIcon(morePressedIcon);
more.setRolloverIcon(moreRolloverIcon);
more.setSelectedIcon(morePressedIcon);
add(more, "gapleft 0:" + String.valueOf(MIN_TAB_WIDTH));
}
}
/**
* Sets up the animation effects when the tab layout changes.
*/
private void setupTabEffects(ChangeType mode, int oldStartIdx, List<FancyTab> visibleTabs) {
// Remove existing effects.
EffectsManager.clearAllEffects();
// Set move-in effects on visible tabs.
for (FancyTab tab : visibleTabs) {
if (vizStartIdx == oldStartIdx) {
// When tab added, tabs slide in from the left.
// When tab removed or selected, tabs slide in from the right.
if (mode == ChangeType.ADDED) {
EffectsManager.setEffect(tab, EffectsUtils.createMoveInEffect(-MIN_TAB_WIDTH, 0, false), TransitionType.APPEARING);
} else {
EffectsManager.setEffect(tab, EffectsUtils.createMoveInEffect(getWidth() - RIGHT_INSET, getHeight() / 2, true), TransitionType.APPEARING);
}
} else if (vizStartIdx < oldStartIdx || oldStartIdx < 0) {
// New tabs slide in from the left.
EffectsManager.setEffect(tab, EffectsUtils.createMoveInEffect(-MIN_TAB_WIDTH, 0, false), TransitionType.APPEARING);
} else {
// New tabs slide in from the right.
EffectsManager.setEffect(tab, EffectsUtils.createMoveInEffect(getWidth() - RIGHT_INSET, getHeight() / 2, true), TransitionType.APPEARING);
}
}
// Set move-out effects on all tabs.
List<FancyTab> allTabs = getTabs();
for (FancyTab tab : allTabs) {
if (vizStartIdx <= oldStartIdx) {
// Old tabs slide out to the right.
EffectsManager.setEffect(tab, EffectsUtils.createMoveOutEffect(getWidth() - RIGHT_INSET, getHeight() / 2, true), TransitionType.DISAPPEARING);
} else {
// Old tabs slide out to the left.
EffectsManager.setEffect(tab, EffectsUtils.createMoveOutEffect(-MIN_TAB_WIDTH, 0, false), TransitionType.DISAPPEARING);
}
}
}
/**
* 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(boolean tabAdded) {
// 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.
// We always select the first tab when a new tab is added.
FancyTab selectedTab = tabAdded ? tabs.get(0) : 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();
if (tabs.size() > 0) {
List<TabActionMap> actionMaps = new ArrayList<TabActionMap>(tabs.size());
for (FancyTab tab : tabs) {
actionMaps.add(tab.getTabActionMap());
}
setTabActionMaps(actionMaps);
}
}
/**
* Handles event when animated layout is completed.
*/
private void layoutDone() {
// Redo layout if layout change is pending.
if (pendingChangeType != null) {
layoutTabs(pendingChangeType);
}
}
/**
* Freezes the current tab layout. This method may be called when we want
* to aggregate multiple tab additions/deletions into a single layout
* update. The <code>updateTabLayout()</code> method should be called to
* unfreeze and update the layout.
*
* @see #updateTabLayout(ChangeType)
*/
public void freezeTabLayout() {
assert SwingUtilities.isEventDispatchThread();
delayedLayout = true;
}
/**
* Updates the tab layout.
*
* @see #freezeTabLayout()
*/
public void updateTabLayout(ChangeType changeType) {
delayedLayout = false;
layoutTabs(changeType);
}
/**
* 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) {
freezeTabLayout();
// Remove all tabs.
while (!getTabs().isEmpty()) {
getTabs().get(0).remove();
}
// Update tab layout.
updateTabLayout(ChangeType.REMOVED);
}
}
/**
* 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) {
freezeTabLayout();
// Remove all other tabs.
while (getTabs().size() > 1) {
FancyTab tab = getTabs().get(0);
if (isFrom(tab, (Component) e.getSource())) {
tab = getTabs().get(1);
}
tab.remove();
}
// Update tab layout.
updateTabLayout(ChangeType.REMOVED);
}
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;
}
}
/**
* Animation manager for the tab layout.
*/
private class LayoutAnimator extends TimingTargetAdapter implements TransitionTarget {
private final Animator animator;
private final ScreenTransition transition;
private List<FancyTab> newVisibleTabs;
private boolean running;
/**
* Constructs a LayoutAnimator for the specified tab container.
*/
public LayoutAnimator(JComponent tabContainer) {
// Set up animation to run for 0.3 seconds at 33 frames per second.
animator = new Animator(300, this);
animator.setResolution(30);
transition = new ScreenTransition(tabContainer, this, animator);
}
/**
* Starts the animated transition.
*/
public void start(List<FancyTab> visibleTabs) {
running = true;
newVisibleTabs = new ArrayList<FancyTab>(visibleTabs);
transition.start();
}
/**
* Returns true if the animated transition is running.
*/
public boolean isRunning() {
return running;
}
@Override
public void setupNextScreen() {
// Add new visible tabs to container.
doTabLayout(newVisibleTabs);
}
@Override
public void end() {
// Reset indicator and notify container.
running = false;
layoutDone();
}
}
}