View Javadoc
1   /*******************************************************************************
2    * jArduino: Arduino C++ Code Generation From Java
3    * Copyright 2020 Tony Washer
4    *
5    * Licensed under the Apache License, Version 2.0 (the "License");
6    * you may not use this file except in compliance with the License.
7    * You may obtain a copy of the License at
8    *
9    *   http://www.apache.org/licenses/LICENSE-2.0
10   *
11   * Unless required by applicable law or agreed to in writing, software
12   * distributed under the License is distributed on an "AS IS" BASIS,
13   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14   * See the License for the specific language governing permissions and
15   * limitations under the License.
16   ******************************************************************************/
17  package net.sourceforge.jarduino.gui;
18  
19  import java.awt.BorderLayout;
20  import java.awt.Color;
21  import java.awt.Component;
22  import java.awt.Dimension;
23  import java.awt.Graphics;
24  import java.awt.Point;
25  import java.awt.event.FocusEvent;
26  import java.awt.event.FocusListener;
27  import java.awt.event.InputEvent;
28  import java.awt.event.KeyEvent;
29  import java.awt.event.KeyListener;
30  import java.awt.event.MouseAdapter;
31  import java.awt.event.MouseEvent;
32  import java.util.ArrayList;
33  import java.util.List;
34  import java.util.function.Consumer;
35  import java.util.function.Function;
36  import javax.swing.BorderFactory;
37  import javax.swing.BoxLayout;
38  import javax.swing.Icon;
39  import javax.swing.JButton;
40  import javax.swing.JComponent;
41  import javax.swing.JDialog;
42  import javax.swing.JFrame;
43  import javax.swing.JLabel;
44  import javax.swing.JPanel;
45  import javax.swing.JScrollBar;
46  import javax.swing.JScrollPane;
47  import javax.swing.JViewport;
48  import javax.swing.ScrollPaneConstants;
49  import javax.swing.SwingConstants;
50  
51  /**
52   * Button with scrollable popupMenu.
53   * @param <T> the type of object
54   */
55  public class ArduinoScrollButton<T> {
56      /**
57       * Background active colour.
58       */
59      private static final Color COLOR_BACKGROUND = Color.decode("#5F9EA0");
60  
61      /**
62       * The Inset size.
63       */
64      private static final int INSET_SIZE = 5;
65  
66      /**
67       * Default max items.
68       */
69      private static final int DEFAULT_MAXITEMS = 10;
70  
71      /**
72       * Minimum max items.
73       */
74      private static final int MIN_MAXITEMS = 3;
75  
76      /**
77       * Default row size.
78       */
79      private static final int DEFAULT_ROWHEIGHT = 16;
80  
81      /**
82       * ScrollBar Width.
83       */
84      private static final int SCROLLBAR_WIDTH = 4;
85  
86      /**
87       * The frame.
88       */
89      private final JFrame theFrame;
90  
91      /**
92       * The button.
93       */
94      private final JButton theButton;
95  
96      /**
97       * The items.
98       */
99      private final List<T> theItems;
100 
101     /**
102      * The menu.
103      */
104     private final ArduinoScrollMenu theMenu;
105 
106     /**
107      * The max # of items.
108      */
109     private int theMaxItems;
110 
111     /**
112      * The highlight colour.
113      */
114     private Color theHighlight;
115 
116     /**
117      * The formatter.
118      */
119     private Function<T, String> theFormatter;
120 
121     /**
122      * The consumer.
123      */
124     private Consumer<T> theConsumer;
125 
126     /**
127      * The selected item.
128      */
129     private T theSelectedItem;
130 
131     /**
132      * The active item.
133      */
134     private T theActiveItem;
135 
136     /**
137      * Constructor.
138      * @param pFrame the frame
139      */
140     ArduinoScrollButton(final JFrame pFrame) {
141         /* record parameters */
142         theFrame = pFrame;
143         theMaxItems = DEFAULT_MAXITEMS;
144         theHighlight = COLOR_BACKGROUND;
145 
146         /* Create items */
147         theItems = new ArrayList<>();
148         theMenu = new ArduinoScrollMenu();
149 
150         /* Configure the button */
151         theButton = new JButton();
152         theButton.setIcon(ArduinoArrowIcon.DOWN);
153         theButton.setHorizontalAlignment(SwingConstants.CENTER);
154         theButton.setVerticalAlignment(SwingConstants.CENTER);
155         theButton.setHorizontalTextPosition(SwingConstants.LEFT);
156         theButton.addActionListener(e -> theMenu.showMenuBelow(theButton));
157 
158         /* Set the default formatter */
159         theFormatter = Object::toString;
160     }
161 
162     /**
163      * Obtain the component.
164      * @return the component
165      */
166     public JComponent getComponent() {
167         return theButton;
168     }
169 
170     /**
171      * Set the maxItems to display.
172      * @param pMaxItems the maximum items
173      */
174     public void setMaxDisplayItems(final int pMaxItems) {
175         theMaxItems = Math.max(MIN_MAXITEMS, pMaxItems);
176     }
177 
178     /**
179      * Obtain the maxItems to display.
180      * @return the maxItems
181      */
182     public int getMaxDisplayItems() {
183         return theMaxItems;
184     }
185 
186     /**
187      * Obtain the highlight colour.
188      * @return the colour
189      */
190     public Color getHighlightColor() {
191         return theHighlight;
192     }
193 
194     /**
195      * Set the item highlight color.
196      * @param pColor the color
197      */
198     public void setHighlightColor(final Color pColor) {
199         theHighlight = pColor;
200     }
201 
202     /**
203      * Add item to menu.
204      * @param pItem the item to add
205      */
206     public void add(final T pItem) {
207         if (theItems.isEmpty()) {
208             setSelectedItem(pItem);
209         }
210         theItems.add(pItem);
211     }
212 
213     /**
214      * Remove all items.
215      */
216     public void removeAll() {
217         theItems.clear();
218         setSelectedItem(null);
219     }
220 
221     /**
222      * Set the formatter.
223      * @param pFormatter the formatter
224      */
225     public void setFormatter(final Function<T, String> pFormatter) {
226         theFormatter = pFormatter;
227     }
228 
229     /**
230      * Set the action consumer.
231      * @param pConsumer the consumer
232      */
233     public void onSelect(final Consumer<T> pConsumer) {
234         theConsumer = pConsumer;
235     }
236 
237     /**
238      * Set the selected item.
239      * @param pItem the item
240      */
241     public void setSelectedItem(final T pItem) {
242         /* Record the selection */
243         theSelectedItem = pItem;
244         theButton.setText(getText(pItem));
245 
246         /* Pass the item to the consumer */
247         if (theConsumer != null && pItem != null) {
248             theConsumer.accept(pItem);
249         }
250 
251         /* Close the menu */
252         closeMenu();
253     }
254 
255     /**
256      * Obtain the selected item.
257      * @return the item
258      */
259     public T getSelectedItem() {
260         return theSelectedItem;
261     }
262 
263     /**
264      * Close the menu.
265      */
266     void closeMenu() {
267         theMenu.closeMenu();
268     }
269 
270     /**
271      * Obtain the text for the item.
272      * @param pItem the item
273      * @return the text
274      */
275     String getText(final T pItem) {
276         return pItem == null ? "Select" : theFormatter.apply(pItem);
277     }
278 
279     /**
280      * scrollable popupMenu.
281      */
282     private class ArduinoScrollMenu {
283         /**
284          * The dialog.
285          */
286         private JDialog theDialog;
287 
288         /**
289          * Build the menu.
290          *
291          * @return the menu
292          */
293         private JPanel buildMenu() {
294             /* Create the panel */
295             final JPanel myMenu = new JPanel();
296             myMenu.setLayout(new BoxLayout(myMenu, BoxLayout.Y_AXIS));
297             myMenu.setBorder(BorderFactory.createEmptyBorder(INSET_SIZE, INSET_SIZE, INSET_SIZE, INSET_SIZE));
298 
299             /* Loop through the items */
300             for (T myItem : theItems) {
301                 /* Create element and add to menu */
302                 final ArduinoScrollableMenuElement myElement = new ArduinoScrollableMenuElement(myItem);
303                 myMenu.add(myElement.getComponent());
304             }
305 
306             /* Return the menu */
307             return myMenu;
308         }
309 
310         /**
311          * Close the menu.
312          */
313         void closeMenu() {
314             if (isVisible()) {
315                 theDialog.setVisible(false);
316                 theDialog = null;
317                 theActiveItem = null;
318             }
319         }
320 
321         /**
322          * Is the menu visible?
323          *
324          * @return true/false
325          */
326         boolean isVisible() {
327             return theDialog != null && theDialog.isShowing();
328         }
329 
330         /**
331          * Show the menu below the component.
332          *
333          * @param pComponent the component
334          */
335         public void showMenuBelow(final JComponent pComponent) {
336             /* Ignore if there is no change */
337             if (isVisible()) {
338                 return;
339             }
340 
341             /* Create the dialog */
342             theDialog = new JDialog(theFrame, false);
343             theDialog.setUndecorated(true);
344 
345             /* Add listeners */
346             final ArduinoScrollListener myListener = new ArduinoScrollListener();
347             theDialog.addFocusListener(myListener);
348             theDialog.addKeyListener(myListener);
349 
350             /* Build the dialog */
351             buildDialog(pComponent);
352 
353             /* Set the correct location and display */
354             final Point myLoc = pComponent.getLocationOnScreen();
355             theDialog.setLocation(myLoc.x, myLoc.y
356                     + pComponent.getHeight());
357             theDialog.setVisible(true);
358         }
359 
360         /**
361          * Build dialog.
362          * @param pComponent the anchoring component
363          */
364         private void buildDialog(final JComponent pComponent) {
365             /* Access the number of entries and the scroll count */
366             final int myCount = theItems.size();
367             final int myScrollCount = Math.min(theMaxItems, myCount);
368 
369             /* Build the menu */
370             JComponent myPanel = buildMenu();
371 
372             /* If we need to scroll */
373             if (myScrollCount != myCount) {
374                 /* Create scroll pane and set its size */
375                 final JScrollPane myScroll = new JScrollPane(myPanel);
376                 myScroll.setHorizontalScrollBarPolicy(ScrollPaneConstants.HORIZONTAL_SCROLLBAR_NEVER);
377                 final JViewport myView = myScroll.getViewport();
378                 final Dimension myPrefSize = myView.getPreferredSize();
379                 final int myLen = theMaxItems * DEFAULT_ROWHEIGHT + 1;
380                 myView.setPreferredSize(new Dimension(myPrefSize.width + SCROLLBAR_WIDTH, myLen));
381                 myPanel = myScroll;
382 
383                 /* Adjust the sensitivity of the scroll bar */
384                 final JScrollBar myBar = myScroll.getVerticalScrollBar();
385                 myBar.setUnitIncrement(DEFAULT_ROWHEIGHT);
386 
387                 /* consume mouseWheel events to prevent menu closing */
388                 theDialog.addMouseWheelListener(InputEvent::consume);
389             }
390 
391             /* Create a new panel and set a line border */
392             final JPanel myMain = new JPanel(new BorderLayout());
393             myMain.add(myPanel, BorderLayout.CENTER);
394             myMain.setBorder(BorderFactory.createLineBorder(Color.BLACK));
395 
396             /* Make sure that we are at least the width of the button */
397             theDialog.getContentPane().add(myMain);
398             final Dimension myPrefSize = myMain.getPreferredSize();
399             final int myWidth = Math.max(myPrefSize.width, pComponent.getWidth());
400             myMain.setPreferredSize(new Dimension(myWidth, myPrefSize.height));
401 
402             /* Pack the dialog */
403             theDialog.pack();
404         }
405     }
406 
407     /**
408      * ScrollListener.
409      */
410     private class ArduinoScrollListener
411             implements FocusListener, KeyListener {
412         @Override
413         public void focusGained(final FocusEvent e) {
414             /* NoOp */
415         }
416 
417         @Override
418         public void focusLost(final FocusEvent e) {
419             closeMenu();
420         }
421 
422         @Override
423         public void keyTyped(final KeyEvent e) {
424             /* NoOp */
425         }
426 
427         @Override
428         public void keyPressed(final KeyEvent e) {
429             switch (e.getKeyCode()) {
430                 case KeyEvent.VK_ESCAPE:
431                     closeMenu();
432                     break;
433                 case KeyEvent.VK_ENTER:
434                     setSelectedItem(theActiveItem);
435                     break;
436                 default:
437                     break;
438             }
439         }
440 
441         @Override
442         public void keyReleased(final KeyEvent e) {
443             /* NoOp */
444         }
445     }
446 
447     /**
448      * Menu element.
449      */
450     private class ArduinoScrollableMenuElement {
451         /**
452          * The label.
453          */
454         private final JLabel theLabel;
455 
456         /**
457          * The item.
458          */
459         private final T theItem;
460 
461         /**
462          * The base colour.
463          */
464         private Color theBaseColor;
465 
466         /**
467          * Constructor.
468          * @param pItem the Item
469          */
470         ArduinoScrollableMenuElement(final T pItem) {
471             /* Store the parameters */
472             theItem = pItem;
473 
474             /* Create the label */
475             theLabel = new JLabel();
476             theLabel.setOpaque(true);
477             theLabel.setHorizontalAlignment(SwingConstants.LEFT);
478             theLabel.setText(getText(pItem));
479             theLabel.setMaximumSize(new Dimension(Integer.MAX_VALUE, DEFAULT_ROWHEIGHT));
480 
481             /* Listen for mouse events */
482             theLabel.addMouseListener(new MouseAdapter() {
483                 @Override
484                 public void mouseEntered(final MouseEvent e) {
485                     handleMouseEntered();
486                     setActive(true);
487                 }
488 
489                 @Override
490                 public void mouseExited(final MouseEvent e) {
491                     setActive(false);
492                 }
493 
494                 @Override
495                 public void mouseClicked(final MouseEvent e) {
496                     setActive(false);
497                     handleMouseClicked();
498                 }
499             });
500         }
501 
502         /**
503          * Obtain the label.
504          * @return the label
505          */
506         JComponent getComponent() {
507             return theLabel;
508         }
509 
510         /**
511          * handle mouseEntered.
512          */
513         void handleMouseEntered() {
514             theActiveItem = theItem;
515         }
516 
517         /**
518          * handle mouseClicked.
519          */
520         void handleMouseClicked() {
521             setSelectedItem(theItem);
522         }
523 
524         /**
525          * Set the active indication.
526          * @param pActive true/false
527          */
528         protected void setActive(final boolean pActive) {
529             if (pActive) {
530                 if (theBaseColor == null) {
531                     theBaseColor = theLabel.getBackground();
532                     theLabel.setBackground(theHighlight);
533                 }
534             } else if (theBaseColor != null) {
535                 theLabel.setBackground(theBaseColor);
536                 theBaseColor = null;
537             }
538         }
539     }
540 
541     /**
542      * Arrow Icons.
543      */
544     private enum ArduinoArrowIcon implements Icon {
545         /**
546          * Up Arrow.
547          */
548         UP(new Point(1, 9), new Point(5, 1), new Point(9, 9)),
549 
550         /**
551          * Down Arrow.
552          */
553         DOWN(new Point(1, 1), new Point(5, 9), new Point(9, 1)),
554 
555         /**
556          * Left Arrow.
557          */
558         LEFT(new Point(1, 5), new Point(9, 1), new Point(9, 9)),
559 
560         /**
561          * Right Arrow.
562          */
563         RIGHT(new Point(1, 1), new Point(1, 9), new Point(9, 5));
564 
565         /**
566          * The size of the icon.
567          */
568         private static final int ICON_SIZE = 10;
569 
570         /**
571          * The number of points.
572          */
573         private final int theNumPoints;
574 
575         /**
576          * X locations of points.
577          */
578         private final transient int[] theXPoints;
579 
580         /**
581          * Y Locations of points.
582          */
583         private final transient int[] theYPoints;
584 
585         /**
586          * Constructor.
587          * @param pPoints the icon points
588          */
589         ArduinoArrowIcon(final Point... pPoints) {
590             /* Allocate arrays */
591             theNumPoints = pPoints.length;
592             theXPoints = new int[theNumPoints];
593             theYPoints = new int[theNumPoints];
594 
595             /* Loop through the points */
596             for (int i = 0; i < theNumPoints; i++) {
597                 /* Store locations */
598                 theXPoints[i] = pPoints[i].x;
599                 theYPoints[i] = pPoints[i].y;
600             }
601         }
602 
603         @Override
604         public void paintIcon(final Component c,
605                               final Graphics g,
606                               final int x,
607                               final int y) {
608             /* Allocate new graphics context */
609             final Graphics g2 = g.create(x, y, ICON_SIZE, ICON_SIZE);
610 
611             /* Initialise graphics */
612             g2.setColor(Color.GRAY);
613             g2.drawPolygon(theXPoints, theYPoints, theNumPoints);
614 
615             /* If the component is enabled */
616             if (c.isEnabled()) {
617                 /* Colour in the polygon */
618                 g2.setColor(Color.BLACK);
619                 g2.fillPolygon(theXPoints, theYPoints, theNumPoints);
620             }
621 
622             /* Dispose of the graphics context */
623             g2.dispose();
624         }
625 
626         @Override
627         public int getIconWidth() {
628             return ICON_SIZE;
629         }
630 
631         @Override
632         public int getIconHeight() {
633             return ICON_SIZE;
634         }
635     }
636 }