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.javafx.menu;
18  
19  import io.github.tonywasher.joceanus.oceanus.event.OceanusEventManager;
20  import io.github.tonywasher.joceanus.oceanus.event.OceanusEventRegistrar;
21  import javafx.animation.KeyFrame;
22  import javafx.animation.Timeline;
23  import javafx.collections.ObservableList;
24  import javafx.event.Event;
25  import javafx.event.EventHandler;
26  import javafx.event.EventType;
27  import javafx.geometry.Dimension2D;
28  import javafx.geometry.Insets;
29  import javafx.geometry.Point2D;
30  import javafx.geometry.Pos;
31  import javafx.geometry.Side;
32  import javafx.scene.Node;
33  import javafx.scene.Scene;
34  import javafx.scene.control.ContentDisplay;
35  import javafx.scene.control.Label;
36  import javafx.scene.input.KeyEvent;
37  import javafx.scene.input.MouseEvent;
38  import javafx.scene.input.ScrollEvent;
39  import javafx.scene.layout.BorderPane;
40  import javafx.scene.layout.VBox;
41  import javafx.scene.shape.Polygon;
42  import javafx.stage.Modality;
43  import javafx.stage.Stage;
44  import javafx.stage.StageStyle;
45  import javafx.util.Duration;
46  import io.github.tonywasher.joceanus.tethys.api.base.TethysUIConstant;
47  import io.github.tonywasher.joceanus.tethys.api.base.TethysUIEvent;
48  import io.github.tonywasher.joceanus.tethys.api.base.TethysUIIcon;
49  import io.github.tonywasher.joceanus.tethys.api.menu.TethysUIScrollIcon;
50  import io.github.tonywasher.joceanus.tethys.api.menu.TethysUIScrollItem;
51  import io.github.tonywasher.joceanus.tethys.api.menu.TethysUIScrollMenu;
52  import io.github.tonywasher.joceanus.tethys.api.menu.TethysUIScrollSubMenu;
53  import io.github.tonywasher.joceanus.tethys.api.menu.TethysUIScrollToggle;
54  import io.github.tonywasher.joceanus.tethys.core.menu.TethysUICoreScrollMenu;
55  import io.github.tonywasher.joceanus.tethys.javafx.base.TethysUIFXArrowIcon;
56  import io.github.tonywasher.joceanus.tethys.javafx.base.TethysUIFXIcon;
57  import io.github.tonywasher.joceanus.tethys.javafx.base.TethysUIFXUtils;
58  
59  import java.util.ArrayList;
60  import java.util.Iterator;
61  import java.util.List;
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 TethysUIFXScrollMenu<T>
71          implements TethysUIScrollMenu<T> {
72      /**
73       * StyleSheet Name.
74       */
75      private static final String CSS_STYLE_NAME = "jtethys-javafx-contextmenu.css";
76  
77      /**
78       * StyleSheet.
79       */
80      private static final String CSS_STYLE = TethysUIFXScrollMenu.class.getResource(CSS_STYLE_NAME).toExternalForm();
81  
82      /**
83       * The menu style.
84       */
85      private static final String STYLE_MENU = TethysUIFXUtils.CSS_STYLE_BASE + "-context";
86  
87      /**
88       * The item style.
89       */
90      static final String STYLE_ITEM = STYLE_MENU + "-item";
91  
92      /**
93       * The Event Manager.
94       */
95      private final OceanusEventManager<TethysUIEvent> theEventManager;
96  
97      /**
98       * List of menu items.
99       */
100     private final List<TethysUIFXScrollElement> theMenuItems;
101 
102     /**
103      * List of active menu items.
104      */
105     private final ObservableList<Node> theActiveItems;
106 
107     /**
108      * First item to show in list.
109      */
110     private int theFirstIndex;
111 
112     /**
113      * Max number of items to display in popUp.
114      */
115     private int theMaxDisplayItems;
116 
117     /**
118      * The ScrollUp Item.
119      */
120     private final ScrollControl theUpItem;
121 
122     /**
123      * The ScrollDown Item.
124      */
125     private final ScrollControl theDownItem;
126 
127     /**
128      * The container box.
129      */
130     private final BorderPane theContainer;
131 
132     /**
133      * The Parent scrollMenu.
134      */
135     private final TethysUIFXScrollSubMenu<T> theParentMenu;
136 
137     /**
138      * The Parent contextMenu.
139      */
140     private final TethysUIFXScrollMenu<T> theParentContext;
141 
142     /**
143      * The Stage.
144      */
145     private Stage theStage;
146 
147     /**
148      * The Active subMenu.
149      */
150     private TethysUIFXScrollSubMenu<T> theActiveMenu;
151 
152     /**
153      * The Active item.
154      */
155     private TethysUIFXScrollItem<T> theActiveItem;
156 
157     /**
158      * The selected value.
159      */
160     private TethysUIFXScrollItem<T> theSelectedItem;
161 
162     /**
163      * Do we need to close menu on toggle?
164      */
165     private boolean closeOnToggle;
166 
167     /**
168      * Do we need to reBuild the menu?
169      */
170     private boolean needReBuild;
171 
172     /**
173      * The size of the menu.
174      */
175     private Dimension2D theMenuSize;
176 
177     /**
178      * Constructor.
179      */
180     TethysUIFXScrollMenu() {
181         this(TethysUICoreScrollMenu.DEFAULT_ITEMCOUNT);
182     }
183 
184     /**
185      * Constructor.
186      *
187      * @param pMaxDisplayItems the maximum number of items to display
188      */
189     TethysUIFXScrollMenu(final int pMaxDisplayItems) {
190         this(null, pMaxDisplayItems);
191     }
192 
193     /**
194      * Constructor.
195      *
196      * @param pParent the parent scroll menu
197      */
198     TethysUIFXScrollMenu(final TethysUIFXScrollSubMenu<T> pParent) {
199         this(pParent, pParent.getContext().getMaxDisplayItems());
200     }
201 
202     /**
203      * Constructor.
204      *
205      * @param pParent          the parent scroll menu
206      * @param pMaxDisplayItems the maximum number of items to display
207      */
208     private TethysUIFXScrollMenu(final TethysUIFXScrollSubMenu<T> pParent,
209                                  final int pMaxDisplayItems) {
210         /* Check parameters */
211         if (pMaxDisplayItems <= 0) {
212             throw new IllegalArgumentException(TethysUICoreScrollMenu.ERROR_MAXITEMS);
213         }
214 
215         /* Create event manager */
216         theEventManager = new OceanusEventManager<>();
217 
218         /* Record parameters */
219         theMaxDisplayItems = pMaxDisplayItems;
220         theParentMenu = pParent;
221         theParentContext = theParentMenu == null
222                 ? null
223                 : theParentMenu.getContext();
224 
225         /* Initially need reBuild */
226         needReBuild = true;
227 
228         /* Initially close on toggle */
229         closeOnToggle = true;
230 
231         /* Create the scroll items */
232         theUpItem = new ScrollControl(TethysUIFXArrowIcon.UP.getArrow(), -1);
233         theDownItem = new ScrollControl(TethysUIFXArrowIcon.DOWN.getArrow(), 1);
234 
235         /* Allocate the list */
236         theMenuItems = new ArrayList<>();
237         final VBox myBox = new VBox();
238         myBox.setSpacing(2);
239         myBox.setPadding(new Insets(2, 2, 2, 2));
240         theActiveItems = myBox.getChildren();
241 
242         /* Create the scene */
243         theContainer = new BorderPane();
244         theContainer.setCenter(myBox);
245         theContainer.getStyleClass().add(STYLE_MENU);
246     }
247 
248     @Override
249     public OceanusEventRegistrar<TethysUIEvent> getEventRegistrar() {
250         return theEventManager.getEventRegistrar();
251     }
252 
253     /**
254      * Ensure the Stage.
255      */
256     private void ensureStage() {
257         if (theStage == null) {
258             createStage();
259         }
260     }
261 
262     /**
263      * Create the Stage.
264      */
265     private void createStage() {
266         /* Non-Modal and undecorated */
267         theStage = new Stage(StageStyle.UNDECORATED);
268         theStage.initModality(Modality.NONE);
269 
270         /* Create the scene */
271         final Scene myScene = new Scene(theContainer);
272         final ObservableList<String> mySheets = myScene.getStylesheets();
273         mySheets.add(CSS_STYLE);
274         theStage.setScene(myScene);
275 
276         /* Add listener to shut dialog on loss of focus */
277         theStage.focusedProperty().addListener((v, o, n) -> handleFocusChange(n));
278 
279         /* ensure that escape closes menu */
280         myScene.setOnKeyPressed(this::handleKeyPress);
281 
282         /* Handle scroll events */
283         theStage.addEventFilter(ScrollEvent.SCROLL, this::handleScroll);
284     }
285 
286     /**
287      * CloseOnFocusLoss.
288      */
289     private void closeOnFocusLoss() {
290         /* Pass call on to parent if it exists */
291         if (theParentContext != null) {
292             theParentContext.closeOnFocusLoss();
293         }
294 
295         /* Close the menu */
296         closeMenu();
297     }
298 
299     @Override
300     public TethysUIScrollItem<T> getSelectedItem() {
301         return theSelectedItem;
302     }
303 
304     /**
305      * Obtain the maximum # of items in the displayed PopUp window.
306      *
307      * @return the # of items
308      */
309     public int getMaxDisplayItems() {
310         return theMaxDisplayItems;
311     }
312 
313     @Override
314     public void setCloseOnToggle(final boolean pCloseOnToggle) {
315         /* Set value */
316         closeOnToggle = pCloseOnToggle;
317     }
318 
319     /**
320      * Is the menu showing?
321      *
322      * @return true/false
323      */
324     public boolean isShowing() {
325         return theStage != null
326                 && theStage.isShowing();
327     }
328 
329     @Override
330     public void setMaxDisplayItems(final int pMaxDisplayItems) {
331         /* Check parameters */
332         if (pMaxDisplayItems <= 0) {
333             throw new IllegalArgumentException(TethysUICoreScrollMenu.ERROR_MAXITEMS);
334         }
335 
336         /* Check state */
337         if (isShowing()) {
338             throw new IllegalStateException();
339         }
340 
341         /* Store parameters */
342         theMaxDisplayItems = pMaxDisplayItems;
343 
344         /* Loop through the children */
345         final Iterator<TethysUIFXScrollElement> myIterator = theMenuItems.iterator();
346         while (myIterator.hasNext()) {
347             final TethysUIFXScrollElement myChild = myIterator.next();
348 
349             /* If this is a subMenu */
350             if (myChild instanceof TethysUIFXScrollSubMenu) {
351                 /* Pass call on */
352                 final TethysUIFXScrollSubMenu<?> mySubMenu = (TethysUIFXScrollSubMenu<?>) myChild;
353                 mySubMenu.setMaxDisplayItems(pMaxDisplayItems);
354             }
355         }
356 
357         /* Request reBuild */
358         needReBuild = true;
359     }
360 
361     /**
362      * Show the menu at position.
363      *
364      * @param pAnchor the anchor node
365      * @param pSide   the side of the anchor node
366      */
367     public void showMenuAtPosition(final Node pAnchor,
368                                    final Side pSide) {
369         /* Ensure the stage */
370         ensureStage();
371 
372         /* determine the size of the menu */
373         determineSize();
374 
375         /* If we have elements */
376         if (theMenuSize != null) {
377             /* determine location to display */
378             final Point2D myLocation = TethysUIFXUtils.obtainDisplayPoint(pAnchor, pSide, theMenuSize);
379 
380             /* Show menu */
381             showMenuAtLocation(myLocation);
382         }
383     }
384 
385     /**
386      * Show the menu at position.
387      *
388      * @param pAnchor the anchor node
389      * @param pX      the relative X position
390      * @param pY      the relative Y position
391      */
392     public void showMenuAtPosition(final Node pAnchor,
393                                    final double pX,
394                                    final double pY) {
395         /* Ensure the stage */
396         ensureStage();
397 
398         /* determine the size of the menu */
399         determineSize();
400 
401         /* If we have elements */
402         if (theMenuSize != null) {
403             /* determine location to display */
404             final Point2D myRequest = new Point2D(pX, pY);
405             final Point2D myLocation = TethysUIFXUtils.obtainDisplayPoint(pAnchor, myRequest, theMenuSize);
406 
407             /* Show menu */
408             showMenuAtLocation(myLocation);
409         }
410     }
411 
412     /**
413      * Show the menu at location.
414      *
415      * @param pLocation the location
416      */
417     private void showMenuAtLocation(final Point2D pLocation) {
418         /* Record position */
419         theStage.setX(pLocation.getX());
420         theStage.setY(pLocation.getY());
421 
422         /* Show menu */
423         showMenu();
424     }
425 
426     /**
427      * Show the menu.
428      */
429     private void showMenu() {
430         /* Initialise the values */
431         theSelectedItem = null;
432         theActiveMenu = null;
433         theActiveItem = null;
434 
435         /* show the menu */
436         theStage.show();
437     }
438 
439     /**
440      * Close non-Modal.
441      */
442     void closeMenu() {
443         /* Close any children */
444         closeChildren();
445 
446         /* Close the menu */
447         theStage.close();
448     }
449 
450     /**
451      * Clear active Item.
452      */
453     void clearActiveItem() {
454         theActiveItem = null;
455     }
456 
457     /**
458      * Set the selected item.
459      *
460      * @param pItem the selected item
461      */
462     void setSelectedItem(final TethysUIFXScrollItem<T> pItem) {
463         /* If we are a child menu */
464         if (theParentContext != null) {
465             /* pass call on to parent */
466             theParentContext.setSelectedItem(pItem);
467 
468             /* else we are top-level */
469         } else {
470             /* We assume that we will close the menu */
471             boolean doCloseMenu = true;
472 
473             /* record selection */
474             theSelectedItem = pItem;
475             if (theSelectedItem instanceof TethysUIFXScrollToggle) {
476                 final TethysUIScrollToggle<?> myItem = (TethysUIScrollToggle<?>) theSelectedItem;
477                 myItem.toggleSelected();
478                 doCloseMenu = closeOnToggle;
479             }
480 
481             /* Close the menu if requested */
482             if (doCloseMenu) {
483                 /* Close the menu */
484                 closeMenu();
485             }
486 
487             /* fire selection event */
488             theEventManager.fireEvent(TethysUIEvent.NEWVALUE, theSelectedItem);
489         }
490     }
491 
492     /**
493      * Handle escapeKey.
494      */
495     private void handleEscapeKey() {
496         /* If we are a child menu */
497         if (theParentContext != null) {
498             /* pass call on to parent */
499             theParentContext.handleEscapeKey();
500 
501             /* else we are top-level */
502         } else {
503             /* fire cancellation event */
504             theEventManager.fireEvent(TethysUIEvent.WINDOWCLOSED);
505 
506             /* Notify the cancel */
507             closeMenu();
508         }
509     }
510 
511     /**
512      * Handle enterKey.
513      */
514     private void handleEnterKey() {
515         /* If we are a child menu */
516         if (theActiveItem != null) {
517             /* assume item is selected */
518             setSelectedItem(theActiveItem);
519         }
520     }
521 
522     /**
523      * Handle focusChange.
524      *
525      * @param pState the focus state
526      */
527     private void handleFocusChange(final Boolean pState) {
528         /* If we've lost focus to other than the active subMenu */
529         if (Boolean.TRUE.equals(!pState)
530                 && theActiveMenu == null) {
531             /* fire cancellation event */
532             if (theParentMenu == null) {
533                 theEventManager.fireEvent(TethysUIEvent.WINDOWCLOSED);
534             }
535 
536             /* Close the menu hierarchy if we are currently showing */
537             if (isShowing()) {
538                 closeOnFocusLoss();
539             }
540         }
541     }
542 
543     /**
544      * Handle keyPress.
545      *
546      * @param pEvent the event
547      */
548     private void handleKeyPress(final KeyEvent pEvent) {
549         switch (pEvent.getCode()) {
550             case ESCAPE:
551                 handleEscapeKey();
552                 break;
553             case ENTER:
554                 handleEnterKey();
555                 break;
556             default:
557                 break;
558         }
559     }
560 
561     /**
562      * Handle scroll.
563      *
564      * @param pEvent the event
565      */
566     private void handleScroll(final ScrollEvent pEvent) {
567         /* request the scroll */
568         final double myDelta = pEvent.getDeltaY() / pEvent.getMultiplierY();
569         requestScroll((int) -myDelta);
570 
571         /* Consume the event */
572         pEvent.consume();
573     }
574 
575     /**
576      * Handle activeItem.
577      *
578      * @param pItem the item
579      */
580     void handleActiveItem(final TethysUIFXScrollItem<T> pItem) {
581         /* Close any children */
582         closeChildren();
583 
584         /* Record that we are the active item */
585         theActiveItem = pItem;
586     }
587 
588     /**
589      * Handle activeMenu.
590      *
591      * @param pMenu the menu
592      */
593     void handleActiveMenu(final TethysUIFXScrollSubMenu<T> pMenu) {
594         /* Hide any existing menu that is not us */
595         if (theActiveMenu != null
596                 && theActiveMenu.getIndex() != pMenu.getIndex()) {
597             theActiveMenu.hide();
598         }
599 
600         /* Set no active Item */
601         theActiveItem = null;
602 
603         /* Record active menu */
604         theActiveMenu = pMenu;
605     }
606 
607     @Override
608     public void removeAllItems() {
609         /* Check state */
610         if (isShowing()) {
611             closeMenu();
612         }
613 
614         /* Clear menuItems */
615         theMenuItems.clear();
616         theFirstIndex = 0;
617         needReBuild = true;
618 
619         /* Clear state */
620         theSelectedItem = null;
621     }
622 
623     @Override
624     public boolean isEmpty() {
625         /* Obtain count */
626         return theMenuItems.isEmpty();
627     }
628 
629     /**
630      * Obtain count of menu Items.
631      *
632      * @return the count
633      */
634     protected int getItemCount() {
635         /* Obtain count */
636         return theMenuItems.size();
637     }
638 
639     /**
640      * Ensure item at index will be visible when displayed.
641      *
642      * @param pIndex the index to show
643      */
644     void scrollToIndex(final int pIndex) {
645         /* Show the index */
646         showIndex(pIndex);
647 
648         /* cascade call upwards */
649         if (theParentMenu != null) {
650             theParentMenu.scrollToMenu();
651         }
652     }
653 
654     /**
655      * Ensure index shown.
656      *
657      * @param pIndex the index to show
658      */
659     private void showIndex(final int pIndex) {
660         /* Ignore if index is out of range */
661         final int myCount = theMenuItems.size();
662         if (pIndex < 0
663                 || pIndex >= myCount) {
664             return;
665         }
666 
667         /* If index is above window */
668         if (pIndex < theFirstIndex) {
669             /* Scroll window upwards and return */
670             requestScroll(pIndex - theFirstIndex);
671             return;
672         }
673 
674         /* If index is beyond last visible index */
675         final int myLastIndex = theFirstIndex
676                 + theMaxDisplayItems
677                 - 1;
678         if (myLastIndex < pIndex) {
679             /* Scroll window downwards */
680             requestScroll(pIndex - myLastIndex);
681         }
682     }
683 
684     @Override
685     public TethysUIScrollItem<T> addItem(final T pValue) {
686         /* Use standard name */
687         return addItem(pValue, pValue.toString(), null);
688     }
689 
690     @Override
691     public TethysUIScrollItem<T> addItem(final T pValue,
692                                          final String pName) {
693         /* Use standard name */
694         return addItem(pValue, pName, null);
695     }
696 
697     @Override
698     public TethysUIScrollItem<T> addItem(final T pValue,
699                                          final TethysUIIcon pGraphic) {
700         /* Use standard name */
701         return addItem(pValue, pValue.toString(), pGraphic);
702     }
703 
704     @Override
705     public TethysUIScrollItem<T> addNullItem(final String pName) {
706         /* Use given name */
707         return addItem(null, pName, null);
708     }
709 
710     @Override
711     public TethysUIScrollItem<T> addNullItem(final String pName,
712                                              final TethysUIIcon pGraphic) {
713         /* Use given name */
714         return addItem(null, pName, pGraphic);
715     }
716 
717     @Override
718     public TethysUIScrollItem<T> addItem(final T pValue,
719                                          final String pName,
720                                          final TethysUIIcon pGraphic) {
721         /* Check state */
722         if (isShowing()) {
723             throw new IllegalStateException();
724         }
725 
726         /* Create element */
727         final TethysUIFXScrollItem<T> myItem = new TethysUIFXScrollItem<>(this, pValue, pName, pGraphic);
728 
729         /* Add to the list of menuItems */
730         theMenuItems.add(myItem);
731         needReBuild = true;
732         return myItem;
733     }
734 
735     @Override
736     public TethysUIScrollSubMenu<T> addSubMenu(final String pName) {
737         /* Use given name */
738         return addSubMenu(pName, null);
739     }
740 
741     @Override
742     public TethysUIScrollSubMenu<T> addSubMenu(final String pName,
743                                                final TethysUIIcon pGraphic) {
744         /* Check state */
745         if (isShowing()) {
746             throw new IllegalStateException();
747         }
748 
749         /* Create menu */
750         final TethysUIFXScrollSubMenu<T> myMenu = new TethysUIFXScrollSubMenu<>(this, pName, pGraphic);
751 
752         /* Add to the list of menuItems */
753         theMenuItems.add(myMenu);
754         needReBuild = true;
755         return myMenu;
756     }
757 
758     @Override
759     public TethysUIScrollToggle<T> addToggleItem(final T pValue) {
760         /* Use standard name */
761         return addToggleItem(pValue, pValue.toString());
762     }
763 
764     @Override
765     public TethysUIScrollToggle<T> addToggleItem(final T pValue,
766                                                  final String pName) {
767         /* Check state */
768         if (isShowing()) {
769             throw new IllegalStateException();
770         }
771 
772         /* Create element */
773         final TethysUIFXScrollToggle<T> myItem = new TethysUIFXScrollToggle<>(this, pValue, pName);
774 
775         /* Add to the list of menuItems */
776         theMenuItems.add(myItem);
777         needReBuild = true;
778         return myItem;
779     }
780 
781     /**
782      * close child menus.
783      */
784     void closeChildren() {
785         /* Close any active subMenu */
786         if (theActiveMenu != null) {
787             theActiveMenu.hide();
788         }
789         theActiveMenu = null;
790     }
791 
792     /**
793      * Determine size of menu.
794      */
795     private void determineSize() {
796         /* NoOp if we do not need to reBuild the menu */
797         if (!needReBuild) {
798             return;
799         }
800 
801         /* If we have items */
802         if (!theMenuItems.isEmpty()) {
803             /* Access the number of entries and the scroll count */
804             final int myCount = theMenuItems.size();
805             final int myScroll = Math.min(theMaxDisplayItems, myCount);
806 
807             /* Remove all items */
808             theActiveItems.clear();
809 
810             /* If we do not need to scroll */
811             if (myScroll == myCount) {
812                 /* Ensure we have no arrows */
813                 theContainer.setTop(null);
814                 theContainer.setBottom(null);
815 
816                 /* Loop through the items to add */
817                 for (int i = 0; i < myCount; i++) {
818                     /* Add the items */
819                     theActiveItems.add(theMenuItems.get(i).getBorderPane());
820                 }
821 
822                 /* Calculate size of menu */
823                 theStage.show();
824                 theStage.close();
825 
826                 /* Determine the size */
827                 theMenuSize = new Dimension2D(theStage.getWidth(), theStage.getHeight());
828 
829                 /* else need to set up scroll */
830             } else {
831                 /* Add the scrolling items */
832                 theContainer.setTop(theUpItem.getLabel());
833                 theContainer.setBottom(theDownItem.getLabel());
834                 theUpItem.setVisible(true);
835                 theDownItem.setVisible(true);
836 
837                 /* Add ALL items */
838                 for (final TethysUIFXScrollElement myItem : theMenuItems) {
839                     /* Add the items */
840                     theActiveItems.add(myItem.getBorderPane());
841                 }
842 
843                 /* Calculate size of menu */
844                 theStage.show();
845                 theStage.close();
846                 final double myWidth = theStage.getWidth();
847 
848                 /* Remove all items */
849                 theActiveItems.clear();
850 
851                 /* Ensure that the starting index is positive */
852                 if (theFirstIndex < 0) {
853                     theFirstIndex = 0;
854                 }
855 
856                 /* Ensure that the starting point is not too late */
857                 int myMaxIndex = theFirstIndex
858                         + myScroll;
859                 if (myMaxIndex > myCount) {
860                     /* Adjust the first index */
861                     theFirstIndex = myCount
862                             - myScroll;
863                     myMaxIndex = myCount;
864                 }
865 
866                 /* Loop through the items to add */
867                 for (int i = theFirstIndex; i < myMaxIndex; i++) {
868                     /* Add the items */
869                     theActiveItems.add(theMenuItems.get(i).getBorderPane());
870                 }
871 
872                 /* Calculate size of menu */
873                 theStage.show();
874                 theStage.close();
875                 final double myHeight = theStage.getHeight();
876 
877                 /* Set visibility of scroll items */
878                 theUpItem.setVisible(theFirstIndex > 0);
879                 theDownItem.setVisible(myMaxIndex < myCount);
880 
881                 /* Determine the size */
882                 theMenuSize = new Dimension2D(myWidth, myHeight);
883 
884                 /* Fix the width */
885                 theStage.setMinWidth(myWidth);
886             }
887 
888             /* else reset the menuSize */
889         } else {
890             theMenuSize = null;
891         }
892 
893         /* Reset flag */
894         needReBuild = false;
895     }
896 
897     /**
898      * ScrollPlusOne.
899      */
900     protected void scrollPlusOne() {
901         /* If we are already built */
902         if (!needReBuild) {
903             /* Access the number of entries */
904             final int myCount = theMenuItems.size();
905 
906             /* Reset the children */
907             closeChildren();
908 
909             /* Ensure Up item is enabled */
910             theUpItem.setVisible(true);
911 
912             /* Remove the first item */
913             theActiveItems.remove(0);
914 
915             /* Add the final item */
916             final int myLast = theFirstIndex + theMaxDisplayItems;
917             final TethysUIFXScrollElement myItem = theMenuItems.get(myLast);
918             theActiveItems.add(myItem.getBorderPane());
919 
920             /* Adjust down item */
921             theDownItem.setVisible(myLast + 1 < myCount);
922         }
923 
924         /* Adjust first index */
925         theFirstIndex++;
926     }
927 
928     /**
929      * ScrollMinusOne.
930      */
931     protected void scrollMinusOne() {
932         /* If we are already built */
933         if (!needReBuild) {
934             /* Reset the children */
935             closeChildren();
936 
937             /* Ensure Down item is enabled */
938             theDownItem.setVisible(true);
939 
940             /* Remove the last item */
941             theActiveItems.remove(theMaxDisplayItems - 1);
942 
943             /* Add the initial item */
944             final TethysUIFXScrollElement myItem = theMenuItems.get(theFirstIndex - 1);
945             theActiveItems.add(0, myItem.getBorderPane());
946 
947             /* Adjust up item */
948             theUpItem.setVisible(theFirstIndex > 1);
949         }
950 
951         /* Adjust first index */
952         theFirstIndex--;
953     }
954 
955     /**
956      * request scroll.
957      *
958      * @param pDelta the delta to scroll.
959      */
960     void requestScroll(final int pDelta) {
961         /* If this is a scroll downwards */
962         if (pDelta > 0) {
963             /* If we can scroll downwards */
964             final int myCount = theMenuItems.size();
965             final int mySpace = myCount - theFirstIndex - theMaxDisplayItems;
966             int myScroll = Math.min(mySpace, pDelta);
967 
968             /* While we have space */
969             while (myScroll-- > 0) {
970                 /* Scroll downwards */
971                 scrollPlusOne();
972             }
973 
974             /* else scroll upwards if we can */
975         } else if (theFirstIndex > 0) {
976             /* Determine space */
977             int myScroll = Math.min(theFirstIndex, -pDelta);
978 
979             /* While we have space */
980             while (myScroll-- > 0) {
981                 /* Scroll upwards */
982                 scrollMinusOne();
983             }
984         }
985     }
986 
987     /**
988      * Scroll element.
989      */
990     public abstract static class TethysUIFXScrollElement {
991         /**
992          * BorderPane.
993          */
994         private final BorderPane theBorderPane;
995 
996         /**
997          * The label.
998          */
999         private final Label theLabel;
1000 
1001         /**
1002          * The icon label.
1003          */
1004         private final Label theIcon;
1005 
1006         /**
1007          * Constructor.
1008          *
1009          * @param pName    the display name
1010          * @param pGraphic the icon for the item
1011          */
1012         private TethysUIFXScrollElement(final String pName,
1013                                         final TethysUIIcon pGraphic) {
1014             /* Create the borderPane */
1015             theBorderPane = new BorderPane();
1016 
1017             /* Create a Label for the name */
1018             theLabel = new Label();
1019             theLabel.setText(pName);
1020             theLabel.setMaxWidth(Double.MAX_VALUE);
1021 
1022             /* Create a Label for the graphic */
1023             theIcon = new Label();
1024             theIcon.setGraphic(TethysUIFXIcon.getIcon(pGraphic));
1025             theIcon.setMinWidth(TethysUIConstant.DEFAULT_ICONSIZE);
1026 
1027             /* Add the children */
1028             theBorderPane.setLeft(theIcon);
1029             theBorderPane.setCenter(theLabel);
1030 
1031             /* Set style of item */
1032             theBorderPane.getStyleClass().add(STYLE_ITEM);
1033         }
1034 
1035         /**
1036          * Obtain the label.
1037          *
1038          * @return the label
1039          */
1040         BorderPane getBorderPane() {
1041             return theBorderPane;
1042         }
1043 
1044         /**
1045          * Add event filter.
1046          *
1047          * @param <T>     the event type
1048          * @param pEvent  the event
1049          * @param pFilter the filter
1050          */
1051         <T extends Event> void addEventFilter(final EventType<T> pEvent,
1052                                               final EventHandler<? super T> pFilter) {
1053             theBorderPane.addEventFilter(pEvent, pFilter);
1054         }
1055 
1056         /**
1057          * Obtain the text.
1058          *
1059          * @return the text
1060          */
1061         public String getText() {
1062             return theLabel.getText();
1063         }
1064 
1065         /**
1066          * Set the graphic.
1067          *
1068          * @param pGraphic the graphic
1069          */
1070         protected void setIcon(final TethysUIFXIcon pGraphic) {
1071             theIcon.setGraphic(TethysUIFXIcon.getIcon(pGraphic));
1072         }
1073 
1074         /**
1075          * Add Menu icon.
1076          */
1077         protected void addMenuIcon() {
1078             final Label myLabel = new Label();
1079             myLabel.setGraphic(TethysUIFXArrowIcon.RIGHT.getArrow());
1080             theBorderPane.setRight(myLabel);
1081         }
1082     }
1083 
1084     /**
1085      * Scroll item.
1086      *
1087      * @param <T> the value type
1088      */
1089     protected static class TethysUIFXScrollItem<T>
1090             extends TethysUIFXScrollElement
1091             implements TethysUIScrollItem<T> {
1092         /**
1093          * Parent context menu.
1094          */
1095         private final TethysUIFXScrollMenu<T> theContext;
1096 
1097         /**
1098          * The index.
1099          */
1100         private final int theIndex;
1101 
1102         /**
1103          * Associated value.
1104          */
1105         private final T theValue;
1106 
1107         /**
1108          * Constructor.
1109          *
1110          * @param pContext the parent context menu
1111          * @param pValue   the value
1112          * @param pName    the display name
1113          * @param pGraphic the icon for the item
1114          */
1115         protected TethysUIFXScrollItem(final TethysUIFXScrollMenu<T> pContext,
1116                                        final T pValue,
1117                                        final String pName,
1118                                        final TethysUIIcon pGraphic) {
1119             /* Call super-constructor */
1120             super(pName, pGraphic);
1121 
1122             /* Record parameters */
1123             theContext = pContext;
1124             theValue = pValue;
1125 
1126             /* Determine the index */
1127             theIndex = theContext.getItemCount();
1128 
1129             /* Handle removal of subMenus */
1130             addEventFilter(MouseEvent.MOUSE_ENTERED, e -> theContext.handleActiveItem(this));
1131 
1132             /* Handle selection */
1133             addEventFilter(MouseEvent.MOUSE_CLICKED, e -> theContext.setSelectedItem(this));
1134         }
1135 
1136         @Override
1137         public T getValue() {
1138             return theValue;
1139         }
1140 
1141         @Override
1142         public void scrollToItem() {
1143             theContext.scrollToIndex(theIndex);
1144         }
1145     }
1146 
1147     /**
1148      * Scroll item.
1149      *
1150      * @param <T> the value type
1151      */
1152     private static final class TethysUIFXScrollToggle<T>
1153             extends TethysUIFXScrollItem<T>
1154             implements TethysUIScrollToggle<T> {
1155         /**
1156          * Selected state.
1157          */
1158         private boolean isSelected;
1159 
1160         /**
1161          * Constructor.
1162          *
1163          * @param pContext the parent context menu
1164          * @param pValue   the value
1165          * @param pName    the display name
1166          */
1167         TethysUIFXScrollToggle(final TethysUIFXScrollMenu<T> pContext,
1168                                final T pValue,
1169                                final String pName) {
1170             /* Call super-constructor */
1171             super(pContext, pValue, pName, null);
1172         }
1173 
1174         @Override
1175         public boolean isSelected() {
1176             return isSelected;
1177         }
1178 
1179         @Override
1180         public void setSelected(final boolean pSelected) {
1181             isSelected = pSelected;
1182             setIcon(isSelected
1183                     ? TethysUIFXUtils.getIconAtSize(TethysUIScrollIcon.CHECKMARK, TethysUIConstant.DEFAULT_ICONSIZE)
1184                     : null);
1185         }
1186 
1187         @Override
1188         public void toggleSelected() {
1189             setSelected(!isSelected);
1190         }
1191     }
1192 
1193     /**
1194      * Scroll menu.
1195      *
1196      * @param <T> the value type
1197      */
1198     public static final class TethysUIFXScrollSubMenu<T>
1199             extends TethysUIFXScrollElement
1200             implements TethysUIScrollSubMenu<T> {
1201         /**
1202          * Parent contextMenu.
1203          */
1204         private final TethysUIFXScrollMenu<T> theContext;
1205 
1206         /**
1207          * The index.
1208          */
1209         private final int theIndex;
1210 
1211         /**
1212          * Associated value.
1213          */
1214         private final TethysUIFXScrollMenu<T> theSubMenu;
1215 
1216         /**
1217          * Constructor.
1218          *
1219          * @param pContext the parent context menu
1220          * @param pName    the name
1221          * @param pGraphic the icon for the menu
1222          */
1223         TethysUIFXScrollSubMenu(final TethysUIFXScrollMenu<T> pContext,
1224                                 final String pName,
1225                                 final TethysUIIcon pGraphic) {
1226             /* Call super-constructor */
1227             super(pName, pGraphic);
1228 
1229             /* Record parameters */
1230             theContext = pContext;
1231 
1232             /* Create the subMenu */
1233             theSubMenu = new TethysUIFXScrollMenu<>(this);
1234 
1235             /* Determine the index */
1236             theIndex = theContext.getItemCount();
1237 
1238             /* Set menu icon */
1239             addMenuIcon();
1240 
1241             /* Handle show menu */
1242             addEventFilter(MouseEvent.MOUSE_ENTERED, e -> {
1243                 theContext.handleActiveMenu(this);
1244                 theSubMenu.showMenuAtPosition(getBorderPane(), Side.RIGHT);
1245             });
1246         }
1247 
1248         /**
1249          * Obtain the parent.
1250          *
1251          * @return the parent
1252          */
1253         TethysUIFXScrollMenu<T> getContext() {
1254             return theContext;
1255         }
1256 
1257         @Override
1258         public TethysUIFXScrollMenu<T> getSubMenu() {
1259             return theSubMenu;
1260         }
1261 
1262         /**
1263          * Obtain the index.
1264          *
1265          * @return the index
1266          */
1267         int getIndex() {
1268             return theIndex;
1269         }
1270 
1271         /**
1272          * Hide the subMenu.
1273          */
1274         void hide() {
1275             theSubMenu.closeMenu();
1276         }
1277 
1278         /**
1279          * Set the number of items in the scrolling portion of the menu.
1280          *
1281          * @param pMaxDisplayItems the maximum number of items to display
1282          * @throws IllegalArgumentException if pMaxDisplayItems is 0 or negative
1283          */
1284         private void setMaxDisplayItems(final int pMaxDisplayItems) {
1285             /* Pass call to subMenu */
1286             theSubMenu.setMaxDisplayItems(pMaxDisplayItems);
1287         }
1288 
1289         /**
1290          * Ensure that this menu is visible immediately the context is displayed.
1291          */
1292         void scrollToMenu() {
1293             theContext.scrollToIndex(theIndex);
1294         }
1295     }
1296 
1297     /**
1298      * Scroll control class.
1299      */
1300     private final class ScrollControl {
1301         /**
1302          * Label.
1303          */
1304         private final Label theLabel;
1305 
1306         /**
1307          * Increment.
1308          */
1309         private final int theIncrement;
1310 
1311         /**
1312          * KickStart Timer.
1313          */
1314         private final Timeline theKickStartTimer;
1315 
1316         /**
1317          * Repeat Timer.
1318          */
1319         private final Timeline theRepeatTimer;
1320 
1321         /**
1322          * Constructor.
1323          *
1324          * @param pIcon      the icon
1325          * @param pIncrement the increment
1326          */
1327         ScrollControl(final Polygon pIcon,
1328                       final int pIncrement) {
1329             /* Create the label */
1330             theLabel = new Label();
1331 
1332             /* Set the icon for the item */
1333             theLabel.setGraphic(pIcon);
1334             theLabel.setContentDisplay(ContentDisplay.GRAPHIC_ONLY);
1335             theLabel.setAlignment(Pos.CENTER);
1336             theLabel.setMaxWidth(Double.MAX_VALUE);
1337             theLabel.getStyleClass().add(STYLE_ITEM);
1338 
1339             /* Store parameters */
1340             theIncrement = pIncrement;
1341 
1342             /* Handle selection */
1343             theLabel.addEventFilter(MouseEvent.MOUSE_CLICKED, e -> processScroll());
1344 
1345             /* Handle mouse enters */
1346             theLabel.addEventHandler(MouseEvent.MOUSE_ENTERED, e -> processMouseEnter());
1347 
1348             /* Handle mouse exits */
1349             theLabel.addEventHandler(MouseEvent.MOUSE_EXITED, e -> processMouseExit());
1350 
1351             /* Create the timers */
1352             theKickStartTimer = new Timeline(new KeyFrame(Duration.millis(TethysUICoreScrollMenu.INITIAL_SCROLLDELAY),
1353                     e -> processScroll()));
1354             theRepeatTimer = new Timeline(new KeyFrame(Duration.millis(TethysUICoreScrollMenu.REPEAT_SCROLLDELAY),
1355                     e -> processScroll()));
1356         }
1357 
1358         /**
1359          * Obtain the label.
1360          *
1361          * @return the label
1362          */
1363         Label getLabel() {
1364             return theLabel;
1365         }
1366 
1367         /**
1368          * Set visibility.
1369          *
1370          * @param pVisible true/false
1371          */
1372         void setVisible(final boolean pVisible) {
1373             theLabel.setVisible(pVisible);
1374         }
1375 
1376         /**
1377          * Process scroll event.
1378          */
1379         private void processScroll() {
1380             /* Request the scroll */
1381             requestScroll(theIncrement);
1382             theRepeatTimer.play();
1383         }
1384 
1385         /**
1386          * Process mouseEnter event.
1387          */
1388         private void processMouseEnter() {
1389             /* Close any children */
1390             closeChildren();
1391 
1392             /* Set no active Item */
1393             clearActiveItem();
1394 
1395             /* Schedule the task */
1396             theKickStartTimer.play();
1397         }
1398 
1399         /**
1400          * Process mouseExit event.
1401          */
1402         private void processMouseExit() {
1403             /* Cancel any timer tasks */
1404             theKickStartTimer.stop();
1405             theRepeatTimer.stop();
1406         }
1407     }
1408 }