View Javadoc
1   /*
2    * Tethys: GUI Utilities
3    * Copyright 2012-2026. Tony Washer
4    *
5    * Licensed under the Apache License, Version 2.0 (the "License"); you may not
6    * use this file except in compliance with the License.  You may obtain a copy
7    * 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, WITHOUT
13   * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.  See the
14   * License for the specific language governing permissions and limitations under
15   * the License.
16   */
17  package io.github.tonywasher.joceanus.tethys.swing.menu;
18  
19  import io.github.tonywasher.joceanus.oceanus.event.OceanusEventManager;
20  import io.github.tonywasher.joceanus.oceanus.event.OceanusEventRegistrar;
21  import io.github.tonywasher.joceanus.tethys.api.base.TethysUIArrowIconId;
22  import io.github.tonywasher.joceanus.tethys.api.base.TethysUIConstant;
23  import io.github.tonywasher.joceanus.tethys.api.base.TethysUIEvent;
24  import io.github.tonywasher.joceanus.tethys.api.base.TethysUIIcon;
25  import io.github.tonywasher.joceanus.tethys.api.base.TethysUIIconId;
26  import io.github.tonywasher.joceanus.tethys.api.menu.TethysUIScrollIcon;
27  import io.github.tonywasher.joceanus.tethys.api.menu.TethysUIScrollItem;
28  import io.github.tonywasher.joceanus.tethys.api.menu.TethysUIScrollMenu;
29  import io.github.tonywasher.joceanus.tethys.api.menu.TethysUIScrollToggle;
30  import io.github.tonywasher.joceanus.tethys.core.menu.TethysUICoreScrollMenu;
31  import io.github.tonywasher.joceanus.tethys.swing.base.TethysUISwingArrowIcon;
32  import io.github.tonywasher.joceanus.tethys.swing.base.TethysUISwingIcon;
33  import io.github.tonywasher.joceanus.tethys.swing.base.TethysUISwingUtils;
34  
35  import javax.swing.BorderFactory;
36  import javax.swing.BoxLayout;
37  import javax.swing.JDialog;
38  import javax.swing.JLabel;
39  import javax.swing.JOptionPane;
40  import javax.swing.JPanel;
41  import javax.swing.SwingConstants;
42  import javax.swing.SwingUtilities;
43  import java.awt.BorderLayout;
44  import java.awt.Color;
45  import java.awt.Component;
46  import java.awt.Dimension;
47  import java.awt.Point;
48  import java.awt.Rectangle;
49  import java.awt.event.FocusEvent;
50  import java.awt.event.FocusListener;
51  import java.awt.event.KeyEvent;
52  import java.awt.event.KeyListener;
53  import java.awt.event.MouseAdapter;
54  import java.awt.event.MouseEvent;
55  import java.awt.event.MouseWheelEvent;
56  import java.awt.event.MouseWheelListener;
57  import java.util.ArrayList;
58  import java.util.Iterator;
59  import java.util.List;
60  import java.util.Timer;
61  import java.util.TimerTask;
62  
63  /**
64   * Scroll-able version of ContextMenu.
65   * <p>
66   * Implemented as Stage since ContextMenu does not allow control of individual elements.
67   *
68   * @param <T> the value type
69   */
70  public class TethysUISwingScrollMenu<T>
71          implements TethysUIScrollMenu<T> {
72      /**
73       * Background active colour.
74       */
75      private static final Color COLOR_BACKGROUND = Color.decode("#add8e6");
76  
77      /**
78       * Timer.
79       */
80      private Timer theTimer;
81  
82      /**
83       * List of menu items.
84       */
85      private final List<TethysUISwingScrollElement> theMenuItems;
86  
87      /**
88       * The items panel.
89       */
90      private final JPanel theActiveItems;
91  
92      /**
93       * First item to show in list.
94       */
95      private int theFirstIndex;
96  
97      /**
98       * Max number of items to display in popUp.
99       */
100     private int theMaxDisplayItems;
101 
102     /**
103      * The Event Manager.
104      */
105     private final OceanusEventManager<TethysUIEvent> theEventManager;
106 
107     /**
108      * The ScrollUp Item.
109      */
110     private final ScrollControl theUpItem;
111 
112     /**
113      * The ScrollDown Item.
114      */
115     private final ScrollControl theDownItem;
116 
117     /**
118      * The container panel.
119      */
120     private final JPanel theContainer;
121 
122     /**
123      * The Parent scrollMenu.
124      */
125     private final TethysUISwingScrollSubMenu<T> theParentMenu;
126 
127     /**
128      * The Parent contextMenu.
129      */
130     private final TethysUISwingScrollMenu<T> theParentContext;
131 
132     /**
133      * The dialog.
134      */
135     private JDialog theDialog;
136 
137     /**
138      * The Active subMenu.
139      */
140     private TethysUISwingScrollSubMenu<T> theActiveMenu;
141 
142     /**
143      * The Active item.
144      */
145     private TethysUISwingScrollItem<T> theActiveItem;
146 
147     /**
148      * The selected value.
149      */
150     private TethysUISwingScrollItem<T> theSelectedItem;
151 
152     /**
153      * Do we need to close menu on toggle?
154      */
155     private boolean closeOnToggle;
156 
157     /**
158      * Do we need to reBuild the menu?
159      */
160     private boolean needReBuild;
161 
162     /**
163      * The size of the menu.
164      */
165     private Dimension theMenuSize;
166 
167     /**
168      * Constructor.
169      */
170     TethysUISwingScrollMenu() {
171         this(TethysUICoreScrollMenu.DEFAULT_ITEMCOUNT);
172     }
173 
174     /**
175      * Constructor.
176      *
177      * @param pMaxDisplayItems the maximum number of items to display
178      */
179     TethysUISwingScrollMenu(final int pMaxDisplayItems) {
180         this(null, pMaxDisplayItems);
181     }
182 
183     /**
184      * Constructor.
185      *
186      * @param pParent the parent scroll menu
187      */
188     TethysUISwingScrollMenu(final TethysUISwingScrollSubMenu<T> pParent) {
189         this(pParent, pParent.getContext().getMaxDisplayItems());
190     }
191 
192     /**
193      * Constructor.
194      *
195      * @param pParent          the parent scroll menu
196      * @param pMaxDisplayItems the maximum number of items to display
197      */
198     TethysUISwingScrollMenu(final TethysUISwingScrollSubMenu<T> pParent,
199                             final int pMaxDisplayItems) {
200         /* Check parameters */
201         if (pMaxDisplayItems <= 0) {
202             throw new IllegalArgumentException(TethysUICoreScrollMenu.ERROR_MAXITEMS);
203         }
204 
205         /* Record parameters */
206         theMaxDisplayItems = pMaxDisplayItems;
207         theParentMenu = pParent;
208         theParentContext = theParentMenu == null
209                 ? null
210                 : theParentMenu.getContext();
211 
212         /* Initially need reBuild */
213         needReBuild = true;
214 
215         /* Initially close on toggle */
216         closeOnToggle = true;
217 
218         /* Create event manager */
219         theEventManager = new OceanusEventManager<>();
220 
221         /* Create the scroll items */
222         theUpItem = new ScrollControl(TethysUISwingArrowIcon.getIconForId(TethysUIArrowIconId.UP), -1);
223         theDownItem = new ScrollControl(TethysUISwingArrowIcon.getIconForId(TethysUIArrowIconId.DOWN), 1);
224 
225         /* Allocate the list */
226         theMenuItems = new ArrayList<>();
227         theActiveItems = new JPanel();
228         theActiveItems.setLayout(new BoxLayout(theActiveItems, BoxLayout.Y_AXIS));
229         theActiveItems.setBorder(BorderFactory.createEmptyBorder(2, 2, 2, 2));
230 
231         /* Create the container */
232         theContainer = new JPanel();
233         theContainer.setLayout(new BorderLayout());
234         theContainer.setBorder(BorderFactory.createLineBorder(Color.lightGray));
235     }
236 
237     @Override
238     public OceanusEventRegistrar<TethysUIEvent> getEventRegistrar() {
239         return theEventManager.getEventRegistrar();
240     }
241 
242     @Override
243     public TethysUIScrollItem<T> getSelectedItem() {
244         return theSelectedItem;
245     }
246 
247     /**
248      * Obtain the maximum # of items in the displayed PopUp window.
249      *
250      * @return the # of items
251      */
252     public int getMaxDisplayItems() {
253         return theMaxDisplayItems;
254     }
255 
256     /**
257      * Obtain the timer.
258      *
259      * @return the timer
260      */
261     Timer getTimer() {
262         return theTimer;
263     }
264 
265     /**
266      * Obtain the dialog.
267      *
268      * @return the dialog
269      */
270     JDialog getDialog() {
271         return theDialog;
272     }
273 
274     @Override
275     public void setCloseOnToggle(final boolean pCloseOnToggle) {
276         /* Set value */
277         closeOnToggle = pCloseOnToggle;
278     }
279 
280     @Override
281     public void setMaxDisplayItems(final int pMaxDisplayItems) {
282         /* Check parameters */
283         if (pMaxDisplayItems <= 0) {
284             throw new IllegalArgumentException(TethysUICoreScrollMenu.ERROR_MAXITEMS);
285         }
286 
287         /* Check state */
288         if (theDialog != null
289                 && theDialog.isVisible()) {
290             throw new IllegalStateException();
291         }
292 
293         /* Store parameters */
294         theMaxDisplayItems = pMaxDisplayItems;
295 
296         /* Loop through the children */
297         final Iterator<TethysUISwingScrollElement> myIterator = theMenuItems.iterator();
298         while (myIterator.hasNext()) {
299             final TethysUISwingScrollElement myChild = myIterator.next();
300 
301             /* If this is a subMenu */
302             if (myChild instanceof TethysUISwingScrollSubMenu) {
303                 /* Pass call on */
304                 final TethysUISwingScrollSubMenu<?> mySubMenu = (TethysUISwingScrollSubMenu<?>) myChild;
305                 mySubMenu.setMaxDisplayItems(pMaxDisplayItems);
306             }
307         }
308 
309         /* Request reBuild */
310         needReBuild = true;
311     }
312 
313     /**
314      * Show the menu at position.
315      *
316      * @param pAnchor the anchor node
317      * @param pSide   the side of the anchor node
318      */
319     public void showMenuAtPosition(final Component pAnchor,
320                                    final int pSide) {
321         /* Create the dialog */
322         if (theDialog == null) {
323             createDialog(pAnchor);
324         }
325 
326         /* determine the size of the menu */
327         determineSize();
328 
329         /* If we have elements */
330         if (theMenuSize != null) {
331             /* determine location to display */
332             final Point myLocation = TethysUISwingUtils.obtainDisplayPoint(pAnchor, pSide, theMenuSize);
333 
334             /* Show menu */
335             showMenuAtLocation(myLocation);
336         }
337     }
338 
339     /**
340      * Show the menu at position.
341      *
342      * @param pAnchor the anchor node
343      * @param pSide   the side of the anchor node
344      */
345     public void showMenuAtPosition(final Rectangle pAnchor,
346                                    final int pSide) {
347         /* Create the dialog */
348         if (theDialog == null) {
349             createDialog(null);
350         }
351 
352         /* determine the size of the menu */
353         determineSize();
354 
355         /* If we have elements */
356         if (theMenuSize != null) {
357             /* determine location to display */
358             final Point myLocation = TethysUISwingUtils.obtainDisplayPoint(pAnchor, pSide, theMenuSize);
359 
360             /* Show menu */
361             showMenuAtLocation(myLocation);
362         }
363     }
364 
365     /**
366      * Show the menu at position.
367      *
368      * @param pAnchor the anchor node
369      * @param pX      the relative X position
370      * @param pY      the relative Y position
371      */
372     public void showMenuAtPosition(final Component pAnchor,
373                                    final double pX,
374                                    final double pY) {
375         /* Create the dialog */
376         if (theDialog == null) {
377             createDialog(pAnchor);
378         }
379 
380         /* determine the size of the menu */
381         determineSize();
382 
383         /* If we have elements */
384         if (theMenuSize != null) {
385             /* determine location to display */
386             final Point myRequest = new Point((int) pX, (int) pY);
387             final Point myLocation = TethysUISwingUtils.obtainDisplayPoint(pAnchor, myRequest, theMenuSize);
388 
389             /* Show menu */
390             showMenuAtLocation(myLocation);
391         }
392     }
393 
394     /**
395      * Show the menu at location.
396      *
397      * @param pLocation the location
398      */
399     private void showMenuAtLocation(final Point pLocation) {
400         /* Record position */
401         theDialog.setLocation(pLocation);
402 
403         /* Show menu */
404         showMenu();
405     }
406 
407     /**
408      * Show the menu.
409      */
410     private void showMenu() {
411         /* Clear any timer */
412         if (theParentMenu == null
413                 && theTimer != null) {
414             theTimer.cancel();
415         }
416 
417         /* Initialise the values */
418         theTimer = theParentMenu != null
419                 ? theParentContext.getTimer()
420                 : new Timer();
421         theSelectedItem = null;
422         theActiveMenu = null;
423         theActiveItem = null;
424 
425         /* show the dialog */
426         theDialog.setVisible(true);
427     }
428 
429     /**
430      * CloseOnFocusLoss.
431      */
432     private void closeOnFocusLoss() {
433         /* Pass call on to parent if it exists */
434         if (theParentContext != null) {
435             theParentContext.closeOnFocusLoss();
436         }
437 
438         /* Close the menu */
439         closeMenu();
440     }
441 
442     /**
443      * Close non-Modal.
444      */
445     void closeMenu() {
446         /* Close any children */
447         closeChildren();
448 
449         /* Close the menu */
450         if (theDialog != null) {
451             /* Close and throw away the dialog */
452             theDialog.setVisible(false);
453             theDialog.getContentPane().removeAll();
454             theDialog = null;
455         }
456 
457         /* Clear any timer */
458         if (theParentMenu == null
459                 && theTimer != null) {
460             theTimer.cancel();
461         }
462         theTimer = null;
463     }
464 
465     /**
466      * Create subDialog.
467      *
468      * @param pOwner the owner component
469      */
470     void ensureSubDialog(final TethysUISwingScrollSubMenu<T> pOwner) {
471         /* If dialog does not exist */
472         if (theDialog == null) {
473             /* Create the new dialog */
474             theDialog = new JDialog(pOwner.getDialog(), false);
475             initDialog();
476         }
477     }
478 
479     /**
480      * Create dialog.
481      *
482      * @param pOwner the owner component
483      */
484     private void createDialog(final Component pOwner) {
485         /* Create the new dialog */
486         theDialog = new JDialog(JOptionPane.getFrameForComponent(pOwner), false);
487         initDialog();
488     }
489 
490     /**
491      * Initialise dialog.
492      */
493     private void initDialog() {
494         /* Create the new dialog */
495         theDialog.setUndecorated(true);
496         theDialog.getContentPane().add(theContainer);
497         needReBuild = true;
498 
499         /* Add listeners */
500         final ScrollListener myListener = new ScrollListener();
501         theDialog.addFocusListener(myListener);
502         theDialog.addKeyListener(myListener);
503         theDialog.addMouseWheelListener(myListener);
504     }
505 
506     /**
507      * ScrollListener.
508      */
509     private final class ScrollListener
510             implements FocusListener, KeyListener, MouseWheelListener {
511         @Override
512         public void focusGained(final FocusEvent e) {
513             /* NoOp */
514         }
515 
516         @Override
517         public void focusLost(final FocusEvent e) {
518             handleFocusLost();
519         }
520 
521         @Override
522         public void keyTyped(final KeyEvent e) {
523             /* NoOp */
524         }
525 
526         @Override
527         public void keyPressed(final KeyEvent e) {
528             switch (e.getKeyCode()) {
529                 case KeyEvent.VK_ESCAPE:
530                     handleEscapeKey();
531                     break;
532                 case KeyEvent.VK_ENTER:
533                     handleEnterKey();
534                     break;
535                 default:
536                     break;
537             }
538         }
539 
540         @Override
541         public void keyReleased(final KeyEvent e) {
542             /* NoOp */
543         }
544 
545         @Override
546         public void mouseWheelMoved(final MouseWheelEvent e) {
547             /* Request the scroll */
548             requestScroll(e.getWheelRotation());
549             e.consume();
550         }
551     }
552 
553     /**
554      * Handle focus lost event.
555      */
556     void handleFocusLost() {
557         /* If we've lost focus to other than the active subMenu */
558         if (theActiveMenu == null) {
559             /* fire cancellation event */
560             if (theParentMenu == null) {
561                 theEventManager.fireEvent(TethysUIEvent.WINDOWCLOSED);
562             }
563 
564             /* Close the menu hierarchy if we are currently showing */
565             if (theDialog != null
566                     && theDialog.isShowing()) {
567                 closeOnFocusLoss();
568             }
569         }
570     }
571 
572     /**
573      * Set the selected item.
574      *
575      * @param pItem the selected item
576      */
577     void setSelectedItem(final TethysUISwingScrollItem<T> pItem) {
578         /* If we are a child menu */
579         if (theParentContext != null) {
580             /* pass call on to parent */
581             theParentContext.setSelectedItem(pItem);
582 
583             /* else we are top-level */
584         } else {
585             /* We assume that we will close the menu */
586             boolean doCloseMenu = true;
587 
588             /* record selection */
589             theSelectedItem = pItem;
590             if (theSelectedItem instanceof TethysUIScrollToggle) {
591                 final TethysUIScrollToggle<?> myItem = (TethysUIScrollToggle<?>) theSelectedItem;
592                 myItem.toggleSelected();
593                 doCloseMenu = closeOnToggle;
594             }
595 
596             /* Close the menu if requested */
597             if (doCloseMenu) {
598                 /* Close the menu */
599                 closeMenu();
600             }
601 
602             /* fire selection event */
603             theEventManager.fireEvent(TethysUIEvent.NEWVALUE, theSelectedItem);
604         }
605     }
606 
607     /**
608      * Handle escapeKey.
609      */
610     void handleEscapeKey() {
611         /* If we are a child menu */
612         if (theParentContext != null) {
613             /* pass call on to parent */
614             theParentContext.handleEscapeKey();
615 
616             /* else we are top-level */
617         } else {
618             /* fire cancellation event */
619             theEventManager.fireEvent(TethysUIEvent.WINDOWCLOSED);
620 
621             /* Notify the cancel */
622             closeMenu();
623         }
624     }
625 
626     /**
627      * Handle enterKey.
628      */
629     void handleEnterKey() {
630         /* If we are a child menu */
631         if (theActiveItem != null) {
632             /* assume item is selected */
633             setSelectedItem(theActiveItem);
634         }
635     }
636 
637     /**
638      * Clear active Item.
639      */
640     void clearActiveItem() {
641         theActiveItem = null;
642     }
643 
644     /**
645      * Handle activeItem.
646      *
647      * @param pItem the item
648      */
649     void handleActiveItem(final TethysUISwingScrollItem<T> pItem) {
650         /* Close any children */
651         closeChildren();
652 
653         /* Record that we are the active item */
654         theActiveItem = pItem;
655     }
656 
657     /**
658      * Handle activeMenu.
659      *
660      * @param pMenu the menu
661      */
662     void handleActiveMenu(final TethysUISwingScrollSubMenu<T> pMenu) {
663         /* Reset existing item */
664         theActiveItem = null;
665 
666         /* Hide any existing menu that is not us */
667         if (theActiveMenu != null
668                 && theActiveMenu.getIndex() != pMenu.getIndex()) {
669             theActiveMenu.hideMenu();
670         }
671 
672         /* Record active menu */
673         theActiveMenu = pMenu;
674     }
675 
676     @Override
677     public void removeAllItems() {
678         /* Check state */
679         if (theDialog != null
680                 && theDialog.isVisible()) {
681             theDialog.setVisible(false);
682         }
683 
684         /* Clear menuItems */
685         theMenuItems.clear();
686         theFirstIndex = 0;
687         needReBuild = true;
688 
689         /* Clear state */
690         theSelectedItem = null;
691     }
692 
693     @Override
694     public boolean isEmpty() {
695         /* Obtain count */
696         return theMenuItems.isEmpty();
697     }
698 
699     /**
700      * Obtain count of menu Items.
701      *
702      * @return the count
703      */
704     protected int getItemCount() {
705         /* Obtain count */
706         return theMenuItems.size();
707     }
708 
709     @Override
710     public TethysUIScrollItem<T> addItem(final T pValue) {
711         /* Use standard name */
712         return addItem(pValue, pValue.toString(), null);
713     }
714 
715     @Override
716     public TethysUIScrollItem<T> addItem(final T pValue,
717                                          final String pName) {
718         /* Use standard name */
719         return addItem(pValue, pName, null);
720     }
721 
722     @Override
723     public TethysUIScrollItem<T> addItem(final T pValue,
724                                          final TethysUIIconId pId) {
725         /* Use standard name */
726         return addItem(pValue, pValue.toString(), TethysUISwingUtils.getIconAtSize(pId, TethysUICoreScrollMenu.ICON_SIZE));
727     }
728 
729     @Override
730     public TethysUIScrollItem<T> addItem(final T pValue,
731                                          final TethysUIIcon pGraphic) {
732         /* Use standard name */
733         return addItem(pValue, pValue.toString(), pGraphic);
734     }
735 
736     @Override
737     public TethysUIScrollItem<T> addNullItem(final String pName) {
738         /* Use given name */
739         return addItem(null, pName, null);
740     }
741 
742     @Override
743     public TethysUIScrollItem<T> addNullItem(final String pName,
744                                              final TethysUIIcon pGraphic) {
745         /* Use given name */
746         return addItem(null, pName, pGraphic);
747     }
748 
749     @Override
750     public TethysUIScrollItem<T> addItem(final T pValue,
751                                          final String pName,
752                                          final TethysUIIcon pGraphic) {
753         /* Check state */
754         if (theDialog != null
755                 && theDialog.isVisible()) {
756             throw new IllegalStateException();
757         }
758 
759         /* Create element */
760         final TethysUISwingScrollItem<T> myItem = new TethysUISwingScrollItem<>(this, pValue, pName, pGraphic);
761 
762         /* Add to the list of menuItems */
763         theMenuItems.add(myItem);
764         needReBuild = true;
765         return myItem;
766     }
767 
768     @Override
769     public TethysUIScrollSubMenu<T> addSubMenu(final String pName) {
770         /* Use given name */
771         return addSubMenu(pName, null);
772     }
773 
774     @Override
775     public TethysUIScrollSubMenu<T> addSubMenu(final String pName,
776                                                final TethysUIIcon pGraphic) {
777         /* Check state */
778         if (theDialog != null
779                 && theDialog.isVisible()) {
780             throw new IllegalStateException();
781         }
782 
783         /* Create menu */
784         final TethysUISwingScrollSubMenu<T> myMenu = new TethysUISwingScrollSubMenu<>(this, pName, pGraphic);
785 
786         /* Add to the list of menuItems */
787         theMenuItems.add(myMenu);
788         needReBuild = true;
789         return myMenu;
790     }
791 
792     @Override
793     public TethysUIScrollToggle<T> addToggleItem(final T pValue) {
794         /* Use standard name */
795         return addToggleItem(pValue, pValue.toString());
796     }
797 
798     @Override
799     public TethysUIScrollToggle<T> addToggleItem(final T pValue,
800                                                  final String pName) {
801         /* Check state */
802         if (theDialog != null
803                 && theDialog.isVisible()) {
804             throw new IllegalStateException();
805         }
806 
807         /* Create element */
808         final TethysUISwingScrollToggle<T> myItem = new TethysUISwingScrollToggle<>(this, pValue, pName);
809 
810         /* Add to the list of menuItems */
811         theMenuItems.add(myItem);
812         needReBuild = true;
813         return myItem;
814     }
815 
816     /**
817      * close child menus.
818      */
819     void closeChildren() {
820         /* Close any active subMenu */
821         if (theActiveMenu != null) {
822             theActiveMenu.hideMenu();
823         }
824         theActiveMenu = null;
825     }
826 
827     /**
828      * Ensure item at index will be visible when displayed.
829      *
830      * @param pIndex the index to show
831      */
832     void scrollToIndex(final int pIndex) {
833         /* Show the index */
834         showIndex(pIndex);
835 
836         /* cascade call upwards */
837         if (theParentMenu != null) {
838             theParentMenu.scrollToMenu();
839         }
840     }
841 
842     /**
843      * Ensure index shown.
844      *
845      * @param pIndex the index to show
846      */
847     private void showIndex(final int pIndex) {
848         /* Ignore if index is out of range */
849         final int myCount = theMenuItems.size();
850         if (pIndex < 0
851                 || pIndex >= myCount) {
852             return;
853         }
854 
855         /* If index is above window */
856         if (pIndex < theFirstIndex) {
857             /* Scroll window upwards and return */
858             requestScroll(pIndex - theFirstIndex);
859             return;
860         }
861 
862         /* If index is beyond last visible index */
863         final int myLastIndex = theFirstIndex
864                 + theMaxDisplayItems
865                 - 1;
866         if (myLastIndex < pIndex) {
867             /* Scroll window downwards */
868             requestScroll(pIndex - myLastIndex);
869         }
870     }
871 
872     /**
873      * Determine size of menu.
874      */
875     private void determineSize() {
876         /* NoOp if we do not need to reBuild the menu */
877         if (!needReBuild) {
878             return;
879         }
880 
881         /* If we have items */
882         if (!theMenuItems.isEmpty()) {
883             /* Access the number of entries and the scroll count */
884             final int myCount = theMenuItems.size();
885             final int myScroll = Math.min(theMaxDisplayItems, myCount);
886 
887             /* Remove all items */
888             theActiveItems.removeAll();
889             theContainer.removeAll();
890             theContainer.add(theActiveItems, BorderLayout.CENTER);
891 
892             /* If we do not need to scroll */
893             if (myScroll == myCount) {
894                 /* Loop through the items to add */
895                 for (int i = 0; i < myCount; i++) {
896                     /* Add the items */
897                     theActiveItems.add(theMenuItems.get(i).getPanel());
898                 }
899 
900                 /* Calculate size of menu */
901                 theDialog.pack();
902 
903                 /* Determine the size */
904                 theMenuSize = new Dimension(theDialog.getWidth(), theDialog.getHeight());
905 
906                 /* else need to set up scroll */
907             } else {
908                 /* Add the scrolling items */
909                 theContainer.add(theUpItem.getPanel(), BorderLayout.PAGE_START);
910                 theContainer.add(theDownItem.getPanel(), BorderLayout.PAGE_END);
911                 theUpItem.getPanel().setEnabled(true);
912                 theDownItem.getPanel().setEnabled(true);
913 
914                 /* Add ALL items */
915                 for (final TethysUISwingScrollElement myItem : theMenuItems) {
916                     /* Add the items */
917                     theActiveItems.add(myItem.getPanel());
918                 }
919 
920                 /* Calculate size of menu */
921                 theDialog.pack();
922                 final int myWidth = theDialog.getWidth();
923 
924                 /* Remove all items */
925                 theActiveItems.removeAll();
926 
927                 /* Ensure that the starting index is positive */
928                 if (theFirstIndex < 0) {
929                     theFirstIndex = 0;
930                 }
931 
932                 /* Ensure that the starting point is not too late */
933                 int myMaxIndex = theFirstIndex
934                         + myScroll;
935                 if (myMaxIndex > myCount) {
936                     /* Adjust the first index */
937                     theFirstIndex = myCount
938                             - myScroll;
939                     myMaxIndex = myCount;
940                 }
941 
942                 /* Loop through the items to add */
943                 for (int i = theFirstIndex; i < myMaxIndex; i++) {
944                     /* Add the items */
945                     theActiveItems.add(theMenuItems.get(i).getPanel());
946                 }
947 
948                 /* Calculate size of menu */
949                 theDialog.pack();
950                 final int myHeight = theDialog.getHeight();
951 
952                 /* Set visibility of scroll items */
953                 theUpItem.getPanel().setEnabled(theFirstIndex > 0);
954                 theDownItem.getPanel().setEnabled(myMaxIndex < myCount);
955 
956                 /* Determine the size */
957                 theMenuSize = new Dimension(myWidth, myHeight);
958 
959                 /* Fix the width */
960                 theDialog.setPreferredSize(theMenuSize);
961                 theDialog.pack();
962             }
963 
964             /* else reset the menuSize */
965         } else {
966             theMenuSize = null;
967         }
968 
969         /* Reset flag */
970         needReBuild = false;
971     }
972 
973     /**
974      * ScrollPlusOne.
975      */
976     protected void scrollPlusOne() {
977         /* If we are already built */
978         if (!needReBuild) {
979             /* Access the number of entries */
980             final int myCount = theMenuItems.size();
981 
982             /* Reset the children */
983             closeChildren();
984 
985             /* Ensure Up item is enabled */
986             theUpItem.getPanel().setEnabled(true);
987 
988             /* Remove the first item */
989             theActiveItems.remove(0);
990 
991             /* Add the final item */
992             final int myLast = theFirstIndex + theMaxDisplayItems;
993             final TethysUISwingScrollElement myItem = theMenuItems.get(myLast);
994             myItem.setActive(false);
995             theActiveItems.add(myItem.getPanel());
996 
997             /* Adjust down item */
998             theDownItem.getPanel().setEnabled(myLast + 1 < myCount);
999 
1000             /* Make sure that the menu is sized correctly */
1001             theDialog.pack();
1002         }
1003 
1004         /* Adjust first index */
1005         theFirstIndex++;
1006     }
1007 
1008     /**
1009      * ScrollMinusOne.
1010      */
1011     protected void scrollMinusOne() {
1012         /* If we are already built */
1013         if (!needReBuild) {
1014             /* Reset the children */
1015             closeChildren();
1016 
1017             /* Ensure Down item is enabled */
1018             theDownItem.getPanel().setEnabled(true);
1019 
1020             /* Remove the last item */
1021             theActiveItems.remove(theMaxDisplayItems - 1);
1022 
1023             /* Add the initial item */
1024             final TethysUISwingScrollElement myItem = theMenuItems.get(theFirstIndex - 1);
1025             myItem.setActive(false);
1026             theActiveItems.add(myItem.getPanel(), 0);
1027 
1028             /* Adjust up item */
1029             theUpItem.getPanel().setEnabled(theFirstIndex > 1);
1030 
1031             /* Make sure that the menu is sized correctly */
1032             theDialog.pack();
1033         }
1034 
1035         /* Adjust first index */
1036         theFirstIndex--;
1037     }
1038 
1039     /**
1040      * request scroll.
1041      *
1042      * @param pDelta the delta to scroll.
1043      */
1044     void requestScroll(final int pDelta) {
1045         /* If this is a scroll downwards */
1046         if (pDelta > 0) {
1047             /* If we can scroll downwards */
1048             final int myCount = theMenuItems.size();
1049             final int mySpace = myCount - theFirstIndex - theMaxDisplayItems;
1050             int myScroll = Math.min(mySpace, pDelta);
1051 
1052             /* While we have space */
1053             while (myScroll-- > 0) {
1054                 /* Scroll downwards */
1055                 scrollPlusOne();
1056             }
1057 
1058             /* else scroll upwards if we can */
1059         } else if (theFirstIndex > 0) {
1060             /* Determine space */
1061             int myScroll = Math.min(theFirstIndex, -pDelta);
1062 
1063             /* While we have space */
1064             while (myScroll-- > 0) {
1065                 /* Scroll upwards */
1066                 scrollMinusOne();
1067             }
1068         }
1069     }
1070 
1071     /**
1072      * Scroll item.
1073      */
1074     public abstract static class TethysUISwingScrollElement {
1075         /**
1076          * Default row size.
1077          */
1078         private static final int DEFAULT_ROWHEIGHT = 16;
1079 
1080         /**
1081          * The panel.
1082          */
1083         private final JPanel thePanel;
1084 
1085         /**
1086          * The label.
1087          */
1088         private final JLabel theLabel;
1089 
1090         /**
1091          * The icon label.
1092          */
1093         private final JLabel theIcon;
1094 
1095         /**
1096          * The base colour.
1097          */
1098         private Color theBaseColor;
1099 
1100         /**
1101          * Constructor.
1102          *
1103          * @param pName    the display name
1104          * @param pGraphic the icon for the item
1105          */
1106         private TethysUISwingScrollElement(final String pName,
1107                                            final TethysUIIcon pGraphic) {
1108             /* Create the panel */
1109             thePanel = new JPanel();
1110 
1111             /* Set border layout */
1112             thePanel.setLayout(new BorderLayout());
1113 
1114             /* Create a Label for the name */
1115             theLabel = new JLabel();
1116             theLabel.setHorizontalAlignment(SwingConstants.LEFT);
1117             theLabel.setText(pName);
1118             theLabel.setMaximumSize(new Dimension(Integer.MAX_VALUE, DEFAULT_ROWHEIGHT));
1119 
1120             /* Create a Label for the graphic */
1121             theIcon = new JLabel();
1122             theIcon.setIcon(TethysUISwingIcon.getIcon(pGraphic));
1123             final Dimension myDim = new Dimension(TethysUIConstant.DEFAULT_ICONSIZE, DEFAULT_ROWHEIGHT);
1124             theIcon.setMinimumSize(myDim);
1125             theIcon.setPreferredSize(myDim);
1126             theIcon.setMaximumSize(myDim);
1127 
1128             /* Add the children */
1129             thePanel.add(theIcon, BorderLayout.LINE_START);
1130             thePanel.add(theLabel, BorderLayout.CENTER);
1131         }
1132 
1133         /**
1134          * Constructor.
1135          *
1136          * @param pGraphic the icon for the item
1137          */
1138         private TethysUISwingScrollElement(final TethysUIIcon pGraphic) {
1139             /* Create the panel */
1140             thePanel = new JPanel();
1141 
1142             /* No label required */
1143             theLabel = null;
1144 
1145             /* Create a Label for the graphic */
1146             theIcon = new JLabel();
1147             theIcon.setIcon(TethysUISwingIcon.getIcon(pGraphic));
1148             theIcon.setHorizontalAlignment(SwingConstants.CENTER);
1149 
1150             /* Add the children */
1151             thePanel.add(theIcon);
1152         }
1153 
1154         /**
1155          * Obtain the panel.
1156          *
1157          * @return the panel
1158          */
1159         protected JPanel getPanel() {
1160             return thePanel;
1161         }
1162 
1163         /**
1164          * Obtain the text.
1165          *
1166          * @return the text
1167          */
1168         public String getText() {
1169             return theLabel.getText();
1170         }
1171 
1172         /**
1173          * Set the graphic.
1174          *
1175          * @param pGraphic the graphic
1176          */
1177         protected void setIcon(final TethysUIIcon pGraphic) {
1178             theIcon.setIcon(TethysUISwingIcon.getIcon(pGraphic));
1179         }
1180 
1181         /**
1182          * Set the active indication.
1183          *
1184          * @param pActive true/false
1185          */
1186         protected void setActive(final boolean pActive) {
1187             if (pActive) {
1188                 if (theBaseColor == null) {
1189                     theBaseColor = thePanel.getBackground();
1190                     thePanel.setBackground(COLOR_BACKGROUND);
1191                 }
1192             } else {
1193                 if (theBaseColor != null) {
1194                     thePanel.setBackground(theBaseColor);
1195                     theBaseColor = null;
1196                 }
1197             }
1198         }
1199 
1200         /**
1201          * Add Menu icon.
1202          */
1203         protected void addMenuIcon() {
1204             final JLabel myLabel = new JLabel(TethysUISwingArrowIcon.RIGHT);
1205             thePanel.add(myLabel, BorderLayout.LINE_END);
1206         }
1207     }
1208 
1209     /**
1210      * Scroll item.
1211      *
1212      * @param <T> the value type
1213      */
1214     protected static class TethysUISwingScrollItem<T>
1215             extends TethysUISwingScrollElement
1216             implements TethysUIScrollItem<T> {
1217         /**
1218          * Parent context menu.
1219          */
1220         private final TethysUISwingScrollMenu<T> theContext;
1221 
1222         /**
1223          * The index.
1224          */
1225         private final int theIndex;
1226 
1227         /**
1228          * Associated value.
1229          */
1230         private final T theValue;
1231 
1232         /**
1233          * Constructor.
1234          *
1235          * @param pContext the parent context menu
1236          * @param pValue   the value
1237          * @param pName    the display name
1238          * @param pGraphic the icon for the item
1239          */
1240         protected TethysUISwingScrollItem(final TethysUISwingScrollMenu<T> pContext,
1241                                           final T pValue,
1242                                           final String pName,
1243                                           final TethysUIIcon pGraphic) {
1244             /* Call super-constructor */
1245             super(pName, pGraphic);
1246 
1247             /* Record parameters */
1248             theContext = pContext;
1249             theValue = pValue;
1250 
1251             /* Determine the index */
1252             theIndex = theContext.getItemCount();
1253 
1254             /* Handle removal of subMenus */
1255             getPanel().addMouseListener(new MouseAdapter() {
1256                 @Override
1257                 public void mouseEntered(final MouseEvent e) {
1258                     handleMouseEntered();
1259                     setActive(true);
1260                 }
1261 
1262                 @Override
1263                 public void mouseExited(final MouseEvent e) {
1264                     setActive(false);
1265                 }
1266 
1267                 @Override
1268                 public void mouseClicked(final MouseEvent e) {
1269                     setActive(false);
1270                     handleMouseClicked();
1271                 }
1272             });
1273         }
1274 
1275         /**
1276          * handle mouseClicked.
1277          */
1278         void handleMouseClicked() {
1279             theContext.setSelectedItem(this);
1280         }
1281 
1282         /**
1283          * handle mouseEntered.
1284          */
1285         void handleMouseEntered() {
1286             theContext.handleActiveItem(this);
1287         }
1288 
1289         @Override
1290         public T getValue() {
1291             return theValue;
1292         }
1293 
1294         @Override
1295         public void scrollToItem() {
1296             theContext.scrollToIndex(theIndex);
1297         }
1298     }
1299 
1300     /**
1301      * Scroll item.
1302      *
1303      * @param <T> the value type
1304      */
1305     private static final class TethysUISwingScrollToggle<T>
1306             extends TethysUISwingScrollItem<T>
1307             implements TethysUIScrollToggle<T> {
1308         /**
1309          * Selected state.
1310          */
1311         private boolean isSelected;
1312 
1313         /**
1314          * Constructor.
1315          *
1316          * @param pContext the parent context menu
1317          * @param pValue   the value
1318          * @param pName    the display name
1319          */
1320         TethysUISwingScrollToggle(final TethysUISwingScrollMenu<T> pContext,
1321                                   final T pValue,
1322                                   final String pName) {
1323             /* Call super-constructor */
1324             super(pContext, pValue, pName, null);
1325         }
1326 
1327         @Override
1328         public boolean isSelected() {
1329             return isSelected;
1330         }
1331 
1332         @Override
1333         public void setSelected(final boolean pSelected) {
1334             isSelected = pSelected;
1335             setIcon(isSelected
1336                     ? TethysUISwingUtils.getIconAtSize(TethysUIScrollIcon.CHECKMARK, TethysUIConstant.DEFAULT_ICONSIZE)
1337                     : null);
1338         }
1339 
1340         @Override
1341         public void toggleSelected() {
1342             setSelected(!isSelected);
1343         }
1344     }
1345 
1346     /**
1347      * Scroll menu.
1348      *
1349      * @param <T> the value type
1350      */
1351     public static final class TethysUISwingScrollSubMenu<T>
1352             extends TethysUISwingScrollElement
1353             implements TethysUIScrollSubMenu<T> {
1354         /**
1355          * Parent contextMenu.
1356          */
1357         private final TethysUISwingScrollMenu<T> theContext;
1358 
1359         /**
1360          * The index.
1361          */
1362         private final int theIndex;
1363 
1364         /**
1365          * Associated value.
1366          */
1367         private final TethysUISwingScrollMenu<T> theSubMenu;
1368 
1369         /**
1370          * Constructor.
1371          *
1372          * @param pContext the parent context menu
1373          * @param pName    the name
1374          * @param pGraphic the icon for the menu
1375          */
1376         TethysUISwingScrollSubMenu(final TethysUISwingScrollMenu<T> pContext,
1377                                    final String pName,
1378                                    final TethysUIIcon pGraphic) {
1379             /* Call super-constructor */
1380             super(pName, pGraphic);
1381 
1382             /* Record parameters */
1383             theContext = pContext;
1384 
1385             /* Create the subMenu */
1386             theSubMenu = new TethysUISwingScrollMenu<>(this);
1387 
1388             /* Determine the index */
1389             theIndex = theContext.getItemCount();
1390 
1391             /* Set menu icon */
1392             addMenuIcon();
1393 
1394             /* Handle show menu */
1395             getPanel().addMouseListener(new MouseAdapter() {
1396                 @Override
1397                 public void mouseEntered(final MouseEvent e) {
1398                     handleMouseEntered();
1399                 }
1400 
1401                 @Override
1402                 public void mouseExited(final MouseEvent e) {
1403                     setActive(false);
1404                 }
1405             });
1406         }
1407 
1408         /**
1409          * handle MouseEntered.
1410          */
1411         void handleMouseEntered() {
1412             /* handle the active menu */
1413             theContext.handleActiveMenu(this);
1414             setActive(true);
1415 
1416             /* Show the menu */
1417             theSubMenu.ensureSubDialog(this);
1418             theSubMenu.showMenuAtPosition(getPanel(), SwingConstants.RIGHT);
1419         }
1420 
1421         /**
1422          * Obtain the parent.
1423          *
1424          * @return the parent
1425          */
1426         TethysUISwingScrollMenu<T> getContext() {
1427             return theContext;
1428         }
1429 
1430         @Override
1431         public TethysUISwingScrollMenu<T> getSubMenu() {
1432             return theSubMenu;
1433         }
1434 
1435         /**
1436          * Obtain the index.
1437          *
1438          * @return the index
1439          */
1440         int getIndex() {
1441             return theIndex;
1442         }
1443 
1444         /**
1445          * Hide the subMenu.
1446          */
1447         void hideMenu() {
1448             theSubMenu.closeMenu();
1449         }
1450 
1451         /**
1452          * Obtain the dialog.
1453          *
1454          * @return the dialog
1455          */
1456         private JDialog getDialog() {
1457             return theContext.getDialog();
1458         }
1459 
1460         /**
1461          * Set the number of items in the scrolling portion of the menu.
1462          *
1463          * @param pMaxDisplayItems the maximum number of items to display
1464          * @throws IllegalArgumentException if pMaxDisplayItems is 0 or negative
1465          */
1466         private void setMaxDisplayItems(final int pMaxDisplayItems) {
1467             /* Pass call to subMenu */
1468             theSubMenu.setMaxDisplayItems(pMaxDisplayItems);
1469         }
1470 
1471         /**
1472          * Ensure that this menu is visible immediately the context is displayed.
1473          */
1474         void scrollToMenu() {
1475             theContext.scrollToIndex(theIndex);
1476         }
1477     }
1478 
1479     /**
1480      * Scroll control class.
1481      */
1482     private final class ScrollControl
1483             extends TethysUISwingScrollElement {
1484         /**
1485          * Increment.
1486          */
1487         private final int theIncrement;
1488 
1489         /**
1490          * Timer.
1491          */
1492         private TimerTask theTimerTask;
1493 
1494         /**
1495          * Constructor.
1496          *
1497          * @param pIcon      the icon
1498          * @param pIncrement the increment
1499          */
1500         ScrollControl(final TethysUIIcon pIcon,
1501                       final int pIncrement) {
1502             /* Set the icon for the item */
1503             super(pIcon);
1504 
1505             /* Store parameters */
1506             theIncrement = pIncrement;
1507 
1508             /* Handle show menu */
1509             getPanel().addMouseListener(new MouseAdapter() {
1510                 @Override
1511                 public void mouseClicked(final MouseEvent e) {
1512                     processScroll();
1513                 }
1514 
1515                 @Override
1516                 public void mouseEntered(final MouseEvent e) {
1517                     processMouseEnter();
1518                 }
1519 
1520                 @Override
1521                 public void mouseExited(final MouseEvent e) {
1522                     processMouseExit();
1523                 }
1524             });
1525         }
1526 
1527         /**
1528          * Process scroll event.
1529          */
1530         void processScroll() {
1531             /* Request the scroll */
1532             requestScroll(theIncrement);
1533         }
1534 
1535         /**
1536          * Process mouseEnter event.
1537          */
1538         void processMouseEnter() {
1539             /* cancel any existing task */
1540             if (theTimerTask != null) {
1541                 theTimerTask.cancel();
1542             }
1543 
1544             /* Close any children */
1545             closeChildren();
1546 
1547             /* Set no active Item */
1548             clearActiveItem();
1549             setActive(true);
1550 
1551             /* Create new timer task */
1552             theTimerTask = new TimerTask() {
1553                 @Override
1554                 public void run() {
1555                     SwingUtilities.invokeLater(ScrollControl.this::processScroll);
1556                 }
1557             };
1558 
1559             /* Schedule the task */
1560             getTimer().schedule(theTimerTask, TethysUICoreScrollMenu.INITIAL_SCROLLDELAY, TethysUICoreScrollMenu.REPEAT_SCROLLDELAY);
1561         }
1562 
1563         /**
1564          * Process mouseExit event.
1565          */
1566         void processMouseExit() {
1567             /* If the timer is stopped */
1568             if (theTimerTask != null) {
1569                 theTimerTask.cancel();
1570                 theTimerTask = null;
1571             }
1572             setActive(false);
1573         }
1574     }
1575 }