/*
* This Class was taken from the swing-bug project. https://swing-bug.dev.java.net/
*
* JMultiButton.java
*
* Created on March 23, 2007, 11:52 PM
*
* Copyright 2006-2007 Nigel Hughes
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
* in compliance with the License. You may obtain a copy of the License at http://www.apache.org/
* licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR
* CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language
* governing permissions and limitations under the License.
*/
package org.rr.commons.swing.components.button;
import java.awt.Color;
import java.awt.Dimension;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.Insets;
import java.awt.Point;
import java.awt.Polygon;
import java.awt.Rectangle;
import java.awt.RenderingHints;
import java.awt.event.ActionEvent;
import java.awt.event.MouseEvent;
import java.awt.event.MouseListener;
import java.util.LinkedList;
import javax.swing.AbstractAction;
import javax.swing.Action;
import javax.swing.JButton;
import javax.swing.JPopupMenu;
/**
* A normal JButton which can have multiple actions added to it. When more than one action is added an arrow is added to the right hand side. If the user clicks
* on the left, the action is performed. If the user clicks on the right, a list of all of the actions is displayed.
*
* @author nigel
*/
public class JMultiButton extends JButton implements MouseListener {
private static final long serialVersionUID = -2990339964071073830L;
/**
* A list of all of the actions available on the button.
*/
private LinkedList<Action> actions = new LinkedList<>();
/**
* The action currently selected
*/
private Action currentAction = null;
/**
* Tracks if the mouse is inside the button or not.
*/
private boolean mouseInside = false;
/**
* Filled in by the paint method it records where the line is drawn and enables the fireActionPerformed method to determine if the list of options should be
* displayed or the current action fired
*/
private int boundary = 0;
/**
* True if all the actions are enabled, false if they are not.
*/
private boolean enabled = true;
private Color hoverColor = new Color(60, 60, 190, 164);
/**
* A flag that tells if a click to the button area should also toggle
* the popup menu.
*/
private boolean fullSizePopupActionArea = false;
public boolean isFullSizePopupActionArea() {
return fullSizePopupActionArea;
}
public void setFullSizePopupActionArea(boolean fullSizePopupActionArea) {
this.fullSizePopupActionArea = fullSizePopupActionArea;
}
/**
* Creates a new instance, no actions are added.
*/
public JMultiButton() {
super("GUI Editor Only");
initialize();
}
/**
* Creates a new instance of JComboButton, with the specified action added
*
* @param action The initial action
*/
public JMultiButton(Action action) {
initialize();
addAction(action);
setCurrentAction(action);
}
/**
* Sets the button up ready for use, must be called by any constructor.
*/
protected void initialize() {
setHorizontalAlignment(JButton.LEFT);
addMouseListener(this);
}
/**
* Gets a linked list with all of the current actions in it.
*
* @return A list containing all of the action.
*/
public LinkedList<Action> getActions() {
return (LinkedList<Action>) actions.clone();
}
/**
* Get the color of the rounded rectangle drawn when the mouse is hovering over the arrow.
*
* @return The color
*/
public Color getHoverColor() {
return hoverColor;
}
/**
* Change the color of the rounded rectangle drawn when the mouse is hovering over the arrow.
*
* @param hoverColor
* The chosen color
*/
public void setHoverColor(Color hoverColor) {
this.hoverColor = hoverColor;
}
/**
* Gets the currently active action
*
* @return The currently selected action
*/
public Action getCurrentAction() {
return currentAction;
}
/**
* Adds multiple actions to the button in a single operation
*
* @param actions
* A linked list full of actions
*/
public void setActions(LinkedList<Action> actions) {
this.actions = (LinkedList<Action>) actions.clone();
setCurrentAction(actions.getFirst());
}
/**
* Adds an action to those available for the button.
*
* @param action
* The action to add
*/
public void addAction(Action action) {
actions.add(action);
if (actions.size() == 1) {
setCurrentAction(action);
}
}
/**
* Removes an action from the list of those availble from the button
*
* @param action
* The action to remove
*/
public void removeAction(Action action) {
actions.remove(action);
}
/**
* Sets the specified action to be the current action
*
* @param action
* The action to make current
*/
public void setCurrentAction(Action action) {
if (actions.contains(action)) {
currentAction = action;
super.setAction(currentAction);
currentAction.setEnabled(this.enabled);
}
}
/**
* Overridden so that a check can be made to see if the current action should be fired, or the list of available actions should be shown.
*
* @param actionEvent
* The action event
*/
protected void fireActionPerformed(ActionEvent actionEvent) {
Point p = getMousePosition();
if (p == null) {
return;
}
if (isFullSizePopupActionArea() || p.x > boundary) {
JPopupMenu.setDefaultLightWeightPopupEnabled(false);
JPopupMenu popup = new JMediumWeightPopupMenu();
popup.setLightWeightPopupEnabled(false);
for (Action action : actions) {
popup.add(new ActionOptionWrapper(action));
}
popup.setMinimumSize(new Dimension(getWidth(), getHeight()));
popup.show(this, 0, getHeight());
} else {
super.fireActionPerformed(actionEvent);
}
}
/**
* Enables the button
*
* @param enabled
* True if the button is enabled, false if it isn't
*/
public void setEnabled(boolean enabled) {
this.enabled = enabled;
super.setEnabled(enabled);
}
/**
* Returns true if the button is enabled, false if it isn't
*
* @return See above
*/
public boolean isEnabled() {
return this.enabled;
}
// private final void debugPaintBounds(Graphics graphics) {
// Rectangle inner = getInternalBounds();
// Rectangle arrow = getArrowBounds();
//
// graphics.setColor(Color.RED);
// graphics.drawRect(inner.x, inner.y, inner.x + inner.width, inner.y + inner.height);
// graphics.drawRect(arrow.x, arrow.y, arrow.x + arrow.width, arrow.y + arrow.height);
// }
/**
* Paints the component
*
* @param graphics
* The graphics context
*/
protected void paintComponent(Graphics graphics) {
super.paintComponent(graphics);
// debugPaintBounds(graphics);
Graphics2D g2d = (Graphics2D) graphics;
g2d.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
boundary = getArrowBounds().x;
drawArrow(g2d);
}
/**
* This function can be over-ridden to draw any kind of arrow (with any kind of hover effect) the over-riding class wants.
*
* @param graphics
* The graphics context
*/
protected void drawArrow(Graphics2D graphics) {
Rectangle arrowBounds = getArrowBounds();
int hShift = arrowBounds.width / 4;
int vShift = arrowBounds.height / 3;
if (!isEnabled()) {
graphics.setColor(new Color(0, 0, 0, 128));
} else if (mouseInside) {
graphics.setColor(hoverColor);
graphics.fillRoundRect(arrowBounds.x + 1, arrowBounds.y, arrowBounds.width, arrowBounds.height, hShift, vShift);
graphics.setColor(getForeground());
} else {
graphics.setColor(getForeground());
}
arrowBounds.x += hShift;
arrowBounds.width -= hShift * 2 - 1;
arrowBounds.y += vShift;
arrowBounds.height -= vShift * 2;
Polygon poly = new Polygon(new int[] { arrowBounds.x, arrowBounds.x + arrowBounds.width, arrowBounds.x + arrowBounds.width / 2 }, new int[] {
arrowBounds.y, arrowBounds.y, arrowBounds.y + arrowBounds.height }, 3);
graphics.fillPolygon(poly);
}
/**
* Gets the internal area of the button, useful for determining the draw size of the seperator etc.
*
* @return The internal bounds in component co-ordinates
*/
private Rectangle getInternalBounds() {
Insets insets = getInsets();
Rectangle rect = new Rectangle(insets.left, insets.top, getWidth() - (insets.left + insets.right + 1), getHeight() - (insets.top + insets.bottom + 1));
return rect;
}
/**
* Gets the area (in component co-ordinates) of the selection image used to bring up the choices for the options
*
* @return The area of the arrow bounds box
*/
private Rectangle getArrowBounds() {
Rectangle rect = getInternalBounds();
rect.x = rect.x + (rect.width - rect.height) + 1;
rect.width = rect.height;
return rect;
}
/**
* Returns a preferred size a bit bigger than it needs to be so there is room for the arrow.
*
* @return The minimum width and height of the button
*/
public Dimension getPreferredSize() {
return adjustDimension(super.getPreferredSize(), true);
}
/**
* Utilitity method allowing the upsizing or downsizing of a dimension by the amount requied by the arrow
*
* @param current
* The dimension to adjust
* @param enlarge
* Upsize or downsize
*
* @return The adjusted dimension
*/
protected Dimension adjustDimension(Dimension current, boolean enlarge) {
if (enlarge) {
current.width += getInternalBounds().height + 1;
} else {
current.width -= getInternalBounds().height + 1;
}
return current;
}
/**
* Returns a minimum size a bit bigger than it needs to be so there is room for the arrow.
*
* @return The minimum width and height of the button
*/
public Dimension getMinimumSize() {
return adjustDimension(super.getMinimumSize(), true);
}
/**
* Ignored
*
* @param mouseEvent The mouse event
*/
public void mouseClicked(MouseEvent mouseEvent) {
}
/**
* Ignored
*
* @param mouseEvent The mouse event
*/
public void mousePressed(MouseEvent mouseEvent) {
}
/**
* Ignored
*
* @param mouseEvent The mouse event
*/
public void mouseReleased(MouseEvent mouseEvent) {
}
/**
* Records the mouse is inside the area
*
* @param mouseEvent The mouse event
*/
public void mouseEntered(MouseEvent mouseEvent) {
mouseInside = true;
repaint();
}
/**
* Records the mouse has moved outside
*
* @param mouseEvent The mouse event
*/
public void mouseExited(MouseEvent mouseEvent) {
mouseInside = false;
repaint();
}
/**
* A wrapper for the actions to be dislayed when the pop-up is drawn.
*/
protected class ActionOptionWrapper extends AbstractAction {
private static final long serialVersionUID = -7161990723874074407L;
Action internalAction = null;
/**
* Creates a new instance of a wrapper action
*
* @param wrapAction
* The action to be wrapped
*/
public ActionOptionWrapper(Action wrapAction) {
internalAction = wrapAction;
putValue(Action.NAME, wrapAction.getValue(Action.NAME));
putValue(Action.SMALL_ICON, wrapAction.getValue(Action.SMALL_ICON));
}
/**
* Fired when the action is performed
*
* @param actionEvent
* The action event
*/
public void actionPerformed(ActionEvent actionEvent) {
setCurrentAction(internalAction);
internalAction.actionPerformed(actionEvent);
}
}
}