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 io.github.tonywasher.joceanus.tethys.api.base.TethysUIConstant;
22  import io.github.tonywasher.joceanus.tethys.api.base.TethysUIEvent;
23  import io.github.tonywasher.joceanus.tethys.api.base.TethysUIIcon;
24  import io.github.tonywasher.joceanus.tethys.api.base.TethysUIIconId;
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.TethysUIScrollToggle;
29  import io.github.tonywasher.joceanus.tethys.core.menu.TethysUICoreScrollMenu;
30  import io.github.tonywasher.joceanus.tethys.javafx.base.TethysUIFXArrowIcon;
31  import io.github.tonywasher.joceanus.tethys.javafx.base.TethysUIFXIcon;
32  import io.github.tonywasher.joceanus.tethys.javafx.base.TethysUIFXUtils;
33  import javafx.animation.KeyFrame;
34  import javafx.animation.Timeline;
35  import javafx.collections.ObservableList;
36  import javafx.event.Event;
37  import javafx.event.EventHandler;
38  import javafx.event.EventType;
39  import javafx.geometry.Dimension2D;
40  import javafx.geometry.Insets;
41  import javafx.geometry.Point2D;
42  import javafx.geometry.Pos;
43  import javafx.geometry.Side;
44  import javafx.scene.Node;
45  import javafx.scene.Scene;
46  import javafx.scene.control.ContentDisplay;
47  import javafx.scene.control.Label;
48  import javafx.scene.input.KeyEvent;
49  import javafx.scene.input.MouseEvent;
50  import javafx.scene.input.ScrollEvent;
51  import javafx.scene.layout.BorderPane;
52  import javafx.scene.layout.VBox;
53  import javafx.scene.shape.Polygon;
54  import javafx.stage.Modality;
55  import javafx.stage.Stage;
56  import javafx.stage.StageStyle;
57  import javafx.util.Duration;
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 (!pState && theActiveMenu == null) {
530             /* fire cancellation event */
531             if (theParentMenu == null) {
532                 theEventManager.fireEvent(TethysUIEvent.WINDOWCLOSED);
533             }
534 
535             /* Close the menu hierarchy if we are currently showing */
536             if (isShowing()) {
537                 closeOnFocusLoss();
538             }
539         }
540     }
541 
542     /**
543      * Handle keyPress.
544      *
545      * @param pEvent the event
546      */
547     private void handleKeyPress(final KeyEvent pEvent) {
548         switch (pEvent.getCode()) {
549             case ESCAPE:
550                 handleEscapeKey();
551                 break;
552             case ENTER:
553                 handleEnterKey();
554                 break;
555             default:
556                 break;
557         }
558     }
559 
560     /**
561      * Handle scroll.
562      *
563      * @param pEvent the event
564      */
565     private void handleScroll(final ScrollEvent pEvent) {
566         /* request the scroll */
567         final double myDelta = pEvent.getDeltaY() / pEvent.getMultiplierY();
568         requestScroll((int) -myDelta);
569 
570         /* Consume the event */
571         pEvent.consume();
572     }
573 
574     /**
575      * Handle activeItem.
576      *
577      * @param pItem the item
578      */
579     void handleActiveItem(final TethysUIFXScrollItem<T> pItem) {
580         /* Close any children */
581         closeChildren();
582 
583         /* Record that we are the active item */
584         theActiveItem = pItem;
585     }
586 
587     /**
588      * Handle activeMenu.
589      *
590      * @param pMenu the menu
591      */
592     void handleActiveMenu(final TethysUIFXScrollSubMenu<T> pMenu) {
593         /* Hide any existing menu that is not us */
594         if (theActiveMenu != null
595                 && theActiveMenu.getIndex() != pMenu.getIndex()) {
596             theActiveMenu.hide();
597         }
598 
599         /* Set no active Item */
600         theActiveItem = null;
601 
602         /* Record active menu */
603         theActiveMenu = pMenu;
604     }
605 
606     @Override
607     public void removeAllItems() {
608         /* Check state */
609         if (isShowing()) {
610             closeMenu();
611         }
612 
613         /* Clear menuItems */
614         theMenuItems.clear();
615         theFirstIndex = 0;
616         needReBuild = true;
617 
618         /* Clear state */
619         theSelectedItem = null;
620     }
621 
622     @Override
623     public boolean isEmpty() {
624         /* Obtain count */
625         return theMenuItems.isEmpty();
626     }
627 
628     /**
629      * Obtain count of menu Items.
630      *
631      * @return the count
632      */
633     protected int getItemCount() {
634         /* Obtain count */
635         return theMenuItems.size();
636     }
637 
638     /**
639      * Ensure item at index will be visible when displayed.
640      *
641      * @param pIndex the index to show
642      */
643     void scrollToIndex(final int pIndex) {
644         /* Show the index */
645         showIndex(pIndex);
646 
647         /* cascade call upwards */
648         if (theParentMenu != null) {
649             theParentMenu.scrollToMenu();
650         }
651     }
652 
653     /**
654      * Ensure index shown.
655      *
656      * @param pIndex the index to show
657      */
658     private void showIndex(final int pIndex) {
659         /* Ignore if index is out of range */
660         final int myCount = theMenuItems.size();
661         if (pIndex < 0
662                 || pIndex >= myCount) {
663             return;
664         }
665 
666         /* If index is above window */
667         if (pIndex < theFirstIndex) {
668             /* Scroll window upwards and return */
669             requestScroll(pIndex - theFirstIndex);
670             return;
671         }
672 
673         /* If index is beyond last visible index */
674         final int myLastIndex = theFirstIndex
675                 + theMaxDisplayItems
676                 - 1;
677         if (myLastIndex < pIndex) {
678             /* Scroll window downwards */
679             requestScroll(pIndex - myLastIndex);
680         }
681     }
682 
683     @Override
684     public TethysUIScrollItem<T> addItem(final T pValue) {
685         /* Use standard name */
686         return addItem(pValue, pValue.toString(), null);
687     }
688 
689     @Override
690     public TethysUIScrollItem<T> addItem(final T pValue,
691                                          final String pName) {
692         /* Use standard name */
693         return addItem(pValue, pName, null);
694     }
695 
696     @Override
697     public TethysUIScrollItem<T> addItem(final T pValue,
698                                          final TethysUIIconId pId) {
699         /* Use standard name */
700         return addItem(pValue, pValue.toString(), TethysUIFXUtils.getIconAtSize(pId, TethysUICoreScrollMenu.ICON_SIZE));
701     }
702 
703     @Override
704     public TethysUIScrollItem<T> addItem(final T pValue,
705                                          final TethysUIIcon pGraphic) {
706         /* Use standard name */
707         return addItem(pValue, pValue.toString(), pGraphic);
708     }
709 
710     @Override
711     public TethysUIScrollItem<T> addNullItem(final String pName) {
712         /* Use given name */
713         return addItem(null, pName, null);
714     }
715 
716     @Override
717     public TethysUIScrollItem<T> addNullItem(final String pName,
718                                              final TethysUIIcon pGraphic) {
719         /* Use given name */
720         return addItem(null, pName, pGraphic);
721     }
722 
723     @Override
724     public TethysUIScrollItem<T> addItem(final T pValue,
725                                          final String pName,
726                                          final TethysUIIcon pGraphic) {
727         /* Check state */
728         if (isShowing()) {
729             throw new IllegalStateException();
730         }
731 
732         /* Create element */
733         final TethysUIFXScrollItem<T> myItem = new TethysUIFXScrollItem<>(this, pValue, pName, pGraphic);
734 
735         /* Add to the list of menuItems */
736         theMenuItems.add(myItem);
737         needReBuild = true;
738         return myItem;
739     }
740 
741     @Override
742     public TethysUIScrollSubMenu<T> addSubMenu(final String pName) {
743         /* Use given name */
744         return addSubMenu(pName, null);
745     }
746 
747     @Override
748     public TethysUIScrollSubMenu<T> addSubMenu(final String pName,
749                                                final TethysUIIcon pGraphic) {
750         /* Check state */
751         if (isShowing()) {
752             throw new IllegalStateException();
753         }
754 
755         /* Create menu */
756         final TethysUIFXScrollSubMenu<T> myMenu = new TethysUIFXScrollSubMenu<>(this, pName, pGraphic);
757 
758         /* Add to the list of menuItems */
759         theMenuItems.add(myMenu);
760         needReBuild = true;
761         return myMenu;
762     }
763 
764     @Override
765     public TethysUIScrollToggle<T> addToggleItem(final T pValue) {
766         /* Use standard name */
767         return addToggleItem(pValue, pValue.toString());
768     }
769 
770     @Override
771     public TethysUIScrollToggle<T> addToggleItem(final T pValue,
772                                                  final String pName) {
773         /* Check state */
774         if (isShowing()) {
775             throw new IllegalStateException();
776         }
777 
778         /* Create element */
779         final TethysUIFXScrollToggle<T> myItem = new TethysUIFXScrollToggle<>(this, pValue, pName);
780 
781         /* Add to the list of menuItems */
782         theMenuItems.add(myItem);
783         needReBuild = true;
784         return myItem;
785     }
786 
787     /**
788      * close child menus.
789      */
790     void closeChildren() {
791         /* Close any active subMenu */
792         if (theActiveMenu != null) {
793             theActiveMenu.hide();
794         }
795         theActiveMenu = null;
796     }
797 
798     /**
799      * Determine size of menu.
800      */
801     private void determineSize() {
802         /* NoOp if we do not need to reBuild the menu */
803         if (!needReBuild) {
804             return;
805         }
806 
807         /* If we have items */
808         if (!theMenuItems.isEmpty()) {
809             /* Access the number of entries and the scroll count */
810             final int myCount = theMenuItems.size();
811             final int myScroll = Math.min(theMaxDisplayItems, myCount);
812 
813             /* Remove all items */
814             theActiveItems.clear();
815 
816             /* If we do not need to scroll */
817             if (myScroll == myCount) {
818                 /* Ensure we have no arrows */
819                 theContainer.setTop(null);
820                 theContainer.setBottom(null);
821 
822                 /* Loop through the items to add */
823                 for (int i = 0; i < myCount; i++) {
824                     /* Add the items */
825                     theActiveItems.add(theMenuItems.get(i).getBorderPane());
826                 }
827 
828                 /* Calculate size of menu */
829                 theStage.show();
830                 theStage.close();
831 
832                 /* Determine the size */
833                 theMenuSize = new Dimension2D(theStage.getWidth(), theStage.getHeight());
834 
835                 /* else need to set up scroll */
836             } else {
837                 /* Add the scrolling items */
838                 theContainer.setTop(theUpItem.getLabel());
839                 theContainer.setBottom(theDownItem.getLabel());
840                 theUpItem.setVisible(true);
841                 theDownItem.setVisible(true);
842 
843                 /* Add ALL items */
844                 for (final TethysUIFXScrollElement myItem : theMenuItems) {
845                     /* Add the items */
846                     theActiveItems.add(myItem.getBorderPane());
847                 }
848 
849                 /* Calculate size of menu */
850                 theStage.show();
851                 theStage.close();
852                 final double myWidth = theStage.getWidth();
853 
854                 /* Remove all items */
855                 theActiveItems.clear();
856 
857                 /* Ensure that the starting index is positive */
858                 if (theFirstIndex < 0) {
859                     theFirstIndex = 0;
860                 }
861 
862                 /* Ensure that the starting point is not too late */
863                 int myMaxIndex = theFirstIndex
864                         + myScroll;
865                 if (myMaxIndex > myCount) {
866                     /* Adjust the first index */
867                     theFirstIndex = myCount
868                             - myScroll;
869                     myMaxIndex = myCount;
870                 }
871 
872                 /* Loop through the items to add */
873                 for (int i = theFirstIndex; i < myMaxIndex; i++) {
874                     /* Add the items */
875                     theActiveItems.add(theMenuItems.get(i).getBorderPane());
876                 }
877 
878                 /* Calculate size of menu */
879                 theStage.show();
880                 theStage.close();
881                 final double myHeight = theStage.getHeight();
882 
883                 /* Set visibility of scroll items */
884                 theUpItem.setVisible(theFirstIndex > 0);
885                 theDownItem.setVisible(myMaxIndex < myCount);
886 
887                 /* Determine the size */
888                 theMenuSize = new Dimension2D(myWidth, myHeight);
889 
890                 /* Fix the width */
891                 theStage.setMinWidth(myWidth);
892             }
893 
894             /* else reset the menuSize */
895         } else {
896             theMenuSize = null;
897         }
898 
899         /* Reset flag */
900         needReBuild = false;
901     }
902 
903     /**
904      * ScrollPlusOne.
905      */
906     protected void scrollPlusOne() {
907         /* If we are already built */
908         if (!needReBuild) {
909             /* Access the number of entries */
910             final int myCount = theMenuItems.size();
911 
912             /* Reset the children */
913             closeChildren();
914 
915             /* Ensure Up item is enabled */
916             theUpItem.setVisible(true);
917 
918             /* Remove the first item */
919             theActiveItems.remove(0);
920 
921             /* Add the final item */
922             final int myLast = theFirstIndex + theMaxDisplayItems;
923             final TethysUIFXScrollElement myItem = theMenuItems.get(myLast);
924             theActiveItems.add(myItem.getBorderPane());
925 
926             /* Adjust down item */
927             theDownItem.setVisible(myLast + 1 < myCount);
928         }
929 
930         /* Adjust first index */
931         theFirstIndex++;
932     }
933 
934     /**
935      * ScrollMinusOne.
936      */
937     protected void scrollMinusOne() {
938         /* If we are already built */
939         if (!needReBuild) {
940             /* Reset the children */
941             closeChildren();
942 
943             /* Ensure Down item is enabled */
944             theDownItem.setVisible(true);
945 
946             /* Remove the last item */
947             theActiveItems.remove(theMaxDisplayItems - 1);
948 
949             /* Add the initial item */
950             final TethysUIFXScrollElement myItem = theMenuItems.get(theFirstIndex - 1);
951             theActiveItems.add(0, myItem.getBorderPane());
952 
953             /* Adjust up item */
954             theUpItem.setVisible(theFirstIndex > 1);
955         }
956 
957         /* Adjust first index */
958         theFirstIndex--;
959     }
960 
961     /**
962      * request scroll.
963      *
964      * @param pDelta the delta to scroll.
965      */
966     void requestScroll(final int pDelta) {
967         /* If this is a scroll downwards */
968         if (pDelta > 0) {
969             /* If we can scroll downwards */
970             final int myCount = theMenuItems.size();
971             final int mySpace = myCount - theFirstIndex - theMaxDisplayItems;
972             int myScroll = Math.min(mySpace, pDelta);
973 
974             /* While we have space */
975             while (myScroll-- > 0) {
976                 /* Scroll downwards */
977                 scrollPlusOne();
978             }
979 
980             /* else scroll upwards if we can */
981         } else if (theFirstIndex > 0) {
982             /* Determine space */
983             int myScroll = Math.min(theFirstIndex, -pDelta);
984 
985             /* While we have space */
986             while (myScroll-- > 0) {
987                 /* Scroll upwards */
988                 scrollMinusOne();
989             }
990         }
991     }
992 
993     /**
994      * Scroll element.
995      */
996     public abstract static class TethysUIFXScrollElement {
997         /**
998          * BorderPane.
999          */
1000         private final BorderPane theBorderPane;
1001 
1002         /**
1003          * The label.
1004          */
1005         private final Label theLabel;
1006 
1007         /**
1008          * The icon label.
1009          */
1010         private final Label theIcon;
1011 
1012         /**
1013          * Constructor.
1014          *
1015          * @param pName    the display name
1016          * @param pGraphic the icon for the item
1017          */
1018         private TethysUIFXScrollElement(final String pName,
1019                                         final TethysUIIcon pGraphic) {
1020             /* Create the borderPane */
1021             theBorderPane = new BorderPane();
1022 
1023             /* Create a Label for the name */
1024             theLabel = new Label();
1025             theLabel.setText(pName);
1026             theLabel.setMaxWidth(Double.MAX_VALUE);
1027 
1028             /* Create a Label for the graphic */
1029             theIcon = new Label();
1030             theIcon.setGraphic(TethysUIFXIcon.getIcon(pGraphic));
1031             theIcon.setMinWidth(TethysUIConstant.DEFAULT_ICONSIZE);
1032 
1033             /* Add the children */
1034             theBorderPane.setLeft(theIcon);
1035             theBorderPane.setCenter(theLabel);
1036 
1037             /* Set style of item */
1038             theBorderPane.getStyleClass().add(STYLE_ITEM);
1039         }
1040 
1041         /**
1042          * Obtain the label.
1043          *
1044          * @return the label
1045          */
1046         BorderPane getBorderPane() {
1047             return theBorderPane;
1048         }
1049 
1050         /**
1051          * Add event filter.
1052          *
1053          * @param <T>     the event type
1054          * @param pEvent  the event
1055          * @param pFilter the filter
1056          */
1057         <T extends Event> void addEventFilter(final EventType<T> pEvent,
1058                                               final EventHandler<? super T> pFilter) {
1059             theBorderPane.addEventFilter(pEvent, pFilter);
1060         }
1061 
1062         /**
1063          * Obtain the text.
1064          *
1065          * @return the text
1066          */
1067         public String getText() {
1068             return theLabel.getText();
1069         }
1070 
1071         /**
1072          * Set the graphic.
1073          *
1074          * @param pGraphic the graphic
1075          */
1076         protected void setIcon(final TethysUIFXIcon pGraphic) {
1077             theIcon.setGraphic(TethysUIFXIcon.getIcon(pGraphic));
1078         }
1079 
1080         /**
1081          * Add Menu icon.
1082          */
1083         protected void addMenuIcon() {
1084             final Label myLabel = new Label();
1085             myLabel.setGraphic(TethysUIFXArrowIcon.RIGHT.getArrow());
1086             theBorderPane.setRight(myLabel);
1087         }
1088     }
1089 
1090     /**
1091      * Scroll item.
1092      *
1093      * @param <T> the value type
1094      */
1095     protected static class TethysUIFXScrollItem<T>
1096             extends TethysUIFXScrollElement
1097             implements TethysUIScrollItem<T> {
1098         /**
1099          * Parent context menu.
1100          */
1101         private final TethysUIFXScrollMenu<T> theContext;
1102 
1103         /**
1104          * The index.
1105          */
1106         private final int theIndex;
1107 
1108         /**
1109          * Associated value.
1110          */
1111         private final T theValue;
1112 
1113         /**
1114          * Constructor.
1115          *
1116          * @param pContext the parent context menu
1117          * @param pValue   the value
1118          * @param pName    the display name
1119          * @param pGraphic the icon for the item
1120          */
1121         protected TethysUIFXScrollItem(final TethysUIFXScrollMenu<T> pContext,
1122                                        final T pValue,
1123                                        final String pName,
1124                                        final TethysUIIcon pGraphic) {
1125             /* Call super-constructor */
1126             super(pName, pGraphic);
1127 
1128             /* Record parameters */
1129             theContext = pContext;
1130             theValue = pValue;
1131 
1132             /* Determine the index */
1133             theIndex = theContext.getItemCount();
1134 
1135             /* Handle removal of subMenus */
1136             addEventFilter(MouseEvent.MOUSE_ENTERED, e -> theContext.handleActiveItem(this));
1137 
1138             /* Handle selection */
1139             addEventFilter(MouseEvent.MOUSE_CLICKED, e -> theContext.setSelectedItem(this));
1140         }
1141 
1142         @Override
1143         public T getValue() {
1144             return theValue;
1145         }
1146 
1147         @Override
1148         public void scrollToItem() {
1149             theContext.scrollToIndex(theIndex);
1150         }
1151     }
1152 
1153     /**
1154      * Scroll item.
1155      *
1156      * @param <T> the value type
1157      */
1158     private static final class TethysUIFXScrollToggle<T>
1159             extends TethysUIFXScrollItem<T>
1160             implements TethysUIScrollToggle<T> {
1161         /**
1162          * Selected state.
1163          */
1164         private boolean isSelected;
1165 
1166         /**
1167          * Constructor.
1168          *
1169          * @param pContext the parent context menu
1170          * @param pValue   the value
1171          * @param pName    the display name
1172          */
1173         TethysUIFXScrollToggle(final TethysUIFXScrollMenu<T> pContext,
1174                                final T pValue,
1175                                final String pName) {
1176             /* Call super-constructor */
1177             super(pContext, pValue, pName, null);
1178         }
1179 
1180         @Override
1181         public boolean isSelected() {
1182             return isSelected;
1183         }
1184 
1185         @Override
1186         public void setSelected(final boolean pSelected) {
1187             isSelected = pSelected;
1188             setIcon(isSelected
1189                     ? TethysUIFXUtils.getIconAtSize(TethysUIScrollIcon.CHECKMARK, TethysUIConstant.DEFAULT_ICONSIZE)
1190                     : null);
1191         }
1192 
1193         @Override
1194         public void toggleSelected() {
1195             setSelected(!isSelected);
1196         }
1197     }
1198 
1199     /**
1200      * Scroll menu.
1201      *
1202      * @param <T> the value type
1203      */
1204     public static final class TethysUIFXScrollSubMenu<T>
1205             extends TethysUIFXScrollElement
1206             implements TethysUIScrollSubMenu<T> {
1207         /**
1208          * Parent contextMenu.
1209          */
1210         private final TethysUIFXScrollMenu<T> theContext;
1211 
1212         /**
1213          * The index.
1214          */
1215         private final int theIndex;
1216 
1217         /**
1218          * Associated value.
1219          */
1220         private final TethysUIFXScrollMenu<T> theSubMenu;
1221 
1222         /**
1223          * Constructor.
1224          *
1225          * @param pContext the parent context menu
1226          * @param pName    the name
1227          * @param pGraphic the icon for the menu
1228          */
1229         TethysUIFXScrollSubMenu(final TethysUIFXScrollMenu<T> pContext,
1230                                 final String pName,
1231                                 final TethysUIIcon pGraphic) {
1232             /* Call super-constructor */
1233             super(pName, pGraphic);
1234 
1235             /* Record parameters */
1236             theContext = pContext;
1237 
1238             /* Create the subMenu */
1239             theSubMenu = new TethysUIFXScrollMenu<>(this);
1240 
1241             /* Determine the index */
1242             theIndex = theContext.getItemCount();
1243 
1244             /* Set menu icon */
1245             addMenuIcon();
1246 
1247             /* Handle show menu */
1248             addEventFilter(MouseEvent.MOUSE_ENTERED, e -> {
1249                 theContext.handleActiveMenu(this);
1250                 theSubMenu.showMenuAtPosition(getBorderPane(), Side.RIGHT);
1251             });
1252         }
1253 
1254         /**
1255          * Obtain the parent.
1256          *
1257          * @return the parent
1258          */
1259         TethysUIFXScrollMenu<T> getContext() {
1260             return theContext;
1261         }
1262 
1263         @Override
1264         public TethysUIFXScrollMenu<T> getSubMenu() {
1265             return theSubMenu;
1266         }
1267 
1268         /**
1269          * Obtain the index.
1270          *
1271          * @return the index
1272          */
1273         int getIndex() {
1274             return theIndex;
1275         }
1276 
1277         /**
1278          * Hide the subMenu.
1279          */
1280         void hide() {
1281             theSubMenu.closeMenu();
1282         }
1283 
1284         /**
1285          * Set the number of items in the scrolling portion of the menu.
1286          *
1287          * @param pMaxDisplayItems the maximum number of items to display
1288          * @throws IllegalArgumentException if pMaxDisplayItems is 0 or negative
1289          */
1290         private void setMaxDisplayItems(final int pMaxDisplayItems) {
1291             /* Pass call to subMenu */
1292             theSubMenu.setMaxDisplayItems(pMaxDisplayItems);
1293         }
1294 
1295         /**
1296          * Ensure that this menu is visible immediately the context is displayed.
1297          */
1298         void scrollToMenu() {
1299             theContext.scrollToIndex(theIndex);
1300         }
1301     }
1302 
1303     /**
1304      * Scroll control class.
1305      */
1306     private final class ScrollControl {
1307         /**
1308          * Label.
1309          */
1310         private final Label theLabel;
1311 
1312         /**
1313          * Increment.
1314          */
1315         private final int theIncrement;
1316 
1317         /**
1318          * KickStart Timer.
1319          */
1320         private final Timeline theKickStartTimer;
1321 
1322         /**
1323          * Repeat Timer.
1324          */
1325         private final Timeline theRepeatTimer;
1326 
1327         /**
1328          * Constructor.
1329          *
1330          * @param pIcon      the icon
1331          * @param pIncrement the increment
1332          */
1333         ScrollControl(final Polygon pIcon,
1334                       final int pIncrement) {
1335             /* Create the label */
1336             theLabel = new Label();
1337 
1338             /* Set the icon for the item */
1339             theLabel.setGraphic(pIcon);
1340             theLabel.setContentDisplay(ContentDisplay.GRAPHIC_ONLY);
1341             theLabel.setAlignment(Pos.CENTER);
1342             theLabel.setMaxWidth(Double.MAX_VALUE);
1343             theLabel.getStyleClass().add(STYLE_ITEM);
1344 
1345             /* Store parameters */
1346             theIncrement = pIncrement;
1347 
1348             /* Handle selection */
1349             theLabel.addEventFilter(MouseEvent.MOUSE_CLICKED, e -> processScroll());
1350 
1351             /* Handle mouse enters */
1352             theLabel.addEventHandler(MouseEvent.MOUSE_ENTERED, e -> processMouseEnter());
1353 
1354             /* Handle mouse exits */
1355             theLabel.addEventHandler(MouseEvent.MOUSE_EXITED, e -> processMouseExit());
1356 
1357             /* Create the timers */
1358             theKickStartTimer = new Timeline(new KeyFrame(Duration.millis(TethysUICoreScrollMenu.INITIAL_SCROLLDELAY),
1359                     e -> processScroll()));
1360             theRepeatTimer = new Timeline(new KeyFrame(Duration.millis(TethysUICoreScrollMenu.REPEAT_SCROLLDELAY),
1361                     e -> processScroll()));
1362         }
1363 
1364         /**
1365          * Obtain the label.
1366          *
1367          * @return the label
1368          */
1369         Label getLabel() {
1370             return theLabel;
1371         }
1372 
1373         /**
1374          * Set visibility.
1375          *
1376          * @param pVisible true/false
1377          */
1378         void setVisible(final boolean pVisible) {
1379             theLabel.setVisible(pVisible);
1380         }
1381 
1382         /**
1383          * Process scroll event.
1384          */
1385         private void processScroll() {
1386             /* Request the scroll */
1387             requestScroll(theIncrement);
1388             theRepeatTimer.play();
1389         }
1390 
1391         /**
1392          * Process mouseEnter event.
1393          */
1394         private void processMouseEnter() {
1395             /* Close any children */
1396             closeChildren();
1397 
1398             /* Set no active Item */
1399             clearActiveItem();
1400 
1401             /* Schedule the task */
1402             theKickStartTimer.play();
1403         }
1404 
1405         /**
1406          * Process mouseExit event.
1407          */
1408         private void processMouseExit() {
1409             /* Cancel any timer tasks */
1410             theKickStartTimer.stop();
1411             theRepeatTimer.stop();
1412         }
1413     }
1414 }