package org.japura.gui;
import java.awt.BorderLayout;
import java.awt.Color;
import java.awt.Component;
import java.awt.Dimension;
import java.awt.FontMetrics;
import java.awt.Graphics;
import java.awt.Insets;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.ComponentAdapter;
import java.awt.event.ComponentEvent;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
import java.awt.event.MouseListener;
import java.awt.event.MouseMotionListener;
import java.net.URL;
import java.util.ArrayList;
import java.util.List;
import java.util.UUID;
import java.util.Vector;
import javax.swing.Action;
import javax.swing.BorderFactory;
import javax.swing.DefaultListModel;
import javax.swing.Icon;
import javax.swing.ImageIcon;
import javax.swing.JButton;
import javax.swing.JList;
import javax.swing.JPanel;
import javax.swing.JPopupMenu;
import javax.swing.ListCellRenderer;
import javax.swing.ListSelectionModel;
import javax.swing.SwingConstants;
import javax.swing.SwingUtilities;
import javax.swing.event.ListSelectionEvent;
import javax.swing.event.ListSelectionListener;
import javax.swing.event.PopupMenuEvent;
import javax.swing.event.PopupMenuListener;
import org.japura.gui.renderer.DefaultSplitButtonRenderer;
import org.japura.gui.renderer.SplitButtonRenderer;
/**
* Copyright (C) 2008-2013 Carlos Eduardo Leite de Andrade
* <P>
* This library is free software: you can redistribute it and/or modify it under
* the terms of the GNU Lesser General Public License as published by the Free
* Software Foundation, either version 3 of the License, or (at your option) any
* later version.
* <P>
* This program 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 Lesser General Public License for more
* details.
* <P>
* You should have received a copy of the GNU Lesser General Public License
* along with this program. If not, see <A
* HREF="www.gnu.org/licenses/">www.gnu.org/licenses/</A>
* <P>
* For more information, contact: <A HREF="www.japura.org">www.japura.org</A>
* <P>
*
* @author Carlos Eduardo Leite de Andrade
*/
public class SplitButton extends JButton{
private static final long serialVersionUID = 4L;
public final static Mode BUTTON = Mode.BUTTON;
public final static Mode MENU = Mode.MENU;
private Mode mode;
private int maxLabelWidth;
private int alignment = SwingConstants.CENTER;
private int imageWidth = 13;
private int gap = 5;
private int separatorGap = 7;
private String actualButton;
private boolean fireBlocked;
private boolean buttonChooserVisible;
private boolean mouseIn;
private List<SplitButton.Button> buttons;
private DefaultListModel listModel;
private JPopupMenu buttonsChooser;
private JPanel buttonsRoot;
private JList buttonsList;
private ListRenderer listRenderer;
private SplitButtonRenderer splitButtonRenderer;
private transient MouseListener originalMouseListener;
private transient MouseListener handlerMouseListener;
public SplitButton() {
this(Mode.BUTTON);
}
public SplitButton(Mode mode) {
if (mode != null) {
this.mode = mode;
} else {
this.mode = BUTTON;
}
buttons = new ArrayList<SplitButton.Button>();
splitButtonRenderer = new DefaultSplitButtonRenderer();
URL url = getClass().getResource("/images/jpr_splitbuttondown.png");
Insets margin = getMargin();
margin.right = gap;
margin.left = gap;
setMargin(margin);
super.setHorizontalTextPosition(SwingConstants.LEFT);
super.setHorizontalAlignment(SwingConstants.RIGHT);
super.setIcon(new ImageIcon(url));
setFocusPainted(false);
originalMouseListener = getMouseListeners()[0];
removeMouseListener(originalMouseListener);
addMouseListener(getHandlerMouseListener());
addComponentListener(new ComponentAdapter() {
@Override
public void componentResized(ComponentEvent e) {
reajustTextGap();
}
});
}
private SplitButton.Button get(String name) {
name = ajustName(name);
if (name == null) {
return null;
}
for (SplitButton.Button button : buttons) {
if (button.isSeparator() == false && button.getName().equals(name)) {
return button;
}
}
return null;
}
private DefaultListModel getListModel() {
if (listModel == null) {
listModel = new DefaultListModel();
}
return listModel;
}
public void showButtonsChooser() {
SwingUtilities.invokeLater(new Runnable() {
@Override
public void run() {
if (isEnabled() == false || isShowing() == false) {
return;
}
buttonChooserVisible = true;
fireBlocked = true;
DefaultListModel model = getListModel();
model.removeAllElements();
boolean lastWasSeparator = false;
boolean lastWasButton = false;
for (SplitButton.Button button : buttons) {
if (button.isSeparator() && lastWasButton == true) {
model.addElement(button);
lastWasButton = false;
lastWasSeparator = true;
} else if (button.isVisible() && button.isSeparator() == false) {
model.addElement(button);
lastWasButton = true;
lastWasSeparator = false;
}
}
while (lastWasSeparator) {
model.remove(model.size() - 1);
lastWasSeparator = false;
if (model.size() > 0) {
SplitButton.Button button =
(SplitButton.Button) model.get(model.size() - 1);
if (button.isSeparator()) {
lastWasSeparator = true;
}
}
}
if (model.size() == 0) {
buttonChooserVisible = false;
fireBlocked = false;
return;
}
Dimension dim = getSize();
Dimension bcDim = getButtonsRoot().getPreferredSize();
Insets insets = getButtonsChooser().getInsets();
int width = dim.width;
int height = bcDim.height + insets.bottom + insets.top;
Dimension newDim = new Dimension(width, height);
getButtonsChooser().setPreferredSize(newDim);
getButtonsChooser().show(SplitButton.this, 0, dim.height);
}
});
}
private JPopupMenu getButtonsChooser() {
if (buttonsChooser == null) {
buttonsChooser = new JPopupMenu();
buttonsChooser.setName("buttonsChooser");
buttonsChooser.add(getButtonsRoot());
buttonsChooser.addPopupMenuListener(new PopupMenuListener() {
@Override
public void popupMenuCanceled(PopupMenuEvent e) {}
@Override
public void popupMenuWillBecomeInvisible(PopupMenuEvent e) {
if (mouseIn) {
buttonChooserVisible = true;
} else {
buttonChooserVisible = false;
}
}
@Override
public void popupMenuWillBecomeVisible(PopupMenuEvent e) {}
});
}
return buttonsChooser;
}
private JPanel getButtonsRoot() {
if (buttonsRoot == null) {
buttonsRoot = new JPanel();
buttonsRoot.setLayout(new BorderLayout());
buttonsRoot.setBorder(BorderFactory.createLineBorder(Color.WHITE, 5));
buttonsRoot.add(getButtonsList(), BorderLayout.CENTER);
}
return buttonsRoot;
}
private JList getButtonsList() {
if (buttonsList == null) {
buttonsList = new JList();
listRenderer = new ListRenderer();
buttonsList.setCellRenderer(listRenderer);
buttonsList.setSelectionMode(ListSelectionModel.SINGLE_SELECTION);
buttonsList.setModel(getListModel());
buttonsList.addListSelectionListener(new ListSelectionListener() {
@Override
public void valueChanged(ListSelectionEvent e) {
if (e.getValueIsAdjusting() == false) {
if (buttonsList.getSelectedIndex() > -1) {
Button button = (Button) buttonsList.getSelectedValue();
if (button.isSeparator() == false && !button.isDisabled()) {
setCurrentButton(button.getName());
doClick();
buttonsList.clearSelection();
}
}
}
}
});
buttonsList.addMouseMotionListener(new MouseMotionListener() {
@Override
public void mouseDragged(MouseEvent e) {}
@Override
public void mouseMoved(MouseEvent e) {
listRenderer.mouseOverIndex =
buttonsList.locationToIndex(e.getPoint());
buttonsList.repaint();
}
});
buttonsList.addMouseListener(new MouseAdapter() {
@Override
public void mouseExited(MouseEvent e) {
listRenderer.mouseOverIndex = -1;
buttonsList.repaint();
}
});
}
return buttonsList;
}
private void removeActualListeners() {
if (actualButton != null) {
SplitButton.Button button = get(actualButton);
if (button != null) {
for (ActionListener listener : button.getListeners()) {
super.removeActionListener(listener);
}
}
}
}
public void clearCurrentButton() {
removeActualListeners();
fireBlocked = false;
getButtonsChooser().setVisible(false);
listRenderer.mouseOverIndex = -1;
actualButton = null;
if (mode.equals(Mode.BUTTON)) {
super.setText("");
}
reajustTextGap();
}
public void setCurrentButton(Action action) {
String name = null;
if (action != null) {
name = (String) action.getValue(Action.NAME);
}
setCurrentButton(name);
}
public void setCurrentButton(String name) {
clearCurrentButton();
name = ajustName(name);
SplitButton.Button button = get(name);
if (name != null && button != null && button.isDisabled() == false) {
actualButton = name;
for (ActionListener listener : button.getListeners()) {
super.addActionListener(listener);
}
if (mode.equals(Mode.BUTTON)) {
super.setText(name);
}
}
reajustTextGap();
}
private void reajustTextGap() {
FontMetrics fm = getFontMetrics(getFont());
Dimension dim = null;
if (isShowing()) {
dim = getSize();
} else {
dim = getPreferredSize();
}
Insets insets = getInsets();
int avaiableWidth =
dim.width - insets.left - insets.right - imageWidth - separatorGap - 3
- gap;
int d = gap + 3 + separatorGap;
int width = fm.stringWidth(getText());
if (alignment == SwingConstants.LEFT) {
super.setIconTextGap(avaiableWidth - width + d);
} else if (alignment == SwingConstants.CENTER) {
super.setIconTextGap((avaiableWidth / 2) - ((width) / 2) + d);
} else if (alignment == SwingConstants.RIGHT) {
super.setIconTextGap(gap + 3 + separatorGap);
}
}
@Override
public final void setHorizontalTextPosition(int textPosition) {}
/**
* Sets the horizontal alignment of the text. {@code SplitButton}'s default is
* {@code SwingConstants.CENTER}.
*
* @param alignment
* the alignment value, one of the following values:
* <ul>
* <li>{@code SwingConstants.RIGHT}
* <li>{@code SwingConstants.LEFT}
* <li>{@code SwingConstants.CENTER} (default)
* </ul>
* @throws IllegalArgumentException
* if the alignment is not one of the valid values
*
*/
@Override
public final void setHorizontalAlignment(int alignment) {
if ((alignment == LEFT) || (alignment == CENTER) || (alignment == RIGHT)) {
this.alignment = alignment;
reajustTextGap();
} else {
throw new IllegalArgumentException();
}
}
@Override
public final void setIconTextGap(int iconTextGap) {}
private void calculateMaxLabelWidth() {
maxLabelWidth = 0;
FontMetrics fm = getFontMetrics(getFont());
for (SplitButton.Button button : buttons) {
if (button.isSeparator() == false) {
String name = button.getName();
maxLabelWidth = Math.max(maxLabelWidth, fm.stringWidth(name));
}
}
if (mode.equals(Mode.MENU)) {
maxLabelWidth = Math.max(maxLabelWidth, fm.stringWidth(getText()));
}
}
@Override
public Dimension getMinimumSize() {
if (isMinimumSizeSet()) {
return super.getMinimumSize();
}
return calculatePreferredSize();
}
@Override
public Dimension getPreferredSize() {
if (isPreferredSizeSet()) {
return super.getPreferredSize();
}
return calculatePreferredSize();
}
private Dimension calculatePreferredSize() {
Dimension dim = super.getPreferredSize();
int width = maxLabelWidth;
Insets insets = getInsets();
width += insets.left + insets.right;
width += gap + imageWidth + separatorGap + 3;
dim.width = width;
return dim;
}
@Override
public final void setIcon(Icon defaultIcon) {}
private String ajustName(String name) {
if (name == null) {
return null;
}
return name.trim();
}
public boolean addButton(String name) {
name = ajustName(name);
if (name != null && name.length() > 0 && get(name) == null) {
buttons.add(new Button(name));
calculateMaxLabelWidth();
if (mode.equals(Mode.BUTTON) && actualButton == null) {
setCurrentButton(name);
}
reajustTextGap();
return true;
}
return false;
}
public void addButton(Action action) {
String name = (String) action.getValue(Action.NAME);
if (addButton(name)) {
addActionListener(name, action);
}
}
public void addButtons(List<Action> actions) {
for (Action action : actions) {
addButton(action);
}
}
public void removeSeparators() {
List<SplitButton.Button> list = new ArrayList<SplitButton.Button>();
for (SplitButton.Button button : buttons) {
if (button.isSeparator()) {
list.add(button);
}
}
for (SplitButton.Button button : list) {
buttons.remove(button);
}
}
public void removeButtons() {
clearCurrentButton();
buttons.clear();
calculateMaxLabelWidth();
reajustTextGap();
}
public void removeButton(Action action) {
String name = (String) action.getValue(Action.NAME);
removeButton(name);
}
public void removeButton(String name) {
SplitButton.Button button = get(name);
if (button != null) {
if (actualButton.equals(name)) {
clearCurrentButton();
}
buttons.remove(button);
calculateMaxLabelWidth();
reajustTextGap();
}
}
public void addSeparator() {
buttons.add(new SplitButton.Button());
}
@Override
public final void addActionListener(ActionListener l) {
if (mode.equals(Mode.BUTTON) && actualButton != null) {
addActionListener(actualButton, l);
}
}
public final void addActionListener(String name, ActionListener l) {
SplitButton.Button button = get(name);
if (button != null) {
List<ActionListener> array = button.getListeners();
if (array.contains(l) == false) {
array.add(l);
if (mode.equals(Mode.BUTTON) && actualButton != null
&& actualButton.equals(name)) {
super.addActionListener(l);
}
}
}
}
@Override
public final void removeActionListener(ActionListener l) {
if (mode.equals(Mode.BUTTON) && actualButton != null) {
removeActionListener(actualButton, l);
}
}
public final void removeActionListener(String name, ActionListener l) {
SplitButton.Button button = get(name);
if (button != null) {
List<ActionListener> array = button.getListeners();
array.remove(l);
if (actualButton.equals(name)) {
super.removeActionListener(l);
}
}
}
@Override
public final ActionListener[] getActionListeners() {
return getActionListeners(actualButton);
}
public final ActionListener[] getActionListeners(String name) {
SplitButton.Button button = get(name);
if (button != null) {
List<ActionListener> array = button.getListeners();
return array.toArray(new ActionListener[0]);
}
return new ActionListener[] {};
}
@Override
protected final void fireActionPerformed(ActionEvent event) {
if (fireBlocked == false) {
super.fireActionPerformed(event);
}
}
public Mode getMode() {
return mode;
}
@Override
public final void setText(String text) {
if (mode.equals(Mode.MENU)) {
super.setText(text);
calculateMaxLabelWidth();
}
}
@Override
protected final void paintComponent(Graphics g) {
super.paintComponent(g);
if (mode.equals(Mode.BUTTON)) {
Dimension dim = getSize();
Insets insets = getInsets();
int x = dim.width - insets.right - imageWidth - separatorGap;
int y = 6;
g.setColor(Color.white);
g.fillRect(x, y, 3, dim.height - (2 * y));
g.setColor(Color.gray);
g.drawLine(x + 1, y, x + 1, dim.height - y);
}
}
public void setButtonEnabled(Action action, boolean enabled) {
String name = (String) action.getValue(Action.NAME);
setButtonEnabled(name, enabled);
}
public void setButtonEnabled(String name, boolean enabled) {
SplitButton.Button button = get(name);
if (button != null) {
button.setDisabled(!enabled);
if (enabled) {
if (mode.equals(Mode.BUTTON) && actualButton == null) {
setCurrentButton(name);
}
} else {
if (mode.equals(Mode.BUTTON) && actualButton != null
&& actualButton.equals(name)) {
boolean founded = false;
for (SplitButton.Button otherButton : buttons) {
if (!otherButton.isDisabled()) {
founded = true;
setCurrentButton(otherButton.getName());
break;
}
}
if (founded == false) {
clearCurrentButton();
}
}
}
}
}
public void setButtonVisible(Action action, boolean visible) {
String name = (String) action.getValue(Action.NAME);
setButtonVisible(name, visible);
}
public void setButtonVisible(String name, boolean visible) {
SplitButton.Button button = get(name);
if (button != null) {
button.setVisible(visible);
}
}
public boolean isButtonVisible(Action action) {
String name = (String) action.getValue(Action.NAME);
return isButtonVisible(name);
}
public boolean isButtonVisible(String name) {
SplitButton.Button button = get(name);
if (button != null) {
return button.isVisible();
}
return false;
}
public boolean isButtonEnabled(Action action) {
String name = (String) action.getValue(Action.NAME);
return isButtonEnabled(name);
}
public boolean isButtonEnabled(String name) {
SplitButton.Button button = get(name);
if (button != null) {
return !button.isDisabled();
}
return false;
}
public String getSelectedButton() {
return actualButton;
}
private MouseListener getHandlerMouseListener() {
if (handlerMouseListener == null) {
handlerMouseListener = new MouseListener() {
@Override
public void mouseClicked(MouseEvent e) {
originalMouseListener.mouseClicked(e);
}
@Override
public void mouseEntered(MouseEvent e) {
mouseIn = true;
originalMouseListener.mouseEntered(e);
}
@Override
public void mouseExited(MouseEvent e) {
mouseIn = false;
originalMouseListener.mouseExited(e);
}
@Override
public void mousePressed(MouseEvent e) {
originalMouseListener.mousePressed(e);
}
@Override
public void mouseReleased(MouseEvent e) {
if (mode.equals(Mode.MENU)) {
removeActualListeners();
}
Dimension dim = getSize();
int x = dim.width - getInsets().right - imageWidth - separatorGap;
if (buttonChooserVisible == false && buttons.size() > 0
&& (mode.equals(Mode.MENU) || e.getPoint().x > x)) {
fireBlocked = true;
showButtonsChooser();
} else {
getButtonsChooser().setVisible(false);
listRenderer.mouseOverIndex = -1;
buttonChooserVisible = false;
}
originalMouseListener.mouseReleased(e);
fireBlocked = false;
}
};
}
return handlerMouseListener;
}
public SplitButtonRenderer getRenderer() {
return splitButtonRenderer;
}
public void setRenderer(SplitButtonRenderer renderer) {
if (renderer == null) {
renderer = new DefaultSplitButtonRenderer();
}
this.splitButtonRenderer = renderer;
}
private class ListRenderer implements ListCellRenderer{
public int mouseOverIndex;
@Override
public Component getListCellRendererComponent(JList list, Object value,
int index,
boolean isSelected,
boolean cellHasFocus) {
Button button = (Button) value;
if (mouseOverIndex == index) {
cellHasFocus = true;
} else {
cellHasFocus = false;
}
String name = null;
if (button.isSeparator() == false) {
name = button.getName();
}
return splitButtonRenderer.getCellRendererComponent(name,
button.isSeparator(), cellHasFocus, !button.isDisabled());
}
}
public static enum Mode {
BUTTON,
MENU
}
private static class Button{
private String id;
private boolean separator;
private boolean visible;
private boolean disabled;
private String name;
private List<ActionListener> listeners;
public Button() {
this(null);
separator = true;
}
public Button(String name) {
id = UUID.randomUUID().toString();
listeners = new Vector<ActionListener>();
this.name = name;
this.visible = true;
}
public boolean isSeparator() {
return separator;
}
public boolean isDisabled() {
return disabled;
}
public String getName() {
return name;
}
public boolean isVisible() {
return visible;
}
public List<ActionListener> getListeners() {
return listeners;
}
public void setVisible(boolean visible) {
this.visible = visible;
}
public void setDisabled(boolean disabled) {
this.disabled = disabled;
}
@Override
public int hashCode() {
final int prime = 31;
int result = 1;
result = prime * result + ((id == null) ? 0 : id.hashCode());
return result;
}
@Override
public boolean equals(Object obj) {
if (this == obj)
return true;
if (obj == null)
return false;
if (getClass() != obj.getClass())
return false;
Button other = (Button) obj;
if (id == null) {
if (other.id != null)
return false;
} else if (!id.equals(other.id))
return false;
return true;
}
}
}