ArduinoScrollButton.java
/*******************************************************************************
* jArduino: Arduino C++ Code Generation From Java
* Copyright 2020 Tony Washer
*
* 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 net.sourceforge.jarduino.gui;
import java.awt.BorderLayout;
import java.awt.Color;
import java.awt.Component;
import java.awt.Dimension;
import java.awt.Graphics;
import java.awt.Point;
import java.awt.event.FocusEvent;
import java.awt.event.FocusListener;
import java.awt.event.InputEvent;
import java.awt.event.KeyEvent;
import java.awt.event.KeyListener;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
import java.util.ArrayList;
import java.util.List;
import java.util.function.Consumer;
import java.util.function.Function;
import javax.swing.BorderFactory;
import javax.swing.BoxLayout;
import javax.swing.Icon;
import javax.swing.JButton;
import javax.swing.JComponent;
import javax.swing.JDialog;
import javax.swing.JFrame;
import javax.swing.JLabel;
import javax.swing.JPanel;
import javax.swing.JScrollBar;
import javax.swing.JScrollPane;
import javax.swing.JViewport;
import javax.swing.ScrollPaneConstants;
import javax.swing.SwingConstants;
/**
* Button with scrollable popupMenu.
* @param <T> the type of object
*/
public class ArduinoScrollButton<T> {
/**
* Background active colour.
*/
private static final Color COLOR_BACKGROUND = Color.decode("#5F9EA0");
/**
* The Inset size.
*/
private static final int INSET_SIZE = 5;
/**
* Default max items.
*/
private static final int DEFAULT_MAXITEMS = 10;
/**
* Minimum max items.
*/
private static final int MIN_MAXITEMS = 3;
/**
* Default row size.
*/
private static final int DEFAULT_ROWHEIGHT = 16;
/**
* ScrollBar Width.
*/
private static final int SCROLLBAR_WIDTH = 4;
/**
* The frame.
*/
private final JFrame theFrame;
/**
* The button.
*/
private final JButton theButton;
/**
* The items.
*/
private final List<T> theItems;
/**
* The menu.
*/
private final ArduinoScrollMenu theMenu;
/**
* The max # of items.
*/
private int theMaxItems;
/**
* The highlight colour.
*/
private Color theHighlight;
/**
* The formatter.
*/
private Function<T, String> theFormatter;
/**
* The consumer.
*/
private Consumer<T> theConsumer;
/**
* The selected item.
*/
private T theSelectedItem;
/**
* The active item.
*/
private T theActiveItem;
/**
* Constructor.
* @param pFrame the frame
*/
ArduinoScrollButton(final JFrame pFrame) {
/* record parameters */
theFrame = pFrame;
theMaxItems = DEFAULT_MAXITEMS;
theHighlight = COLOR_BACKGROUND;
/* Create items */
theItems = new ArrayList<>();
theMenu = new ArduinoScrollMenu();
/* Configure the button */
theButton = new JButton();
theButton.setIcon(ArduinoArrowIcon.DOWN);
theButton.setHorizontalAlignment(SwingConstants.CENTER);
theButton.setVerticalAlignment(SwingConstants.CENTER);
theButton.setHorizontalTextPosition(SwingConstants.LEFT);
theButton.addActionListener(e -> theMenu.showMenuBelow(theButton));
/* Set the default formatter */
theFormatter = Object::toString;
}
/**
* Obtain the component.
* @return the component
*/
public JComponent getComponent() {
return theButton;
}
/**
* Set the maxItems to display.
* @param pMaxItems the maximum items
*/
public void setMaxDisplayItems(final int pMaxItems) {
theMaxItems = Math.max(MIN_MAXITEMS, pMaxItems);
}
/**
* Obtain the maxItems to display.
* @return the maxItems
*/
public int getMaxDisplayItems() {
return theMaxItems;
}
/**
* Obtain the highlight colour.
* @return the colour
*/
public Color getHighlightColor() {
return theHighlight;
}
/**
* Set the item highlight color.
* @param pColor the color
*/
public void setHighlightColor(final Color pColor) {
theHighlight = pColor;
}
/**
* Add item to menu.
* @param pItem the item to add
*/
public void add(final T pItem) {
if (theItems.isEmpty()) {
setSelectedItem(pItem);
}
theItems.add(pItem);
}
/**
* Remove all items.
*/
public void removeAll() {
theItems.clear();
setSelectedItem(null);
}
/**
* Set the formatter.
* @param pFormatter the formatter
*/
public void setFormatter(final Function<T, String> pFormatter) {
theFormatter = pFormatter;
}
/**
* Set the action consumer.
* @param pConsumer the consumer
*/
public void onSelect(final Consumer<T> pConsumer) {
theConsumer = pConsumer;
}
/**
* Set the selected item.
* @param pItem the item
*/
public void setSelectedItem(final T pItem) {
/* Record the selection */
theSelectedItem = pItem;
theButton.setText(getText(pItem));
/* Pass the item to the consumer */
if (theConsumer != null && pItem != null) {
theConsumer.accept(pItem);
}
/* Close the menu */
closeMenu();
}
/**
* Obtain the selected item.
* @return the item
*/
public T getSelectedItem() {
return theSelectedItem;
}
/**
* Close the menu.
*/
void closeMenu() {
theMenu.closeMenu();
}
/**
* Obtain the text for the item.
* @param pItem the item
* @return the text
*/
String getText(final T pItem) {
return pItem == null ? "Select" : theFormatter.apply(pItem);
}
/**
* scrollable popupMenu.
*/
private class ArduinoScrollMenu {
/**
* The dialog.
*/
private JDialog theDialog;
/**
* Build the menu.
*
* @return the menu
*/
private JPanel buildMenu() {
/* Create the panel */
final JPanel myMenu = new JPanel();
myMenu.setLayout(new BoxLayout(myMenu, BoxLayout.Y_AXIS));
myMenu.setBorder(BorderFactory.createEmptyBorder(INSET_SIZE, INSET_SIZE, INSET_SIZE, INSET_SIZE));
/* Loop through the items */
for (T myItem : theItems) {
/* Create element and add to menu */
final ArduinoScrollableMenuElement myElement = new ArduinoScrollableMenuElement(myItem);
myMenu.add(myElement.getComponent());
}
/* Return the menu */
return myMenu;
}
/**
* Close the menu.
*/
void closeMenu() {
if (isVisible()) {
theDialog.setVisible(false);
theDialog = null;
theActiveItem = null;
}
}
/**
* Is the menu visible?
*
* @return true/false
*/
boolean isVisible() {
return theDialog != null && theDialog.isShowing();
}
/**
* Show the menu below the component.
*
* @param pComponent the component
*/
public void showMenuBelow(final JComponent pComponent) {
/* Ignore if there is no change */
if (isVisible()) {
return;
}
/* Create the dialog */
theDialog = new JDialog(theFrame, false);
theDialog.setUndecorated(true);
/* Add listeners */
final ArduinoScrollListener myListener = new ArduinoScrollListener();
theDialog.addFocusListener(myListener);
theDialog.addKeyListener(myListener);
/* Build the dialog */
buildDialog(pComponent);
/* Set the correct location and display */
final Point myLoc = pComponent.getLocationOnScreen();
theDialog.setLocation(myLoc.x, myLoc.y
+ pComponent.getHeight());
theDialog.setVisible(true);
}
/**
* Build dialog.
* @param pComponent the anchoring component
*/
private void buildDialog(final JComponent pComponent) {
/* Access the number of entries and the scroll count */
final int myCount = theItems.size();
final int myScrollCount = Math.min(theMaxItems, myCount);
/* Build the menu */
JComponent myPanel = buildMenu();
/* If we need to scroll */
if (myScrollCount != myCount) {
/* Create scroll pane and set its size */
final JScrollPane myScroll = new JScrollPane(myPanel);
myScroll.setHorizontalScrollBarPolicy(ScrollPaneConstants.HORIZONTAL_SCROLLBAR_NEVER);
final JViewport myView = myScroll.getViewport();
final Dimension myPrefSize = myView.getPreferredSize();
final int myLen = theMaxItems * DEFAULT_ROWHEIGHT + 1;
myView.setPreferredSize(new Dimension(myPrefSize.width + SCROLLBAR_WIDTH, myLen));
myPanel = myScroll;
/* Adjust the sensitivity of the scroll bar */
final JScrollBar myBar = myScroll.getVerticalScrollBar();
myBar.setUnitIncrement(DEFAULT_ROWHEIGHT);
/* consume mouseWheel events to prevent menu closing */
theDialog.addMouseWheelListener(InputEvent::consume);
}
/* Create a new panel and set a line border */
final JPanel myMain = new JPanel(new BorderLayout());
myMain.add(myPanel, BorderLayout.CENTER);
myMain.setBorder(BorderFactory.createLineBorder(Color.BLACK));
/* Make sure that we are at least the width of the button */
theDialog.getContentPane().add(myMain);
final Dimension myPrefSize = myMain.getPreferredSize();
final int myWidth = Math.max(myPrefSize.width, pComponent.getWidth());
myMain.setPreferredSize(new Dimension(myWidth, myPrefSize.height));
/* Pack the dialog */
theDialog.pack();
}
}
/**
* ScrollListener.
*/
private class ArduinoScrollListener
implements FocusListener, KeyListener {
@Override
public void focusGained(final FocusEvent e) {
/* NoOp */
}
@Override
public void focusLost(final FocusEvent e) {
closeMenu();
}
@Override
public void keyTyped(final KeyEvent e) {
/* NoOp */
}
@Override
public void keyPressed(final KeyEvent e) {
switch (e.getKeyCode()) {
case KeyEvent.VK_ESCAPE:
closeMenu();
break;
case KeyEvent.VK_ENTER:
setSelectedItem(theActiveItem);
break;
default:
break;
}
}
@Override
public void keyReleased(final KeyEvent e) {
/* NoOp */
}
}
/**
* Menu element.
*/
private class ArduinoScrollableMenuElement {
/**
* The label.
*/
private final JLabel theLabel;
/**
* The item.
*/
private final T theItem;
/**
* The base colour.
*/
private Color theBaseColor;
/**
* Constructor.
* @param pItem the Item
*/
ArduinoScrollableMenuElement(final T pItem) {
/* Store the parameters */
theItem = pItem;
/* Create the label */
theLabel = new JLabel();
theLabel.setOpaque(true);
theLabel.setHorizontalAlignment(SwingConstants.LEFT);
theLabel.setText(getText(pItem));
theLabel.setMaximumSize(new Dimension(Integer.MAX_VALUE, DEFAULT_ROWHEIGHT));
/* Listen for mouse events */
theLabel.addMouseListener(new MouseAdapter() {
@Override
public void mouseEntered(final MouseEvent e) {
handleMouseEntered();
setActive(true);
}
@Override
public void mouseExited(final MouseEvent e) {
setActive(false);
}
@Override
public void mouseClicked(final MouseEvent e) {
setActive(false);
handleMouseClicked();
}
});
}
/**
* Obtain the label.
* @return the label
*/
JComponent getComponent() {
return theLabel;
}
/**
* handle mouseEntered.
*/
void handleMouseEntered() {
theActiveItem = theItem;
}
/**
* handle mouseClicked.
*/
void handleMouseClicked() {
setSelectedItem(theItem);
}
/**
* Set the active indication.
* @param pActive true/false
*/
protected void setActive(final boolean pActive) {
if (pActive) {
if (theBaseColor == null) {
theBaseColor = theLabel.getBackground();
theLabel.setBackground(theHighlight);
}
} else if (theBaseColor != null) {
theLabel.setBackground(theBaseColor);
theBaseColor = null;
}
}
}
/**
* Arrow Icons.
*/
private enum ArduinoArrowIcon implements Icon {
/**
* Up Arrow.
*/
UP(new Point(1, 9), new Point(5, 1), new Point(9, 9)),
/**
* Down Arrow.
*/
DOWN(new Point(1, 1), new Point(5, 9), new Point(9, 1)),
/**
* Left Arrow.
*/
LEFT(new Point(1, 5), new Point(9, 1), new Point(9, 9)),
/**
* Right Arrow.
*/
RIGHT(new Point(1, 1), new Point(1, 9), new Point(9, 5));
/**
* The size of the icon.
*/
private static final int ICON_SIZE = 10;
/**
* The number of points.
*/
private final int theNumPoints;
/**
* X locations of points.
*/
private final transient int[] theXPoints;
/**
* Y Locations of points.
*/
private final transient int[] theYPoints;
/**
* Constructor.
* @param pPoints the icon points
*/
ArduinoArrowIcon(final Point... pPoints) {
/* Allocate arrays */
theNumPoints = pPoints.length;
theXPoints = new int[theNumPoints];
theYPoints = new int[theNumPoints];
/* Loop through the points */
for (int i = 0; i < theNumPoints; i++) {
/* Store locations */
theXPoints[i] = pPoints[i].x;
theYPoints[i] = pPoints[i].y;
}
}
@Override
public void paintIcon(final Component c,
final Graphics g,
final int x,
final int y) {
/* Allocate new graphics context */
final Graphics g2 = g.create(x, y, ICON_SIZE, ICON_SIZE);
/* Initialise graphics */
g2.setColor(Color.GRAY);
g2.drawPolygon(theXPoints, theYPoints, theNumPoints);
/* If the component is enabled */
if (c.isEnabled()) {
/* Colour in the polygon */
g2.setColor(Color.BLACK);
g2.fillPolygon(theXPoints, theYPoints, theNumPoints);
}
/* Dispose of the graphics context */
g2.dispose();
}
@Override
public int getIconWidth() {
return ICON_SIZE;
}
@Override
public int getIconHeight() {
return ICON_SIZE;
}
}
}