/*
* Copyright (c) 2003-2010 Flamingo Kirill Grouchnikov
* and <a href="http://www.topologi.com">Topologi</a>.
* Contributed by <b>Rick Jelliffe</b> of <b>Topologi</b>
* in January 2006. in All Rights Reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* o Redistributions of source code must retain the above copyright notice,
* this list of conditions and the following disclaimer.
*
* o 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.
*
* o Neither the name of Flamingo Kirill Grouchnikov Topologi nor the names of
* its contributors may be used to endorse or promote products derived
* from this software without specific prior written permission.
*
* 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 org.pushingpixels.flamingo.internal.ui.bcb;
import java.awt.*;
import java.awt.event.*;
import java.util.*;
import java.util.List;
import java.util.concurrent.atomic.AtomicInteger;
import javax.swing.*;
import javax.swing.border.EmptyBorder;
import javax.swing.event.ChangeEvent;
import javax.swing.event.ChangeListener;
import javax.swing.plaf.ComponentUI;
import javax.swing.plaf.UIResource;
import org.pushingpixels.flamingo.api.bcb.*;
import org.pushingpixels.flamingo.api.common.*;
import org.pushingpixels.flamingo.api.common.JCommandButton.CommandButtonKind;
import org.pushingpixels.flamingo.api.common.JCommandButton.CommandButtonPopupOrientationKind;
import org.pushingpixels.flamingo.api.common.icon.EmptyResizableIcon;
import org.pushingpixels.flamingo.api.common.icon.ResizableIcon;
import org.pushingpixels.flamingo.api.common.model.PopupButtonModel;
import org.pushingpixels.flamingo.api.common.popup.*;
import org.pushingpixels.flamingo.internal.utils.FlamingoUtilities;
/**
* Basic UI for breadcrumb bar ({@link JBreadcrumbBar}).
*
* @author Topologi
* @author Kirill Grouchnikov
* @author Pawel Hajda
*/
public class BasicBreadcrumbBarUI extends BreadcrumbBarUI {
/**
* The associated breadcrumb bar.
*/
protected JBreadcrumbBar breadcrumbBar;
protected JPanel mainPanel;
protected JScrollablePanel<JPanel> scrollerPanel;
protected ComponentListener componentListener;
protected JCommandButton dummy;
/**
* Contains the item path.
*/
protected LinkedList modelStack;
protected LinkedList<JCommandButton> buttonStack;
protected BreadcrumbPathListener pathListener;
private AtomicInteger atomicCounter;
/*
* (non-Javadoc)
*
* @see javax.swing.plaf.ComponentUI#createUI(javax.swing.JComponent)
*/
public static ComponentUI createUI(JComponent c) {
return new BasicBreadcrumbBarUI();
}
/*
* (non-Javadoc)
*
* @see javax.swing.plaf.ComponentUI#installUI(javax.swing.JComponent)
*/
@Override
public void installUI(JComponent c) {
this.breadcrumbBar = (JBreadcrumbBar) c;
this.modelStack = new LinkedList();
this.buttonStack = new LinkedList<JCommandButton>();
installDefaults(this.breadcrumbBar);
installComponents(this.breadcrumbBar);
installListeners(this.breadcrumbBar);
c.setLayout(createLayoutManager());
if (this.breadcrumbBar.getCallback() != null) {
SwingWorker<List<StringValuePair>, Void> worker = new SwingWorker<List<StringValuePair>, Void>() {
@Override
protected List<StringValuePair> doInBackground()
throws Exception {
return breadcrumbBar.getCallback().getPathChoices(null);
}
@Override
protected void done() {
try {
pushChoices(new BreadcrumbItemChoices(null, get()));
} catch (Exception exc) {
}
}
};
worker.execute();
}
this.dummy = new JCommandButton("Dummy", new EmptyResizableIcon(16));
this.dummy.setDisplayState(CommandButtonDisplayState.MEDIUM);
this.dummy
.setCommandButtonKind(CommandButtonKind.ACTION_AND_POPUP_MAIN_ACTION);
}
/*
* (non-Javadoc)
*
* @see javax.swing.plaf.ComponentUI#uninstallUI(javax.swing.JComponent)
*/
@Override
public void uninstallUI(JComponent c) {
c.setLayout(null);
uninstallListeners((JBreadcrumbBar) c);
uninstallComponents((JBreadcrumbBar) c);
uninstallDefaults((JBreadcrumbBar) c);
this.breadcrumbBar = null;
}
protected void installDefaults(JBreadcrumbBar bar) {
Font currFont = bar.getFont();
if ((currFont == null) || (currFont instanceof UIResource)) {
Font font = FlamingoUtilities.getFont(null, "BreadcrumbBar.font",
"Button.font", "Panel.font");
bar.setFont(font);
}
}
protected void installComponents(JBreadcrumbBar bar) {
this.mainPanel = new JPanel(new FlowLayout(FlowLayout.LEFT, 0, 0));
this.mainPanel.setBorder(new EmptyBorder(0, 0, 0, 0));
this.mainPanel.setOpaque(false);
this.scrollerPanel = new JScrollablePanel<JPanel>(this.mainPanel,
JScrollablePanel.ScrollType.HORIZONTALLY);
bar.add(this.scrollerPanel, BorderLayout.CENTER);
}
protected void installListeners(final JBreadcrumbBar bar) {
this.atomicCounter = new AtomicInteger(0);
this.componentListener = new ComponentAdapter() {
@Override
public void componentResized(ComponentEvent e) {
updateComponents();
}
};
bar.addComponentListener(this.componentListener);
this.pathListener = new BreadcrumbPathListener() {
private SwingWorker<Void, Object> pathChangeWorker;
@Override
public void breadcrumbPathEvent(BreadcrumbPathEvent event) {
final int indexOfFirstChange = event.getIndexOfFirstChange();
if ((this.pathChangeWorker != null)
&& !this.pathChangeWorker.isDone()) {
this.pathChangeWorker.cancel(true);
}
this.pathChangeWorker = new SwingWorker<Void, Object>() {
@Override
protected Void doInBackground() throws Exception {
atomicCounter.incrementAndGet();
synchronized (BasicBreadcrumbBarUI.this) {
// remove stack elements after the first change
if (indexOfFirstChange == 0) {
modelStack.clear();
} else {
int toLeave = indexOfFirstChange * 2 + 1;
while (modelStack.size() > toLeave)
modelStack.removeLast();
}
}
SwingUtilities.invokeLater(new Runnable() {
@Override
public void run() {
updateComponents();
}
});
if (indexOfFirstChange == 0) {
List<StringValuePair> rootChoices = breadcrumbBar
.getCallback().getPathChoices(null);
BreadcrumbItemChoices bic = new BreadcrumbItemChoices(
null, rootChoices);
if (!this.isCancelled()) {
publish(bic);
}
}
List<BreadcrumbItem> items = breadcrumbBar.getModel()
.getItems();
if (items != null) {
for (int itemIndex = indexOfFirstChange; itemIndex < items
.size(); itemIndex++) {
if (this.isCancelled())
break;
BreadcrumbItem item = items.get(itemIndex);
publish(item);
// now check if it has any children
List<BreadcrumbItem> subPath = new ArrayList<BreadcrumbItem>();
for (int j = 0; j <= itemIndex; j++) {
subPath.add(items.get(j));
}
BreadcrumbItemChoices bic = new BreadcrumbItemChoices(
item, breadcrumbBar.getCallback()
.getPathChoices(subPath));
if ((bic.getChoices() != null)
&& (bic.getChoices().length > 0)) {
// add the selector - the current item has
// children
publish(bic);
}
}
}
return null;
}
@Override
protected void process(List<Object> chunks) {
if (chunks != null) {
for (Object chunk : chunks) {
if (this.isCancelled()
|| atomicCounter.get() > 1)
break;
if (chunk instanceof BreadcrumbItemChoices) {
pushChoices((BreadcrumbItemChoices) chunk,
false);
}
if (chunk instanceof BreadcrumbItem) {
pushChoice((BreadcrumbItem) chunk, false);
}
}
}
updateComponents();
}
@Override
protected void done() {
atomicCounter.decrementAndGet();
}
};
pathChangeWorker.execute();
}
};
this.breadcrumbBar.getModel().addPathListener(this.pathListener);
}
protected void uninstallDefaults(JBreadcrumbBar bar) {
}
protected void uninstallComponents(JBreadcrumbBar bar) {
this.mainPanel.removeAll();
this.buttonStack.clear();
bar.remove(this.scrollerPanel);
}
protected void uninstallListeners(JBreadcrumbBar bar) {
bar.removeComponentListener(this.componentListener);
this.componentListener = null;
this.breadcrumbBar.getModel().removePathListener(this.pathListener);
this.pathListener = null;
}
/**
* Invoked by <code>installUI</code> to create a layout manager object to
* manage the {@link JBreadcrumbBar}.
*
* @return a layout manager object
*
* @see BreadcrumbBarLayout
*/
protected LayoutManager createLayoutManager() {
return new BreadcrumbBarLayout();
}
/**
* Layout for the breadcrumb bar.
*
* @author Kirill Grouchnikov
* @author Topologi
*/
protected class BreadcrumbBarLayout implements LayoutManager {
/**
* Creates new layout manager.
*/
public BreadcrumbBarLayout() {
}
/*
* (non-Javadoc)
*
* @see java.awt.LayoutManager#addLayoutComponent(java.lang.String,
* java.awt.Component)
*/
@Override
public void addLayoutComponent(String name, Component c) {
}
/*
* (non-Javadoc)
*
* @see java.awt.LayoutManager#removeLayoutComponent(java.awt.Component)
*/
@Override
public void removeLayoutComponent(Component c) {
}
/*
* (non-Javadoc)
*
* @see java.awt.LayoutManager#preferredLayoutSize(java.awt.Container)
*/
@Override
public Dimension preferredLayoutSize(Container c) {
// The height of breadcrumb bar is
// computed based on the preferred height of a command
// button in MEDIUM state.
int buttonHeight = dummy.getPreferredSize().height;
Insets ins = c.getInsets();
return new Dimension(c.getWidth(), buttonHeight + ins.top
+ ins.bottom);
}
/*
* (non-Javadoc)
*
* @see java.awt.LayoutManager#minimumLayoutSize(java.awt.Container)
*/
@Override
public Dimension minimumLayoutSize(Container c) {
int buttonHeight = dummy.getPreferredSize().height;
return new Dimension(10, buttonHeight);
}
/*
* (non-Javadoc)
*
* @see java.awt.LayoutManager#layoutContainer(java.awt.Container)
*/
@Override
public void layoutContainer(Container c) {
int width = c.getWidth();
int height = c.getHeight();
scrollerPanel.setBounds(0, 0, width, height);
}
}
protected synchronized void updateComponents() {
if (!this.breadcrumbBar.isVisible())
return;
this.mainPanel.removeAll();
buttonStack.clear();
// update the ui
for (int i = 0; i < modelStack.size(); i++) {
Object element = modelStack.get(i);
if (element instanceof BreadcrumbItemChoices) {
BreadcrumbItemChoices bic = (BreadcrumbItemChoices) element;
if (buttonStack.isEmpty()) {
JCommandButton button = new JCommandButton("");
button.setCommandButtonKind(CommandButtonKind.POPUP_ONLY);
configureBreadcrumbButton(button);
configurePopupAction(button, bic);
configurePopupRollover(button);
buttonStack.add(button);
} else {
JCommandButton button = buttonStack.getLast();
int oldW = button.getPreferredSize().width;
button
.setCommandButtonKind(CommandButtonKind.ACTION_AND_POPUP_MAIN_ACTION);
configurePopupAction(button, bic);
configurePopupRollover(button);
}
} else if (element instanceof BreadcrumbItem) {
BreadcrumbItem bi = (BreadcrumbItem) element;
JCommandButton button = new JCommandButton(bi.getKey());
configureBreadcrumbButton(button);
configureMainAction(button, bi);
final Icon icon = bi.getIcon();
if (icon != null) {
button.setIcon(new ResizableIcon() {
int iw = icon.getIconWidth();
int ih = icon.getIconHeight();
@Override
public void paintIcon(Component c, Graphics g, int x,
int y) {
int dx = (iw - icon.getIconWidth()) / 2;
int dy = (ih - icon.getIconHeight()) / 2;
icon.paintIcon(c, g, x + dx, y + dy);
}
@Override
public int getIconWidth() {
return iw;
}
@Override
public int getIconHeight() {
return ih;
}
@Override
public void setDimension(Dimension newDimension) {
iw = newDimension.width;
ih = newDimension.height;
}
});
}
if (i > 0) {
BreadcrumbItemChoices lastBic = (BreadcrumbItemChoices) modelStack
.get(i - 1);
BreadcrumbItem[] choices = lastBic.getChoices();
if (choices != null) {
for (int j = 0; j < choices.length; j++) {
if (bi.getKey().equals(choices[j].getKey())) {
lastBic.setSelectedIndex(j);
break;
}
}
}
}
buttonStack.addLast(button);
}
}
for (JCommandButton jcb : buttonStack) {
this.mainPanel.add(jcb);
}
this.scrollerPanel.revalidate();
this.scrollerPanel.repaint();
SwingUtilities.invokeLater(new Runnable() {
@Override
public void run() {
// scroll to the last element in the breadcrumb bar
scrollerPanel.scrollToIfNecessary(
mainPanel.getPreferredSize().width, 0);
scrollerPanel.repaint();
}
});
}
private void configureMainAction(JCommandButton button,
final BreadcrumbItem bi) {
button.getActionModel().addActionListener(new ActionListener() {
@Override
public void actionPerformed(ActionEvent e) {
SwingUtilities.invokeLater(new Runnable() {
@Override
public void run() {
BreadcrumbBarModel barModel = breadcrumbBar.getModel();
int itemIndex = barModel.indexOf(bi);
int toLeave = (itemIndex < 0) ? 0 : itemIndex + 1;
barModel.setCumulative(true);
while (barModel.getItemCount() > toLeave) {
barModel.removeLast();
}
barModel.setCumulative(false);
}
});
}
});
}
private void configurePopupAction(JCommandButton button,
final BreadcrumbItemChoices bic) {
button.setPopupCallback(new PopupPanelCallback() {
@Override
public JPopupPanel getPopupPanel(JCommandButton commandButton) {
JCommandPopupMenu popup = new JCommandPopupMenu();
for (int i = 0; i < bic.getChoices().length; i++) {
final BreadcrumbItem bi = bic.getChoices()[i];
JCommandMenuButton menuButton = new JCommandMenuButton(bi
.getKey(), null);
final Icon icon = bi.getIcon();
if (icon != null) {
menuButton.setIcon(new ResizableIcon() {
int iw = icon.getIconWidth();
int ih = icon.getIconHeight();
@Override
public void paintIcon(Component c, Graphics g,
int x, int y) {
int dx = (iw - icon.getIconWidth()) / 2;
int dy = (ih - icon.getIconHeight()) / 2;
icon.paintIcon(c, g, x + dx, y + dy);
}
@Override
public int getIconWidth() {
return iw;
}
@Override
public int getIconHeight() {
return ih;
}
@Override
public void setDimension(Dimension newDimension) {
iw = newDimension.width;
ih = newDimension.height;
}
});
}
if (i == bic.getSelectedIndex()) {
menuButton.setFont(menuButton.getFont().deriveFont(
Font.BOLD));
}
final int biIndex = i;
menuButton.getActionModel().addActionListener(
new ActionListener() {
@Override
public void actionPerformed(ActionEvent e) {
SwingUtilities.invokeLater(new Runnable() {
@Override
public void run() {
if (bi == null)
return;
BreadcrumbBarModel barModel = breadcrumbBar
.getModel();
barModel.setCumulative(true);
int itemIndex = barModel
.indexOf(bic.getAncestor());
int toLeave = ((bic.getAncestor() == null) || (itemIndex < 0)) ? 0
: itemIndex + 1;
while (barModel.getItemCount() > toLeave) {
barModel.removeLast();
}
barModel.addLast(bi);
bic.setSelectedIndex(biIndex);
barModel.setCumulative(false);
}
});
}
});
popup.addMenuButton(menuButton);
menuButton.setCursor(Cursor
.getPredefinedCursor(Cursor.HAND_CURSOR));
}
popup.setMaxVisibleMenuButtons(10);
return popup;
}
});
}
private void configurePopupRollover(final JCommandButton button) {
button.getPopupModel().addChangeListener(new ChangeListener() {
boolean rollover = button.getPopupModel().isRollover();
@Override
public void stateChanged(ChangeEvent e) {
SwingUtilities.invokeLater(new Runnable() {
@Override
public void run() {
boolean isRollover = button.getPopupModel()
.isRollover();
if (isRollover == rollover)
return;
if (isRollover) {
// does any *other* button show popup?
for (JCommandButton bcbButton : buttonStack) {
if (bcbButton == button)
continue;
if (bcbButton.getPopupModel().isPopupShowing()) {
// scroll to view
scrollerPanel.scrollToIfNecessary(button
.getBounds().x, button.getWidth());
// simulate click on the popup area
// of *this* button
button.doPopupClick();
}
}
}
rollover = isRollover;
}
});
}
});
}
private void configureBreadcrumbButton(final JCommandButton button) {
button.setDisplayState(CommandButtonDisplayState.MEDIUM);
button
.setPopupOrientationKind(CommandButtonPopupOrientationKind.SIDEWARD);
button.setHGapScaleFactor(0.75);
button.getPopupModel().addChangeListener(new ChangeListener() {
@Override
public void stateChanged(ChangeEvent e) {
PopupButtonModel model = button.getPopupModel();
boolean displayDownwards = model.isRollover()
|| model.isPopupShowing();
CommandButtonPopupOrientationKind popupOrientationKind = displayDownwards ? CommandButtonPopupOrientationKind.DOWNWARD
: CommandButtonPopupOrientationKind.SIDEWARD;
button.setPopupOrientationKind(popupOrientationKind);
}
});
}
/**
* Pushes a choice to the top position of the stack. If the current top is
* already a {@link BreadcrumbItemChoices}, replace it.
*
* @param bic
* The choice item to push.
* @return The item that has been pushed.
*/
protected Object pushChoices(BreadcrumbItemChoices bic) {
return pushChoices(bic, true);
}
/**
* Pushes a choice to the top position of the stack. If the current top is
* already a {@link BreadcrumbItemChoices}, replace it.
*
* @param bic
* The choice item to push.
* @param toUpdateUI
* Indication whether the bar should be repainted.
* @return The item that has been pushed.
*/
protected synchronized Object pushChoices(BreadcrumbItemChoices bic,
boolean toUpdateUI) {
if (bic == null)
return null;
if (modelStack.size() % 2 == 1) {
modelStack.pop();
}
modelStack.addLast(bic);
if (toUpdateUI) {
updateComponents();
}
return bic;
}
/**
* Pushes an item to the top position of the stack. If the current top is
* already a {@link BreadcrumbItemChoices}, replace it.
*
* @param bi
* The item to push.
* @param toUpdateUI
* Indication whether the bar should be repainted.
* @return The item that has been pushed.
*/
protected synchronized Object pushChoice(BreadcrumbItem bi,
boolean toUpdateUI) {
assert (bi != null);
Object result;
// synchronized (stack) {
if (!modelStack.isEmpty() && modelStack.size() % 2 == 0) {
modelStack.pop();
}
bi.setIndex(modelStack.size());
modelStack.addLast(bi);
// }
return bi;
}
}