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.menu.TethysUIScrollIcon;
26  import io.github.tonywasher.joceanus.tethys.api.menu.TethysUIScrollItem;
27  import io.github.tonywasher.joceanus.tethys.api.menu.TethysUIScrollMenu;
28  import io.github.tonywasher.joceanus.tethys.api.menu.TethysUIScrollSubMenu;
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 TethysUIIcon pGraphic) {
725         /* Use standard name */
726         return addItem(pValue, pValue.toString(), pGraphic);
727     }
728 
729     @Override
730     public TethysUIScrollItem<T> addNullItem(final String pName) {
731         /* Use given name */
732         return addItem(null, pName, null);
733     }
734 
735     @Override
736     public TethysUIScrollItem<T> addNullItem(final String pName,
737                                              final TethysUIIcon pGraphic) {
738         /* Use given name */
739         return addItem(null, pName, pGraphic);
740     }
741 
742     @Override
743     public TethysUIScrollItem<T> addItem(final T pValue,
744                                          final String pName,
745                                          final TethysUIIcon pGraphic) {
746         /* Check state */
747         if (theDialog != null
748                 && theDialog.isVisible()) {
749             throw new IllegalStateException();
750         }
751 
752         /* Create element */
753         final TethysUISwingScrollItem<T> myItem = new TethysUISwingScrollItem<>(this, pValue, pName, pGraphic);
754 
755         /* Add to the list of menuItems */
756         theMenuItems.add(myItem);
757         needReBuild = true;
758         return myItem;
759     }
760 
761     @Override
762     public TethysUIScrollSubMenu<T> addSubMenu(final String pName) {
763         /* Use given name */
764         return addSubMenu(pName, null);
765     }
766 
767     @Override
768     public TethysUIScrollSubMenu<T> addSubMenu(final String pName,
769                                                final TethysUIIcon pGraphic) {
770         /* Check state */
771         if (theDialog != null
772                 && theDialog.isVisible()) {
773             throw new IllegalStateException();
774         }
775 
776         /* Create menu */
777         final TethysUISwingScrollSubMenu<T> myMenu = new TethysUISwingScrollSubMenu<>(this, pName, pGraphic);
778 
779         /* Add to the list of menuItems */
780         theMenuItems.add(myMenu);
781         needReBuild = true;
782         return myMenu;
783     }
784 
785     @Override
786     public TethysUIScrollToggle<T> addToggleItem(final T pValue) {
787         /* Use standard name */
788         return addToggleItem(pValue, pValue.toString());
789     }
790 
791     @Override
792     public TethysUIScrollToggle<T> addToggleItem(final T pValue,
793                                                  final String pName) {
794         /* Check state */
795         if (theDialog != null
796                 && theDialog.isVisible()) {
797             throw new IllegalStateException();
798         }
799 
800         /* Create element */
801         final TethysUISwingScrollToggle<T> myItem = new TethysUISwingScrollToggle<>(this, pValue, pName);
802 
803         /* Add to the list of menuItems */
804         theMenuItems.add(myItem);
805         needReBuild = true;
806         return myItem;
807     }
808 
809     /**
810      * close child menus.
811      */
812     void closeChildren() {
813         /* Close any active subMenu */
814         if (theActiveMenu != null) {
815             theActiveMenu.hideMenu();
816         }
817         theActiveMenu = null;
818     }
819 
820     /**
821      * Ensure item at index will be visible when displayed.
822      *
823      * @param pIndex the index to show
824      */
825     void scrollToIndex(final int pIndex) {
826         /* Show the index */
827         showIndex(pIndex);
828 
829         /* cascade call upwards */
830         if (theParentMenu != null) {
831             theParentMenu.scrollToMenu();
832         }
833     }
834 
835     /**
836      * Ensure index shown.
837      *
838      * @param pIndex the index to show
839      */
840     private void showIndex(final int pIndex) {
841         /* Ignore if index is out of range */
842         final int myCount = theMenuItems.size();
843         if (pIndex < 0
844                 || pIndex >= myCount) {
845             return;
846         }
847 
848         /* If index is above window */
849         if (pIndex < theFirstIndex) {
850             /* Scroll window upwards and return */
851             requestScroll(pIndex - theFirstIndex);
852             return;
853         }
854 
855         /* If index is beyond last visible index */
856         final int myLastIndex = theFirstIndex
857                 + theMaxDisplayItems
858                 - 1;
859         if (myLastIndex < pIndex) {
860             /* Scroll window downwards */
861             requestScroll(pIndex - myLastIndex);
862         }
863     }
864 
865     /**
866      * Determine size of menu.
867      */
868     private void determineSize() {
869         /* NoOp if we do not need to reBuild the menu */
870         if (!needReBuild) {
871             return;
872         }
873 
874         /* If we have items */
875         if (!theMenuItems.isEmpty()) {
876             /* Access the number of entries and the scroll count */
877             final int myCount = theMenuItems.size();
878             final int myScroll = Math.min(theMaxDisplayItems, myCount);
879 
880             /* Remove all items */
881             theActiveItems.removeAll();
882             theContainer.removeAll();
883             theContainer.add(theActiveItems, BorderLayout.CENTER);
884 
885             /* If we do not need to scroll */
886             if (myScroll == myCount) {
887                 /* Loop through the items to add */
888                 for (int i = 0; i < myCount; i++) {
889                     /* Add the items */
890                     theActiveItems.add(theMenuItems.get(i).getPanel());
891                 }
892 
893                 /* Calculate size of menu */
894                 theDialog.pack();
895 
896                 /* Determine the size */
897                 theMenuSize = new Dimension(theDialog.getWidth(), theDialog.getHeight());
898 
899                 /* else need to set up scroll */
900             } else {
901                 /* Add the scrolling items */
902                 theContainer.add(theUpItem.getPanel(), BorderLayout.PAGE_START);
903                 theContainer.add(theDownItem.getPanel(), BorderLayout.PAGE_END);
904                 theUpItem.getPanel().setEnabled(true);
905                 theDownItem.getPanel().setEnabled(true);
906 
907                 /* Add ALL items */
908                 for (final TethysUISwingScrollElement myItem : theMenuItems) {
909                     /* Add the items */
910                     theActiveItems.add(myItem.getPanel());
911                 }
912 
913                 /* Calculate size of menu */
914                 theDialog.pack();
915                 final int myWidth = theDialog.getWidth();
916 
917                 /* Remove all items */
918                 theActiveItems.removeAll();
919 
920                 /* Ensure that the starting index is positive */
921                 if (theFirstIndex < 0) {
922                     theFirstIndex = 0;
923                 }
924 
925                 /* Ensure that the starting point is not too late */
926                 int myMaxIndex = theFirstIndex
927                         + myScroll;
928                 if (myMaxIndex > myCount) {
929                     /* Adjust the first index */
930                     theFirstIndex = myCount
931                             - myScroll;
932                     myMaxIndex = myCount;
933                 }
934 
935                 /* Loop through the items to add */
936                 for (int i = theFirstIndex; i < myMaxIndex; i++) {
937                     /* Add the items */
938                     theActiveItems.add(theMenuItems.get(i).getPanel());
939                 }
940 
941                 /* Calculate size of menu */
942                 theDialog.pack();
943                 final int myHeight = theDialog.getHeight();
944 
945                 /* Set visibility of scroll items */
946                 theUpItem.getPanel().setEnabled(theFirstIndex > 0);
947                 theDownItem.getPanel().setEnabled(myMaxIndex < myCount);
948 
949                 /* Determine the size */
950                 theMenuSize = new Dimension(myWidth, myHeight);
951 
952                 /* Fix the width */
953                 theDialog.setPreferredSize(theMenuSize);
954                 theDialog.pack();
955             }
956 
957             /* else reset the menuSize */
958         } else {
959             theMenuSize = null;
960         }
961 
962         /* Reset flag */
963         needReBuild = false;
964     }
965 
966     /**
967      * ScrollPlusOne.
968      */
969     protected void scrollPlusOne() {
970         /* If we are already built */
971         if (!needReBuild) {
972             /* Access the number of entries */
973             final int myCount = theMenuItems.size();
974 
975             /* Reset the children */
976             closeChildren();
977 
978             /* Ensure Up item is enabled */
979             theUpItem.getPanel().setEnabled(true);
980 
981             /* Remove the first item */
982             theActiveItems.remove(0);
983 
984             /* Add the final item */
985             final int myLast = theFirstIndex + theMaxDisplayItems;
986             final TethysUISwingScrollElement myItem = theMenuItems.get(myLast);
987             myItem.setActive(false);
988             theActiveItems.add(myItem.getPanel());
989 
990             /* Adjust down item */
991             theDownItem.getPanel().setEnabled(myLast + 1 < myCount);
992 
993             /* Make sure that the menu is sized correctly */
994             theDialog.pack();
995         }
996 
997         /* Adjust first index */
998         theFirstIndex++;
999     }
1000 
1001     /**
1002      * ScrollMinusOne.
1003      */
1004     protected void scrollMinusOne() {
1005         /* If we are already built */
1006         if (!needReBuild) {
1007             /* Reset the children */
1008             closeChildren();
1009 
1010             /* Ensure Down item is enabled */
1011             theDownItem.getPanel().setEnabled(true);
1012 
1013             /* Remove the last item */
1014             theActiveItems.remove(theMaxDisplayItems - 1);
1015 
1016             /* Add the initial item */
1017             final TethysUISwingScrollElement myItem = theMenuItems.get(theFirstIndex - 1);
1018             myItem.setActive(false);
1019             theActiveItems.add(myItem.getPanel(), 0);
1020 
1021             /* Adjust up item */
1022             theUpItem.getPanel().setEnabled(theFirstIndex > 1);
1023 
1024             /* Make sure that the menu is sized correctly */
1025             theDialog.pack();
1026         }
1027 
1028         /* Adjust first index */
1029         theFirstIndex--;
1030     }
1031 
1032     /**
1033      * request scroll.
1034      *
1035      * @param pDelta the delta to scroll.
1036      */
1037     void requestScroll(final int pDelta) {
1038         /* If this is a scroll downwards */
1039         if (pDelta > 0) {
1040             /* If we can scroll downwards */
1041             final int myCount = theMenuItems.size();
1042             final int mySpace = myCount - theFirstIndex - theMaxDisplayItems;
1043             int myScroll = Math.min(mySpace, pDelta);
1044 
1045             /* While we have space */
1046             while (myScroll-- > 0) {
1047                 /* Scroll downwards */
1048                 scrollPlusOne();
1049             }
1050 
1051             /* else scroll upwards if we can */
1052         } else if (theFirstIndex > 0) {
1053             /* Determine space */
1054             int myScroll = Math.min(theFirstIndex, -pDelta);
1055 
1056             /* While we have space */
1057             while (myScroll-- > 0) {
1058                 /* Scroll upwards */
1059                 scrollMinusOne();
1060             }
1061         }
1062     }
1063 
1064     /**
1065      * Scroll item.
1066      */
1067     public abstract static class TethysUISwingScrollElement {
1068         /**
1069          * Default row size.
1070          */
1071         private static final int DEFAULT_ROWHEIGHT = 16;
1072 
1073         /**
1074          * The panel.
1075          */
1076         private final JPanel thePanel;
1077 
1078         /**
1079          * The label.
1080          */
1081         private final JLabel theLabel;
1082 
1083         /**
1084          * The icon label.
1085          */
1086         private final JLabel theIcon;
1087 
1088         /**
1089          * The base colour.
1090          */
1091         private Color theBaseColor;
1092 
1093         /**
1094          * Constructor.
1095          *
1096          * @param pName    the display name
1097          * @param pGraphic the icon for the item
1098          */
1099         private TethysUISwingScrollElement(final String pName,
1100                                            final TethysUIIcon pGraphic) {
1101             /* Create the panel */
1102             thePanel = new JPanel();
1103 
1104             /* Set border layout */
1105             thePanel.setLayout(new BorderLayout());
1106 
1107             /* Create a Label for the name */
1108             theLabel = new JLabel();
1109             theLabel.setHorizontalAlignment(SwingConstants.LEFT);
1110             theLabel.setText(pName);
1111             theLabel.setMaximumSize(new Dimension(Integer.MAX_VALUE, DEFAULT_ROWHEIGHT));
1112 
1113             /* Create a Label for the graphic */
1114             theIcon = new JLabel();
1115             theIcon.setIcon(TethysUISwingIcon.getIcon(pGraphic));
1116             final Dimension myDim = new Dimension(TethysUIConstant.DEFAULT_ICONSIZE, DEFAULT_ROWHEIGHT);
1117             theIcon.setMinimumSize(myDim);
1118             theIcon.setPreferredSize(myDim);
1119             theIcon.setMaximumSize(myDim);
1120 
1121             /* Add the children */
1122             thePanel.add(theIcon, BorderLayout.LINE_START);
1123             thePanel.add(theLabel, BorderLayout.CENTER);
1124         }
1125 
1126         /**
1127          * Constructor.
1128          *
1129          * @param pGraphic the icon for the item
1130          */
1131         private TethysUISwingScrollElement(final TethysUIIcon pGraphic) {
1132             /* Create the panel */
1133             thePanel = new JPanel();
1134 
1135             /* No label required */
1136             theLabel = null;
1137 
1138             /* Create a Label for the graphic */
1139             theIcon = new JLabel();
1140             theIcon.setIcon(TethysUISwingIcon.getIcon(pGraphic));
1141             theIcon.setHorizontalAlignment(SwingConstants.CENTER);
1142 
1143             /* Add the children */
1144             thePanel.add(theIcon);
1145         }
1146 
1147         /**
1148          * Obtain the panel.
1149          *
1150          * @return the panel
1151          */
1152         protected JPanel getPanel() {
1153             return thePanel;
1154         }
1155 
1156         /**
1157          * Obtain the text.
1158          *
1159          * @return the text
1160          */
1161         public String getText() {
1162             return theLabel.getText();
1163         }
1164 
1165         /**
1166          * Set the graphic.
1167          *
1168          * @param pGraphic the graphic
1169          */
1170         protected void setIcon(final TethysUIIcon pGraphic) {
1171             theIcon.setIcon(TethysUISwingIcon.getIcon(pGraphic));
1172         }
1173 
1174         /**
1175          * Set the active indication.
1176          *
1177          * @param pActive true/false
1178          */
1179         protected void setActive(final boolean pActive) {
1180             if (pActive) {
1181                 if (theBaseColor == null) {
1182                     theBaseColor = thePanel.getBackground();
1183                     thePanel.setBackground(COLOR_BACKGROUND);
1184                 }
1185             } else {
1186                 if (theBaseColor != null) {
1187                     thePanel.setBackground(theBaseColor);
1188                     theBaseColor = null;
1189                 }
1190             }
1191         }
1192 
1193         /**
1194          * Add Menu icon.
1195          */
1196         protected void addMenuIcon() {
1197             final JLabel myLabel = new JLabel(TethysUISwingArrowIcon.RIGHT);
1198             thePanel.add(myLabel, BorderLayout.LINE_END);
1199         }
1200     }
1201 
1202     /**
1203      * Scroll item.
1204      *
1205      * @param <T> the value type
1206      */
1207     protected static class TethysUISwingScrollItem<T>
1208             extends TethysUISwingScrollElement
1209             implements TethysUIScrollItem<T> {
1210         /**
1211          * Parent context menu.
1212          */
1213         private final TethysUISwingScrollMenu<T> theContext;
1214 
1215         /**
1216          * The index.
1217          */
1218         private final int theIndex;
1219 
1220         /**
1221          * Associated value.
1222          */
1223         private final T theValue;
1224 
1225         /**
1226          * Constructor.
1227          *
1228          * @param pContext the parent context menu
1229          * @param pValue   the value
1230          * @param pName    the display name
1231          * @param pGraphic the icon for the item
1232          */
1233         protected TethysUISwingScrollItem(final TethysUISwingScrollMenu<T> pContext,
1234                                           final T pValue,
1235                                           final String pName,
1236                                           final TethysUIIcon pGraphic) {
1237             /* Call super-constructor */
1238             super(pName, pGraphic);
1239 
1240             /* Record parameters */
1241             theContext = pContext;
1242             theValue = pValue;
1243 
1244             /* Determine the index */
1245             theIndex = theContext.getItemCount();
1246 
1247             /* Handle removal of subMenus */
1248             getPanel().addMouseListener(new MouseAdapter() {
1249                 @Override
1250                 public void mouseEntered(final MouseEvent e) {
1251                     handleMouseEntered();
1252                     setActive(true);
1253                 }
1254 
1255                 @Override
1256                 public void mouseExited(final MouseEvent e) {
1257                     setActive(false);
1258                 }
1259 
1260                 @Override
1261                 public void mouseClicked(final MouseEvent e) {
1262                     setActive(false);
1263                     handleMouseClicked();
1264                 }
1265             });
1266         }
1267 
1268         /**
1269          * handle mouseClicked.
1270          */
1271         void handleMouseClicked() {
1272             theContext.setSelectedItem(this);
1273         }
1274 
1275         /**
1276          * handle mouseEntered.
1277          */
1278         void handleMouseEntered() {
1279             theContext.handleActiveItem(this);
1280         }
1281 
1282         @Override
1283         public T getValue() {
1284             return theValue;
1285         }
1286 
1287         @Override
1288         public void scrollToItem() {
1289             theContext.scrollToIndex(theIndex);
1290         }
1291     }
1292 
1293     /**
1294      * Scroll item.
1295      *
1296      * @param <T> the value type
1297      */
1298     private static final class TethysUISwingScrollToggle<T>
1299             extends TethysUISwingScrollItem<T>
1300             implements TethysUIScrollToggle<T> {
1301         /**
1302          * Selected state.
1303          */
1304         private boolean isSelected;
1305 
1306         /**
1307          * Constructor.
1308          *
1309          * @param pContext the parent context menu
1310          * @param pValue   the value
1311          * @param pName    the display name
1312          */
1313         TethysUISwingScrollToggle(final TethysUISwingScrollMenu<T> pContext,
1314                                   final T pValue,
1315                                   final String pName) {
1316             /* Call super-constructor */
1317             super(pContext, pValue, pName, null);
1318         }
1319 
1320         @Override
1321         public boolean isSelected() {
1322             return isSelected;
1323         }
1324 
1325         @Override
1326         public void setSelected(final boolean pSelected) {
1327             isSelected = pSelected;
1328             setIcon(isSelected
1329                     ? TethysUISwingUtils.getIconAtSize(TethysUIScrollIcon.CHECKMARK, TethysUIConstant.DEFAULT_ICONSIZE)
1330                     : null);
1331         }
1332 
1333         @Override
1334         public void toggleSelected() {
1335             setSelected(!isSelected);
1336         }
1337     }
1338 
1339     /**
1340      * Scroll menu.
1341      *
1342      * @param <T> the value type
1343      */
1344     public static final class TethysUISwingScrollSubMenu<T>
1345             extends TethysUISwingScrollElement
1346             implements TethysUIScrollSubMenu<T> {
1347         /**
1348          * Parent contextMenu.
1349          */
1350         private final TethysUISwingScrollMenu<T> theContext;
1351 
1352         /**
1353          * The index.
1354          */
1355         private final int theIndex;
1356 
1357         /**
1358          * Associated value.
1359          */
1360         private final TethysUISwingScrollMenu<T> theSubMenu;
1361 
1362         /**
1363          * Constructor.
1364          *
1365          * @param pContext the parent context menu
1366          * @param pName    the name
1367          * @param pGraphic the icon for the menu
1368          */
1369         TethysUISwingScrollSubMenu(final TethysUISwingScrollMenu<T> pContext,
1370                                    final String pName,
1371                                    final TethysUIIcon pGraphic) {
1372             /* Call super-constructor */
1373             super(pName, pGraphic);
1374 
1375             /* Record parameters */
1376             theContext = pContext;
1377 
1378             /* Create the subMenu */
1379             theSubMenu = new TethysUISwingScrollMenu<>(this);
1380 
1381             /* Determine the index */
1382             theIndex = theContext.getItemCount();
1383 
1384             /* Set menu icon */
1385             addMenuIcon();
1386 
1387             /* Handle show menu */
1388             getPanel().addMouseListener(new MouseAdapter() {
1389                 @Override
1390                 public void mouseEntered(final MouseEvent e) {
1391                     handleMouseEntered();
1392                 }
1393 
1394                 @Override
1395                 public void mouseExited(final MouseEvent e) {
1396                     setActive(false);
1397                 }
1398             });
1399         }
1400 
1401         /**
1402          * handle MouseEntered.
1403          */
1404         void handleMouseEntered() {
1405             /* handle the active menu */
1406             theContext.handleActiveMenu(this);
1407             setActive(true);
1408 
1409             /* Show the menu */
1410             theSubMenu.ensureSubDialog(this);
1411             theSubMenu.showMenuAtPosition(getPanel(), SwingConstants.RIGHT);
1412         }
1413 
1414         /**
1415          * Obtain the parent.
1416          *
1417          * @return the parent
1418          */
1419         TethysUISwingScrollMenu<T> getContext() {
1420             return theContext;
1421         }
1422 
1423         @Override
1424         public TethysUISwingScrollMenu<T> getSubMenu() {
1425             return theSubMenu;
1426         }
1427 
1428         /**
1429          * Obtain the index.
1430          *
1431          * @return the index
1432          */
1433         int getIndex() {
1434             return theIndex;
1435         }
1436 
1437         /**
1438          * Hide the subMenu.
1439          */
1440         void hideMenu() {
1441             theSubMenu.closeMenu();
1442         }
1443 
1444         /**
1445          * Obtain the dialog.
1446          *
1447          * @return the dialog
1448          */
1449         private JDialog getDialog() {
1450             return theContext.getDialog();
1451         }
1452 
1453         /**
1454          * Set the number of items in the scrolling portion of the menu.
1455          *
1456          * @param pMaxDisplayItems the maximum number of items to display
1457          * @throws IllegalArgumentException if pMaxDisplayItems is 0 or negative
1458          */
1459         private void setMaxDisplayItems(final int pMaxDisplayItems) {
1460             /* Pass call to subMenu */
1461             theSubMenu.setMaxDisplayItems(pMaxDisplayItems);
1462         }
1463 
1464         /**
1465          * Ensure that this menu is visible immediately the context is displayed.
1466          */
1467         void scrollToMenu() {
1468             theContext.scrollToIndex(theIndex);
1469         }
1470     }
1471 
1472     /**
1473      * Scroll control class.
1474      */
1475     private final class ScrollControl
1476             extends TethysUISwingScrollElement {
1477         /**
1478          * Increment.
1479          */
1480         private final int theIncrement;
1481 
1482         /**
1483          * Timer.
1484          */
1485         private TimerTask theTimerTask;
1486 
1487         /**
1488          * Constructor.
1489          *
1490          * @param pIcon      the icon
1491          * @param pIncrement the increment
1492          */
1493         ScrollControl(final TethysUIIcon pIcon,
1494                       final int pIncrement) {
1495             /* Set the icon for the item */
1496             super(pIcon);
1497 
1498             /* Store parameters */
1499             theIncrement = pIncrement;
1500 
1501             /* Handle show menu */
1502             getPanel().addMouseListener(new MouseAdapter() {
1503                 @Override
1504                 public void mouseClicked(final MouseEvent e) {
1505                     processScroll();
1506                 }
1507 
1508                 @Override
1509                 public void mouseEntered(final MouseEvent e) {
1510                     processMouseEnter();
1511                 }
1512 
1513                 @Override
1514                 public void mouseExited(final MouseEvent e) {
1515                     processMouseExit();
1516                 }
1517             });
1518         }
1519 
1520         /**
1521          * Process scroll event.
1522          */
1523         void processScroll() {
1524             /* Request the scroll */
1525             requestScroll(theIncrement);
1526         }
1527 
1528         /**
1529          * Process mouseEnter event.
1530          */
1531         void processMouseEnter() {
1532             /* cancel any existing task */
1533             if (theTimerTask != null) {
1534                 theTimerTask.cancel();
1535             }
1536 
1537             /* Close any children */
1538             closeChildren();
1539 
1540             /* Set no active Item */
1541             clearActiveItem();
1542             setActive(true);
1543 
1544             /* Create new timer task */
1545             theTimerTask = new TimerTask() {
1546                 @Override
1547                 public void run() {
1548                     SwingUtilities.invokeLater(ScrollControl.this::processScroll);
1549                 }
1550             };
1551 
1552             /* Schedule the task */
1553             getTimer().schedule(theTimerTask, TethysUICoreScrollMenu.INITIAL_SCROLLDELAY, TethysUICoreScrollMenu.REPEAT_SCROLLDELAY);
1554         }
1555 
1556         /**
1557          * Process mouseExit event.
1558          */
1559         void processMouseExit() {
1560             /* If the timer is stopped */
1561             if (theTimerTask != null) {
1562                 theTimerTask.cancel();
1563                 theTimerTask = null;
1564             }
1565             setActive(false);
1566         }
1567     }
1568 }