1 /*
2 * Copyright (c) 2010, 2017, Oracle and/or its affiliates. All rights reserved.
3 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
4 *
5 * This code is free software; you can redistribute it and/or modify it
6 * under the terms of the GNU General Public License version 2 only, as
7 * published by the Free Software Foundation. Oracle designates this
8 * particular file as subject to the "Classpath" exception as provided
9 * by Oracle in the LICENSE file that accompanied this code.
10 *
11 * This code is distributed in the hope that it will be useful, but WITHOUT
12 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
13 * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
14 * version 2 for more details (a copy is included in the LICENSE file that
15 * accompanied this code).
16 *
17 * You should have received a copy of the GNU General Public License version
18 * 2 along with this work; if not, write to the Free Software Foundation,
19 * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
20 *
21 * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
22 * or visit www.oracle.com if you need additional information or have any
23 * questions.
24 */
25
26 package javafx.scene.control.skin;
27
28 import static com.sun.javafx.FXPermissions.ACCESS_WINDOW_LIST_PERMISSION;
29
30 import com.sun.javafx.scene.traversal.Direction;
31 import javafx.css.converter.EnumConverter;
32 import javafx.css.converter.SizeConverter;
33 import com.sun.javafx.scene.control.MenuBarButton;
34 import com.sun.javafx.scene.control.skin.Utils;
35 import com.sun.javafx.scene.traversal.ParentTraversalEngine;
36 import javafx.beans.InvalidationListener;
37 import javafx.beans.property.DoubleProperty;
38 import javafx.beans.property.ObjectProperty;
39 import javafx.beans.property.ReadOnlyProperty;
40 import javafx.beans.value.ChangeListener;
41 import javafx.beans.value.WeakChangeListener;
42 import javafx.beans.value.WritableValue;
43 import javafx.collections.ListChangeListener;
44 import javafx.collections.MapChangeListener;
45 import javafx.collections.ObservableList;
46 import javafx.css.CssMetaData;
47 import javafx.css.Styleable;
48 import javafx.css.StyleableDoubleProperty;
49 import javafx.css.StyleableObjectProperty;
50 import javafx.css.StyleableProperty;
51 import javafx.event.ActionEvent;
52 import javafx.event.EventHandler;
53 import javafx.event.WeakEventHandler;
54 import javafx.geometry.Bounds;
55 import javafx.geometry.NodeOrientation;
56 import javafx.geometry.Pos;
57 import javafx.scene.AccessibleAttribute;
58 import javafx.scene.Node;
59 import javafx.scene.Scene;
60 import javafx.scene.control.Control;
61 import javafx.scene.control.CustomMenuItem;
62 import javafx.scene.control.Menu;
63 import javafx.scene.control.MenuBar;
64 import javafx.scene.control.MenuButton;
65 import javafx.scene.control.MenuItem;
66 import javafx.scene.control.SeparatorMenuItem;
67 import javafx.scene.control.Skin;
68 import javafx.scene.control.SkinBase;
69 import javafx.scene.input.KeyCombination;
70 import javafx.scene.input.KeyEvent;
71 import javafx.scene.input.MouseEvent;
72 import javafx.scene.layout.HBox;
73 import javafx.stage.Stage;
74
75 import static javafx.scene.input.KeyCode.*;
76
77 import java.lang.ref.Reference;
78 import java.lang.ref.WeakReference;
79 import java.util.ArrayList;
80 import java.util.Collections;
81 import java.util.Iterator;
82 import java.util.List;
83 import java.util.Map;
84 import java.util.Optional;
85 import java.util.WeakHashMap;
86
87 import com.sun.javafx.menu.MenuBase;
88 import com.sun.javafx.scene.ParentHelper;
89 import com.sun.javafx.scene.SceneHelper;
90 import com.sun.javafx.scene.control.GlobalMenuAdapter;
91 import com.sun.javafx.tk.Toolkit;
92 import java.util.function.Predicate;
93 import javafx.stage.Window;
94 import javafx.util.Pair;
95
96 import java.security.AccessController;
97 import java.security.PrivilegedAction;
98
99 /**
100 * Default skin implementation for the {@link MenuBar} control. In essence it is
101 * a simple toolbar. For the time being there is no overflow behavior and we just
102 * hide nodes which fall outside the bounds.
103 *
104 * @see MenuBar
105 * @since 9
106 */
107 public class MenuBarSkin extends SkinBase<MenuBar> {
108
109 private static final ObservableList<Window> stages;
110
111 static {
112 final Predicate<Window> findStage = (w) -> w instanceof Stage;
113 ObservableList<Window> windows = AccessController.doPrivileged(
114 (PrivilegedAction<ObservableList<Window>>) () -> Window.getWindows(),
115 null,
116 ACCESS_WINDOW_LIST_PERMISSION);
117 stages = windows.filtered(findStage);
118 }
119
120 /***************************************************************************
121 * *
122 * Private fields *
123 * *
124 **************************************************************************/
125
126 private final HBox container;
127
128 // represents the currently _open_ menu
129 private Menu openMenu;
130 private MenuBarButton openMenuButton;
131
132 // represents the currently _focused_ menu. If openMenu is non-null, this should equal
133 // openMenu. If openMenu is null, this can be any menu in the menu bar.
134 private Menu focusedMenu;
135 private int focusedMenuIndex = -1;
136
137 private static WeakHashMap<Stage, Reference<MenuBarSkin>> systemMenuMap;
138 private static List<MenuBase> wrappedDefaultMenus = new ArrayList<>();
139 private static Stage currentMenuBarStage;
140 private List<MenuBase> wrappedMenus;
141
142 private WeakEventHandler<KeyEvent> weakSceneKeyEventHandler;
143 private WeakEventHandler<MouseEvent> weakSceneMouseEventHandler;
144 private WeakEventHandler<KeyEvent> weakSceneAltKeyEventHandler;
145 private WeakChangeListener<Boolean> weakWindowFocusListener;
146 private WeakChangeListener<Window> weakWindowSceneListener;
147 private EventHandler<KeyEvent> keyEventHandler;
148 private EventHandler<KeyEvent> altKeyEventHandler;
149 private EventHandler<MouseEvent> mouseEventHandler;
150 private ChangeListener<Boolean> menuBarFocusedPropertyListener;
151 private ChangeListener<Scene> sceneChangeListener;
152 private ChangeListener<Boolean> menuVisibilityChangeListener;
153
154 private boolean pendingDismiss = false;
155
156 private boolean altKeyPressed = false;
157
158
159 /***************************************************************************
160 * *
161 * Listeners / Callbacks *
162 * *
163 **************************************************************************/
164
165 // RT-20411 : reset menu selected/focused state
166 private EventHandler<ActionEvent> menuActionEventHandler = t -> {
167 if (t.getSource() instanceof CustomMenuItem) {
168 // RT-29614 If CustomMenuItem hideOnClick is false, dont hide
169 CustomMenuItem cmi = (CustomMenuItem)t.getSource();
170 if (!cmi.isHideOnClick()) return;
171 }
172 unSelectMenus();
173 };
174
175 private ListChangeListener<MenuItem> menuItemListener = (c) -> {
176 while (c.next()) {
177 for (MenuItem mi : c.getAddedSubList()) {
178 updateActionListeners(mi, true);
179 }
180 for (MenuItem mi: c.getRemoved()) {
181 updateActionListeners(mi, false);
182 }
183 }
184 };
185
186 Runnable firstMenuRunnable = new Runnable() {
187 public void run() {
188 /*
189 ** check that this menubar's container has contents,
190 ** and that the first item is a MenuButton....
191 ** otherwise the transfer is off!
192 */
193 if (container.getChildren().size() > 0) {
194 if (container.getChildren().get(0) instanceof MenuButton) {
195 // container.getChildren().get(0).requestFocus();
196 if (focusedMenuIndex != 0) {
197 unSelectMenus();
198 menuModeStart(0);
199 openMenuButton = ((MenuBarButton)container.getChildren().get(0));
200 // openMenu = getSkinnable().getMenus().get(0);
201 openMenuButton.setHover();
202 }
203 else {
204 unSelectMenus();
205 }
206 }
207 }
208 }
209 };
210
211
212
213 /***************************************************************************
214 * *
215 * Constructors *
216 * *
217 **************************************************************************/
218
219 /**
220 * Creates a new MenuBarSkin instance, installing the necessary child
221 * nodes into the Control {@link Control#getChildren() children} list, as
222 * well as the necessary input mappings for handling key, mouse, etc events.
223 *
224 * @param control The control that this skin should be installed onto.
225 */
226 public MenuBarSkin(final MenuBar control) {
227 super(control);
228
229 container = new HBox();
230 container.getStyleClass().add("container");
231 getChildren().add(container);
232
233 // Key navigation
234 keyEventHandler = event -> {
235 // process right left and may be tab key events
236 if (focusedMenu != null) {
237 switch (event.getCode()) {
238 case LEFT: {
239 boolean isRTL = control.getEffectiveNodeOrientation() == NodeOrientation.RIGHT_TO_LEFT;
240 if (control.getScene().getWindow().isFocused()) {
241 if (openMenu != null && !openMenu.isShowing()) {
242 if (isRTL) {
243 moveToMenu(Direction.NEXT, false); // just move the selection bar
244 } else {
245 moveToMenu(Direction.PREVIOUS, false); // just move the selection bar
246 }
247 event.consume();
248 return;
249 }
250 if (isRTL) {
251 moveToMenu(Direction.NEXT, true);
252 } else {
253 moveToMenu(Direction.PREVIOUS, true);
254 }
255 }
256 event.consume();
257 break;
258 }
259 case RIGHT:
260 {
261 boolean isRTL = control.getEffectiveNodeOrientation() == NodeOrientation.RIGHT_TO_LEFT;
262 if (control.getScene().getWindow().isFocused()) {
263 if (openMenu != null && !openMenu.isShowing()) {
264 if (isRTL) {
265 moveToMenu(Direction.PREVIOUS, false); // just move the selection bar
266 } else {
267 moveToMenu(Direction.NEXT, false); // just move the selection bar
268 }
269 event.consume();
270 return;
271 }
272 if (isRTL) {
273 moveToMenu(Direction.PREVIOUS, true);
274 } else {
275 moveToMenu(Direction.NEXT, true);
276 }
277 }
278 event.consume();
279 break;
280 }
281 case DOWN:
282 //case SPACE:
283 //case ENTER:
284 // RT-18859: Doing nothing for space and enter
285 if (control.getScene().getWindow().isFocused()) {
286 if (focusedMenuIndex != -1) {
287 Menu menuToOpen = getSkinnable().getMenus().get(focusedMenuIndex);
288 showMenu(menuToOpen, true);
289 event.consume();
290 }
291 }
292 break;
293 case ESCAPE:
294 unSelectMenus();
295 event.consume();
296 break;
297 default:
298 break;
299 }
300 }
301 };
302 menuBarFocusedPropertyListener = (ov, t, t1) -> {
303 if (t1) {
304 // RT-23147 when MenuBar's focusTraversable is true the first
305 // menu will visually indicate focus
306 unSelectMenus();
307 menuModeStart(0);
308 openMenuButton = ((MenuBarButton)container.getChildren().get(0));
309 setFocusedMenuIndex(0);
310 openMenuButton.setHover();
311 } else {
312 unSelectMenus();
313 }
314 };
315 weakSceneKeyEventHandler = new WeakEventHandler<KeyEvent>(keyEventHandler);
316 Utils.executeOnceWhenPropertyIsNonNull(control.sceneProperty(), (Scene scene) -> {
317 scene.addEventFilter(KeyEvent.KEY_PRESSED, weakSceneKeyEventHandler);
318 });
319
320 // When we click else where in the scene - menu selection should be cleared.
321 mouseEventHandler = t -> {
322 Bounds containerScreenBounds = container.localToScreen(container.getLayoutBounds());
323 if (containerScreenBounds == null || !containerScreenBounds.contains(t.getScreenX(), t.getScreenY())) {
324 unSelectMenus();
325 }
326 };
327 weakSceneMouseEventHandler = new WeakEventHandler<MouseEvent>(mouseEventHandler);
328 Utils.executeOnceWhenPropertyIsNonNull(control.sceneProperty(), (Scene scene) -> {
329 scene.addEventFilter(MouseEvent.MOUSE_CLICKED, weakSceneMouseEventHandler);
330 });
331
332 weakWindowFocusListener = new WeakChangeListener<Boolean>((ov, t, t1) -> {
333 if (!t1) {
334 unSelectMenus();
335 }
336 });
337 // When the parent window looses focus - menu selection should be cleared
338 Utils.executeOnceWhenPropertyIsNonNull(control.sceneProperty(), (Scene scene) -> {
339 if (scene.getWindow() != null) {
340 scene.getWindow().focusedProperty().addListener(weakWindowFocusListener);
341 } else {
342 ChangeListener<Window> sceneWindowListener = (observable, oldValue, newValue) -> {
343 if (oldValue != null)
344 oldValue.focusedProperty().removeListener(weakWindowFocusListener);
345 if (newValue != null)
346 newValue.focusedProperty().addListener(weakWindowFocusListener);
347 };
348 weakWindowSceneListener = new WeakChangeListener<>(sceneWindowListener);
349 scene.windowProperty().addListener(weakWindowSceneListener);
350 }
351 });
352
353 menuVisibilityChangeListener = (ov, t, t1) -> {
354 rebuildUI();
355 };
356
357 rebuildUI();
358 control.getMenus().addListener((ListChangeListener<Menu>) c -> {
359 rebuildUI();
360 });
361
362 if (Toolkit.getToolkit().getSystemMenu().isSupported()) {
363 control.useSystemMenuBarProperty().addListener(valueModel -> {
364 rebuildUI();
365 });
366 }
367
368 // When the mouse leaves the menu, the last hovered item should lose
369 // it's focus so that it is no longer selected. This code returns focus
370 // to the MenuBar itself, such that keyboard navigation can continue.
371 // fix RT-12254 : menu bar should not request focus on mouse exit.
372 // addEventFilter(MouseEvent.MOUSE_EXITED, new EventHandler<MouseEvent>() {
373 // @Override
374 // public void handle(MouseEvent event) {
375 // requestFocus();
376 // }
377 // });
378
379 /*
380 ** add an accelerator for F10 on windows and ctrl+F10 on mac/linux
381 ** pressing f10 will select the first menu button on a menubar
382 */
383 final KeyCombination acceleratorKeyCombo;
384 if (com.sun.javafx.util.Utils.isMac()) {
385 acceleratorKeyCombo = KeyCombination.keyCombination("ctrl+F10");
386 } else {
387 acceleratorKeyCombo = KeyCombination.keyCombination("F10");
388 }
389
390 altKeyEventHandler = e -> {
391 if (e.getEventType() == KeyEvent.KEY_PRESSED) {
392 // Clear menu selection when ALT is pressed by itself
393 altKeyPressed = false;
394 if (e.getCode() == ALT && !e.isConsumed()) {
395 if (focusedMenuIndex == -1) {
396 altKeyPressed = true;
397 }
398 unSelectMenus();
399 }
400 } else if (e.getEventType() == KeyEvent.KEY_RELEASED) {
401 // Put focus on the first menu when ALT is released
402 // directly after being pressed by itself
403 if (altKeyPressed && e.getCode() == ALT && !e.isConsumed()) {
404 firstMenuRunnable.run();
405 }
406 altKeyPressed = false;
407 }
408 };
409 weakSceneAltKeyEventHandler = new WeakEventHandler<>(altKeyEventHandler);
410
411 Utils.executeOnceWhenPropertyIsNonNull(control.sceneProperty(), (Scene scene) -> {
412 scene.getAccelerators().put(acceleratorKeyCombo, firstMenuRunnable);
413 scene.addEventHandler(KeyEvent.ANY, weakSceneAltKeyEventHandler);
414 });
415
416 ParentTraversalEngine engine = new ParentTraversalEngine(getSkinnable());
417 engine.addTraverseListener((node, bounds) -> {
418 if (openMenu != null) openMenu.hide();
419 setFocusedMenuIndex(0);
420 });
421 ParentHelper.setTraversalEngine(getSkinnable(), engine);
422
423 control.sceneProperty().addListener((ov, t, t1) -> {
424 // remove event handlers / filters from the old scene (t)
425 if (t != null) {
426 if (weakSceneKeyEventHandler != null) {
427 t.removeEventFilter(KeyEvent.KEY_PRESSED, weakSceneKeyEventHandler);
428 }
429 if (weakSceneMouseEventHandler != null) {
430 t.removeEventFilter(MouseEvent.MOUSE_CLICKED, weakSceneMouseEventHandler);
431 }
432 if (weakSceneAltKeyEventHandler != null) {
433 t.removeEventHandler(KeyEvent.ANY, weakSceneAltKeyEventHandler);
434 }
435 }
436
437 /**
438 * remove the f10 accelerator from the old scene
439 * add it to the new scene
440 */
441 if (t != null) {
442 t.getAccelerators().remove(acceleratorKeyCombo);
443 }
444 if (t1 != null ) {
445 t1.getAccelerators().put(acceleratorKeyCombo, firstMenuRunnable);
446 }
447 });
448 }
449
450 private void showMenu(Menu menu) {
451 showMenu(menu, false);
452 }
453
454 private void showMenu(Menu menu, boolean selectFirstItem) {
455 // hide the currently visible menu, and move to the next one
456 if (openMenu == menu) return;
457 if (openMenu != null) {
458 openMenu.hide();
459 }
460
461 openMenu = menu;
462 if (!menu.isShowing() && !isMenuEmpty(menu)) {
463 if (selectFirstItem) {
464 // put selection / focus on first item in menu
465 MenuButton menuButton = getNodeForMenu(focusedMenuIndex);
466 Skin<?> skin = menuButton.getSkin();
467 if (skin instanceof MenuButtonSkinBase) {
468 ((MenuButtonSkinBase)skin).requestFocusOnFirstMenuItem();
469 }
470 }
471
472 openMenu.show();
473 }
474 }
475
476 private void setFocusedMenuIndex(int index) {
477 this.focusedMenuIndex = index;
478 focusedMenu = index == -1 ? null : getSkinnable().getMenus().get(index);
479
480 if (focusedMenu != null && focusedMenuIndex != -1) {
481 openMenuButton = (MenuBarButton)container.getChildren().get(focusedMenuIndex);
482 openMenuButton.setHover();
483 }
484 }
485
486
487
488 /***************************************************************************
489 * *
490 * Static methods *
491 * *
492 **************************************************************************/
493
494 // RT-22480: This is intended as private API for SceneBuilder,
495 // pending fix for RT-19857: Keeping menu in the Mac menu bar when
496 // there is no more stage
497 /**
498 * Set the default system menu bar. This allows an application to keep menu
499 * in the system menu bar after the last Window is closed.
500 * @param menuBar the menu bar
501 */
502 public static void setDefaultSystemMenuBar(final MenuBar menuBar) {
503 if (Toolkit.getToolkit().getSystemMenu().isSupported()) {
504 wrappedDefaultMenus.clear();
505 for (Menu menu : menuBar.getMenus()) {
506 wrappedDefaultMenus.add(GlobalMenuAdapter.adapt(menu));
507 }
508 menuBar.getMenus().addListener((ListChangeListener<Menu>) c -> {
509 wrappedDefaultMenus.clear();
510 for (Menu menu : menuBar.getMenus()) {
511 wrappedDefaultMenus.add(GlobalMenuAdapter.adapt(menu));
512 }
513 });
514 }
515 }
516
517 private static MenuBarSkin getMenuBarSkin(Stage stage) {
518 if (systemMenuMap == null) return null;
519 Reference<MenuBarSkin> skinRef = systemMenuMap.get(stage);
520 return skinRef == null ? null : skinRef.get();
521 }
522
523 private static void setSystemMenu(Stage stage) {
524 if (stage != null && stage.isFocused()) {
525 while (stage != null && stage.getOwner() instanceof Stage) {
526 MenuBarSkin skin = getMenuBarSkin(stage);
527 if (skin != null && skin.wrappedMenus != null) {
528 break;
529 } else {
530 // This is a secondary stage (dialog) that doesn't
531 // have own menu bar.
532 //
533 // Continue looking for a menu bar in the parent stage.
534 stage = (Stage)stage.getOwner();
535 }
536 }
537 } else {
538 stage = null;
539 }
540
541 if (stage != currentMenuBarStage) {
542 List<MenuBase> menuList = null;
543 if (stage != null) {
544 MenuBarSkin skin = getMenuBarSkin(stage);
545 if (skin != null) {
546 menuList = skin.wrappedMenus;
547 }
548 }
549 if (menuList == null) {
550 menuList = wrappedDefaultMenus;
551 }
552 Toolkit.getToolkit().getSystemMenu().setMenus(menuList);
553 currentMenuBarStage = stage;
554 }
555 }
556
557 private static void initSystemMenuBar() {
558 systemMenuMap = new WeakHashMap<>();
559
560 final InvalidationListener focusedStageListener = ov -> {
561 setSystemMenu((Stage)((ReadOnlyProperty<?>)ov).getBean());
562 };
563
564 for (Window stage : stages) {
565 stage.focusedProperty().addListener(focusedStageListener);
566 }
567 stages.addListener((ListChangeListener<Window>) c -> {
568 while (c.next()) {
569 for (Window stage : c.getRemoved()) {
570 stage.focusedProperty().removeListener(focusedStageListener);
571 }
572 for (Window stage : c.getAddedSubList()) {
573 stage.focusedProperty().addListener(focusedStageListener);
574 setSystemMenu((Stage) stage);
575 }
576 }
577 });
578 }
579
580
581
582 /***************************************************************************
583 * *
584 * Properties *
585 * *
586 **************************************************************************/
587
588 /**
589 * Specifies the spacing between menu buttons on the MenuBar.
590 */
591 // --- spacing
592 private DoubleProperty spacing;
593 public final void setSpacing(double value) {
594 spacingProperty().set(snapSpaceX(value));
595 }
596
597 public final double getSpacing() {
598 return spacing == null ? 0.0 : snapSpaceX(spacing.get());
599 }
600
601 public final DoubleProperty spacingProperty() {
602 if (spacing == null) {
603 spacing = new StyleableDoubleProperty() {
604
605 @Override
606 protected void invalidated() {
607 final double value = get();
608 container.setSpacing(value);
609 }
610
611 @Override
612 public Object getBean() {
613 return MenuBarSkin.this;
614 }
615
616 @Override
617 public String getName() {
618 return "spacing";
619 }
620
621 @Override
622 public CssMetaData<MenuBar,Number> getCssMetaData() {
623 return SPACING;
624 }
625 };
626 }
627 return spacing;
628 }
629
630 /**
631 * Specifies the alignment of the menu buttons inside the MenuBar (by default
632 * it is Pos.TOP_LEFT).
633 */
634 // --- container alignment
635 private ObjectProperty<Pos> containerAlignment;
636 public final void setContainerAlignment(Pos value) {
637 containerAlignmentProperty().set(value);
638 }
639
640 public final Pos getContainerAlignment() {
641 return containerAlignment == null ? Pos.TOP_LEFT : containerAlignment.get();
642 }
643
644 public final ObjectProperty<Pos> containerAlignmentProperty() {
645 if (containerAlignment == null) {
646 containerAlignment = new StyleableObjectProperty<Pos>(Pos.TOP_LEFT) {
647
648 @Override
649 public void invalidated() {
650 final Pos value = get();
651 container.setAlignment(value);
652 }
653
654 @Override
655 public Object getBean() {
656 return MenuBarSkin.this;
657 }
658
659 @Override
660 public String getName() {
661 return "containerAlignment";
662 }
663
664 @Override
665 public CssMetaData<MenuBar,Pos> getCssMetaData() {
666 return ALIGNMENT;
667 }
668 };
669 }
670 return containerAlignment;
671 }
672
673
674
675 /***************************************************************************
676 * *
677 * Public API *
678 * *
679 **************************************************************************/
680
681 /** {@inheritDoc} */
682 @Override public void dispose() {
683 cleanUpSystemMenu();
684 // call super.dispose last since it sets control to null
685 super.dispose();
686 }
687
688 // Return empty insets when "container" is empty, which happens
689 // when using the system menu bar.
690
691 /** {@inheritDoc} */
692 @Override protected double snappedTopInset() {
693 return container.getChildren().isEmpty() ? 0 : super.snappedTopInset();
694 }
695 /** {@inheritDoc} */
696 @Override protected double snappedBottomInset() {
697 return container.getChildren().isEmpty() ? 0 : super.snappedBottomInset();
698 }
699 /** {@inheritDoc} */
700 @Override protected double snappedLeftInset() {
701 return container.getChildren().isEmpty() ? 0 : super.snappedLeftInset();
702 }
703 /** {@inheritDoc} */
704 @Override protected double snappedRightInset() {
705 return container.getChildren().isEmpty() ? 0 : super.snappedRightInset();
706 }
707
708 /**
709 * Layout the menu bar. This is a simple horizontal layout like an hbox.
710 * Any menu items which don't fit into it will simply be made invisible.
711 */
712 /** {@inheritDoc} */
713 @Override protected void layoutChildren(final double x, final double y,
714 final double w, final double h) {
715 // layout the menus one after another
716 container.resizeRelocate(x, y, w, h);
717 }
718
719 /** {@inheritDoc} */
720 @Override protected double computeMinWidth(double height, double topInset, double rightInset, double bottomInset, double leftInset) {
721 return container.minWidth(height) + snappedLeftInset() + snappedRightInset();
722 }
723
724 /** {@inheritDoc} */
725 @Override protected double computePrefWidth(double height, double topInset, double rightInset, double bottomInset, double leftInset) {
726 return container.prefWidth(height) + snappedLeftInset() + snappedRightInset();
727 }
728
729 /** {@inheritDoc} */
730 @Override protected double computeMinHeight(double width, double topInset, double rightInset, double bottomInset, double leftInset) {
731 return container.minHeight(width) + snappedTopInset() + snappedBottomInset();
732 }
733
734 /** {@inheritDoc} */
735 @Override protected double computePrefHeight(double width, double topInset, double rightInset, double bottomInset, double leftInset) {
736 return container.prefHeight(width) + snappedTopInset() + snappedBottomInset();
737 }
738
739 // grow horizontally, but not vertically
740 /** {@inheritDoc} */
741 @Override protected double computeMaxHeight(double width, double topInset, double rightInset, double bottomInset, double leftInset) {
742 return getSkinnable().prefHeight(-1);
743 }
744
745
746
747 /***************************************************************************
748 * *
749 * Private implementation *
750 * *
751 **************************************************************************/
752
753 // For testing purpose only.
754 MenuButton getNodeForMenu(int i) {
755 if (i < container.getChildren().size()) {
756 return (MenuBarButton)container.getChildren().get(i);
757 }
758 return null;
759 }
760
761 int getFocusedMenuIndex() {
762 return focusedMenuIndex;
763 }
764
765 private boolean menusContainCustomMenuItem() {
766 for (Menu menu : getSkinnable().getMenus()) {
767 if (menuContainsCustomMenuItem(menu)) {
768 System.err.println("Warning: MenuBar ignored property useSystemMenuBar because menus contain CustomMenuItem");
769 return true;
770 }
771 }
772 return false;
773 }
774
775 private boolean menuContainsCustomMenuItem(Menu menu) {
776 for (MenuItem mi : menu.getItems()) {
777 if (mi instanceof CustomMenuItem && !(mi instanceof SeparatorMenuItem)) {
778 return true;
779 } else if (mi instanceof Menu) {
780 if (menuContainsCustomMenuItem((Menu)mi)) {
781 return true;
782 }
783 }
784 }
785 return false;
786 }
787
788 private int getMenuBarButtonIndex(MenuBarButton m) {
789 for (int i= 0; i < container.getChildren().size(); i++) {
790 MenuBarButton menuButton = (MenuBarButton)container.getChildren().get(i);
791 if (m == menuButton) {
792 return i;
793 }
794 }
795 return -1;
796 }
797
798 private void updateActionListeners(MenuItem item, boolean add) {
799 if (item instanceof Menu) {
800 Menu menu = (Menu) item;
801
802 if (add) {
803 menu.getItems().addListener(menuItemListener);
804 } else {
805 menu.getItems().removeListener(menuItemListener);
806 }
807
808 for (MenuItem mi : menu.getItems()) {
809 updateActionListeners(mi, add);
810 }
811 } else {
812 if (add) {
813 item.addEventHandler(ActionEvent.ACTION, menuActionEventHandler);
814 } else {
815 item.removeEventHandler(ActionEvent.ACTION, menuActionEventHandler);
816 }
817 }
818 }
819
820 private void rebuildUI() {
821 getSkinnable().focusedProperty().removeListener(menuBarFocusedPropertyListener);
822 for (Menu m : getSkinnable().getMenus()) {
823 // remove action listeners
824 updateActionListeners(m, false);
825
826 m.visibleProperty().removeListener(menuVisibilityChangeListener);
827 }
828 for (Node n : container.getChildren()) {
829 // Stop observing menu's showing & disable property for changes.
830 // Need to unbind before clearing container's children.
831 MenuBarButton menuButton = (MenuBarButton)n;
832 menuButton.hide();
833 menuButton.menu.showingProperty().removeListener(menuButton.menuListener);
834 menuButton.disableProperty().unbind();
835 menuButton.textProperty().unbind();
836 menuButton.graphicProperty().unbind();
837 menuButton.styleProperty().unbind();
838
839 menuButton.dispose();
840
841 // RT-29729 : old instance of context menu window/popup for this MenuButton needs
842 // to be cleaned up. Setting the skin to null - results in a call to dispose()
843 // on the skin which in this case MenuButtonSkinBase - does the subsequent
844 // clean up to ContextMenu/popup window.
845 menuButton.setSkin(null);
846 menuButton = null;
847 }
848 container.getChildren().clear();
849
850 if (Toolkit.getToolkit().getSystemMenu().isSupported()) {
851 final Scene scene = getSkinnable().getScene();
852 if (scene != null) {
853 // RT-36554 - make sure system menu is updated when this MenuBar's scene changes.
854 if (sceneChangeListener == null) {
855 sceneChangeListener = (observable, oldValue, newValue) -> {
856
857 if (oldValue != null) {
858 if (oldValue.getWindow() instanceof Stage) {
859 final Stage stage = (Stage) oldValue.getWindow();
860 final MenuBarSkin curMBSkin = getMenuBarSkin(stage);
861 if (curMBSkin == MenuBarSkin.this) {
862 curMBSkin.wrappedMenus = null;
863 systemMenuMap.remove(stage);
864 if (currentMenuBarStage == stage) {
865 currentMenuBarStage = null;
866 setSystemMenu(stage);
867 }
868 } else {
869 if (curMBSkin != null && curMBSkin.getSkinnable() != null &&
870 curMBSkin.getSkinnable().isUseSystemMenuBar()) {
871 curMBSkin.getSkinnable().setUseSystemMenuBar(false);
872 }
873 }
874 }
875 }
876
877 if (newValue != null) {
878 if (getSkinnable().isUseSystemMenuBar() && !menusContainCustomMenuItem()) {
879 if (newValue.getWindow() instanceof Stage) {
880 final Stage stage = (Stage) newValue.getWindow();
881 if (systemMenuMap == null) {
882 initSystemMenuBar();
883 }
884 wrappedMenus = new ArrayList<>();
885 systemMenuMap.put(stage, new WeakReference<>(this));
886 for (Menu menu : getSkinnable().getMenus()) {
887 wrappedMenus.add(GlobalMenuAdapter.adapt(menu));
888 }
889 currentMenuBarStage = null;
890 setSystemMenu(stage);
891
892 // TODO: Why two request layout calls here?
893 getSkinnable().requestLayout();
894 javafx.application.Platform.runLater(() -> getSkinnable().requestLayout());
895 }
896 }
897 }
898 };
899 getSkinnable().sceneProperty().addListener(sceneChangeListener);
900 }
901
902 // Fake a change event to trigger an update to the system menu.
903 sceneChangeListener.changed(getSkinnable().sceneProperty(), scene, scene);
904
905 // If the system menu references this MenuBarSkin, then we're done with rebuilding the UI.
906 // If the system menu does not reference this MenuBarSkin, then the MenuBar is a child of the scene
907 // and we continue with the update.
908 // If there is no system menu but this skinnable uses the system menu bar, then the
909 // stage just isn't focused yet (see setSystemMenu) and we're done rebuilding the UI.
910 if (currentMenuBarStage != null ? getMenuBarSkin(currentMenuBarStage) == MenuBarSkin.this : getSkinnable().isUseSystemMenuBar()) {
911 return;
912 }
913
914 } else {
915 // if scene is null, make sure this MenuBarSkin isn't left behind as the system menu
916 if (currentMenuBarStage != null) {
917 final MenuBarSkin curMBSkin = getMenuBarSkin(currentMenuBarStage);
918 if (curMBSkin == MenuBarSkin.this) {
919 setSystemMenu(null);
920 }
921 }
922 }
923 }
924
925 getSkinnable().focusedProperty().addListener(menuBarFocusedPropertyListener);
926 for (final Menu menu : getSkinnable().getMenus()) {
927
928 menu.visibleProperty().addListener(menuVisibilityChangeListener);
929
930 if (!menu.isVisible()) continue;
931 final MenuBarButton menuButton = new MenuBarButton(this, menu);
932 menuButton.setFocusTraversable(false);
933 menuButton.getStyleClass().add("menu");
934 menuButton.setStyle(menu.getStyle()); // copy style
935
936 menuButton.getItems().setAll(menu.getItems());
937 container.getChildren().add(menuButton);
938
939 menuButton.menuListener = (observable, oldValue, newValue) -> {
940 if (menu.isShowing()) {
941 menuButton.show();
942 menuModeStart(container.getChildren().indexOf(menuButton));
943 } else {
944 menuButton.hide();
945 }
946 };
947 menuButton.menu = menu;
948 menu.showingProperty().addListener(menuButton.menuListener);
949 menuButton.disableProperty().bindBidirectional(menu.disableProperty());
950 menuButton.textProperty().bind(menu.textProperty());
951 menuButton.graphicProperty().bind(menu.graphicProperty());
952 menuButton.styleProperty().bind(menu.styleProperty());
953 menuButton.getProperties().addListener((MapChangeListener<Object, Object>) c -> {
954 if (c.wasAdded() && MenuButtonSkin.AUTOHIDE.equals(c.getKey())) {
955 menuButton.getProperties().remove(MenuButtonSkin.AUTOHIDE);
956 menu.hide();
957 }
958 });
959 menuButton.showingProperty().addListener((observable, oldValue, isShowing) -> {
960 if (isShowing) {
961 if(openMenuButton == null && focusedMenuIndex != -1)
962 openMenuButton = (MenuBarButton)container.getChildren().get(focusedMenuIndex);
963
964 if (openMenuButton != null && openMenuButton != menuButton) {
965 openMenuButton.clearHover();
966 }
967 openMenuButton = menuButton;
968 showMenu(menu);
969 } else {
970 // Fix for JDK-8167138 - we need to clear out the openMenu / openMenuButton
971 // when the menu is hidden (e.g. via autoHide), so that we can open it again
972 // the next time (if it is the first menu requested to show)
973 openMenu = null;
974 openMenuButton = null;
975 }
976 });
977
978 menuButton.setOnMousePressed(event -> {
979 pendingDismiss = menuButton.isShowing();
980
981 // check if the owner window has focus
982 if (menuButton.getScene().getWindow().isFocused()) {
983 showMenu(menu);
984 // update FocusedIndex
985 menuModeStart(getMenuBarButtonIndex(menuButton));
986 }
987 });
988
989 menuButton.setOnMouseReleased(event -> {
990 // check if the owner window has focus
991 if (menuButton.getScene().getWindow().isFocused()) {
992 if (pendingDismiss) {
993 resetOpenMenu();
994 }
995 }
996 pendingDismiss = false;
997 });
998
999 menuButton.setOnMouseEntered(event -> {
1000 // check if the owner window has focus
1001 if (menuButton.getScene() != null && menuButton.getScene().getWindow() != null &&
1002 menuButton.getScene().getWindow().isFocused()) {
1003 if (openMenuButton != null && openMenuButton != menuButton) {
1004 openMenuButton.clearHover();
1005 openMenuButton = null;
1006 openMenuButton = menuButton;
1007 }
1008 updateFocusedIndex();
1009 if (openMenu != null && openMenu != menu) {
1010 showMenu(menu);
1011 }
1012 }
1013 });
1014 updateActionListeners(menu, true);
1015 }
1016 getSkinnable().requestLayout();
1017 }
1018
1019 private void cleanUpSystemMenu() {
1020 if (sceneChangeListener != null && getSkinnable() != null) {
1021 getSkinnable().sceneProperty().removeListener(sceneChangeListener);
1022 // rebuildUI creates sceneChangeListener and adds sceneChangeListener to sceneProperty,
1023 // so sceneChangeListener needs to be reset to null in the off chance that this
1024 // skin instance is reused.
1025 sceneChangeListener = null;
1026 }
1027
1028 if (currentMenuBarStage != null && getMenuBarSkin(currentMenuBarStage) == MenuBarSkin.this) {
1029 setSystemMenu(null);
1030 }
1031
1032 if (systemMenuMap != null) {
1033 Iterator<Map.Entry<Stage,Reference<MenuBarSkin>>> iterator = systemMenuMap.entrySet().iterator();
1034 while (iterator.hasNext()) {
1035 Map.Entry<Stage,Reference<MenuBarSkin>> entry = iterator.next();
1036 Reference<MenuBarSkin> ref = entry.getValue();
1037 MenuBarSkin skin = ref != null ? ref.get() : null;
1038 if (skin == null || skin == MenuBarSkin.this) {
1039 iterator.remove();
1040 }
1041 }
1042 }
1043 }
1044
1045 private boolean isMenuEmpty(Menu menu) {
1046 boolean retVal = true;
1047 if (menu != null) {
1048 for (MenuItem m : menu.getItems()) {
1049 if (m != null && m.isVisible()) retVal = false;
1050 }
1051 }
1052 return retVal;
1053 }
1054
1055 private void resetOpenMenu() {
1056 if (openMenu != null) {
1057 openMenu.hide();
1058 openMenu = null;
1059 }
1060 }
1061
1062 private void unSelectMenus() {
1063 clearMenuButtonHover();
1064 if (focusedMenuIndex == -1) return;
1065 if (openMenu != null) {
1066 openMenu.hide();
1067 openMenu = null;
1068 }
1069 if (openMenuButton != null) {
1070 openMenuButton.clearHover();
1071 openMenuButton = null;
1072 }
1073 menuModeEnd();
1074 }
1075
1076 private void menuModeStart(int newIndex) {
1077 if (focusedMenuIndex == -1) {
1078 SceneHelper.getSceneAccessor().setTransientFocusContainer(getSkinnable().getScene(), getSkinnable());
1079 }
1080 setFocusedMenuIndex(newIndex);
1081 }
1082
1083 private void menuModeEnd() {
1084 if (focusedMenuIndex != -1) {
1085 SceneHelper.getSceneAccessor().setTransientFocusContainer(getSkinnable().getScene(), null);
1086
1087 /* Return the a11y focus to a control in the scene. */
1088 getSkinnable().notifyAccessibleAttributeChanged(AccessibleAttribute.FOCUS_NODE);
1089 }
1090 setFocusedMenuIndex(-1);
1091 }
1092
1093 private void moveToMenu(Direction dir, boolean doShow) {
1094 boolean showNextMenu = doShow && focusedMenu.isShowing();
1095 findSibling(dir, focusedMenuIndex).ifPresent(p -> {
1096 setFocusedMenuIndex(p.getValue());
1097 if (showNextMenu) {
1098 // we explicitly do *not* allow selection - we are moving
1099 // to a sibling menu, and therefore selection should be reset
1100 showMenu(p.getKey(), false);
1101 }
1102 });
1103 }
1104
1105 private Optional<Pair<Menu,Integer>> findSibling(Direction dir, int startIndex) {
1106 if (startIndex == -1) {
1107 return Optional.empty();
1108 }
1109
1110 final int totalMenus = getSkinnable().getMenus().size();
1111 int i = 0;
1112 int nextIndex = 0;
1113
1114 // Traverse all menus in menubar to find nextIndex
1115 while (i < totalMenus) {
1116 i++;
1117
1118 nextIndex = (startIndex + (dir.isForward() ? 1 : -1)) % totalMenus;
1119
1120 if (nextIndex == -1) {
1121 // loop backwards to end
1122 nextIndex = totalMenus - 1;
1123 }
1124
1125 // if menu at nextIndex is disabled, skip it
1126 if (getSkinnable().getMenus().get(nextIndex).isDisable()) {
1127 // Calculate new nextIndex by continuing loop
1128 startIndex = nextIndex;
1129 } else {
1130 // nextIndex is to be highlighted
1131 break;
1132 }
1133 }
1134
1135 clearMenuButtonHover();
1136 return Optional.of(new Pair<>(getSkinnable().getMenus().get(nextIndex), nextIndex));
1137 }
1138
1139 private void updateFocusedIndex() {
1140 int index = 0;
1141 for(Node n : container.getChildren()) {
1142 if (n.isHover()) {
1143 setFocusedMenuIndex(index);
1144 return;
1145 }
1146 index++;
1147 }
1148 menuModeEnd();
1149 }
1150
1151 private void clearMenuButtonHover() {
1152 for(Node n : container.getChildren()) {
1153 if (n.isHover()) {
1154 ((MenuBarButton)n).clearHover();
1155 ((MenuBarButton)n).disarm();
1156 return;
1157 }
1158 }
1159 }
1160
1161
1162
1163 /***************************************************************************
1164 * *
1165 * CSS *
1166 * *
1167 **************************************************************************/
1168
1169 private static final CssMetaData<MenuBar,Number> SPACING =
1170 new CssMetaData<MenuBar,Number>("-fx-spacing",
1171 SizeConverter.getInstance(), 0.0) {
1172
1173 @Override
1174 public boolean isSettable(MenuBar n) {
1175 final MenuBarSkin skin = (MenuBarSkin) n.getSkin();
1176 return skin.spacing == null || !skin.spacing.isBound();
1177 }
1178
1179 @Override
1180 public StyleableProperty<Number> getStyleableProperty(MenuBar n) {
1181 final MenuBarSkin skin = (MenuBarSkin) n.getSkin();
1182 return (StyleableProperty<Number>)(WritableValue<Number>)skin.spacingProperty();
1183 }
1184 };
1185
1186 private static final CssMetaData<MenuBar,Pos> ALIGNMENT =
1187 new CssMetaData<MenuBar,Pos>("-fx-alignment",
1188 new EnumConverter<Pos>(Pos.class), Pos.TOP_LEFT ) {
1189
1190 @Override
1191 public boolean isSettable(MenuBar n) {
1192 final MenuBarSkin skin = (MenuBarSkin) n.getSkin();
1193 return skin.containerAlignment == null || !skin.containerAlignment.isBound();
1194 }
1195
1196 @Override
1197 public StyleableProperty<Pos> getStyleableProperty(MenuBar n) {
1198 final MenuBarSkin skin = (MenuBarSkin) n.getSkin();
1199 return (StyleableProperty<Pos>)(WritableValue<Pos>)skin.containerAlignmentProperty();
1200 }
1201 };
1202
1203
1204 private static final List<CssMetaData<? extends Styleable, ?>> STYLEABLES;
1205 static {
1206
1207 final List<CssMetaData<? extends Styleable, ?>> styleables =
1208 new ArrayList<CssMetaData<? extends Styleable, ?>>(SkinBase.getClassCssMetaData());
1209
1210 // StackPane also has -fx-alignment. Replace it with
1211 // MenuBarSkin's.
1212 // TODO: Really should be able to reference StackPane.StyleableProperties.ALIGNMENT
1213 final String alignmentProperty = ALIGNMENT.getProperty();
1214 for (int n=0, nMax=styleables.size(); n<nMax; n++) {
1215 final CssMetaData<?,?> prop = styleables.get(n);
1216 if (alignmentProperty.equals(prop.getProperty())) styleables.remove(prop);
1217 }
1218
1219 styleables.add(SPACING);
1220 styleables.add(ALIGNMENT);
1221 STYLEABLES = Collections.unmodifiableList(styleables);
1222
1223 }
1224
1225 /**
1226 * Returns the CssMetaData associated with this class, which may include the
1227 * CssMetaData of its superclasses.
1228 * @return the CssMetaData associated with this class, which may include the
1229 * CssMetaData of its superclasses
1230 */
1231 public static List<CssMetaData<? extends Styleable, ?>> getClassCssMetaData() {
1232 return STYLEABLES;
1233 }
1234
1235 /**
1236 * {@inheritDoc}
1237 */
1238 @Override
1239 public List<CssMetaData<? extends Styleable, ?>> getCssMetaData() {
1240 return getClassCssMetaData();
1241 }
1242
1243 /***************************************************************************
1244 * *
1245 * Accessibility handling *
1246 * *
1247 **************************************************************************/
1248
1249 /** {@inheritDoc} */
1250 @Override protected Object queryAccessibleAttribute(AccessibleAttribute attribute, Object... parameters) {
1251 switch (attribute) {
1252 case FOCUS_NODE: return openMenuButton;
1253 default: return super.queryAccessibleAttribute(attribute, parameters);
1254 }
1255 }
1256 }