--- old/modules/controls/src/main/java/com/sun/javafx/scene/control/skin/MenuBarSkin.java 2015-09-03 15:20:08.929141400 -0700 +++ /dev/null 2015-09-03 15:20:09.000000000 -0700 @@ -1,1254 +0,0 @@ -/* - * Copyright (c) 2010, 2015, Oracle and/or its affiliates. All rights reserved. - * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. - * - * This code is free software; you can redistribute it and/or modify it - * under the terms of the GNU General Public License version 2 only, as - * published by the Free Software Foundation. Oracle designates this - * particular file as subject to the "Classpath" exception as provided - * by Oracle in the LICENSE file that accompanied this code. - * - * This code is distributed in the hope that it will be useful, but WITHOUT - * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or - * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License - * version 2 for more details (a copy is included in the LICENSE file that - * accompanied this code). - * - * You should have received a copy of the GNU General Public License version - * 2 along with this work; if not, write to the Free Software Foundation, - * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. - * - * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA - * or visit www.oracle.com if you need additional information or have any - * questions. - */ - -package com.sun.javafx.scene.control.skin; - -import com.sun.javafx.css.converters.EnumConverter; -import com.sun.javafx.css.converters.SizeConverter; -import com.sun.javafx.scene.traversal.ParentTraversalEngine; -import javafx.beans.InvalidationListener; -import javafx.beans.property.DoubleProperty; -import javafx.beans.property.ObjectProperty; -import javafx.beans.property.ReadOnlyProperty; -import javafx.beans.value.ChangeListener; -import javafx.beans.value.WeakChangeListener; -import javafx.beans.value.WritableValue; -import javafx.collections.ListChangeListener; -import javafx.collections.MapChangeListener; -import javafx.collections.ObservableList; -import javafx.css.CssMetaData; -import javafx.css.Styleable; -import javafx.css.StyleableDoubleProperty; -import javafx.css.StyleableObjectProperty; -import javafx.css.StyleableProperty; -import javafx.event.ActionEvent; -import javafx.event.EventHandler; -import javafx.event.WeakEventHandler; -import javafx.geometry.Bounds; -import javafx.geometry.NodeOrientation; -import javafx.geometry.Pos; -import javafx.scene.AccessibleAttribute; -import javafx.scene.AccessibleRole; -import javafx.scene.Node; -import javafx.scene.Scene; -import javafx.scene.control.CustomMenuItem; -import javafx.scene.control.Menu; -import javafx.scene.control.MenuBar; -import javafx.scene.control.MenuButton; -import javafx.scene.control.MenuItem; -import javafx.scene.control.SeparatorMenuItem; -import javafx.scene.control.SkinBase; -import javafx.scene.input.KeyCombination; -import javafx.scene.input.KeyEvent; -import javafx.scene.input.MouseEvent; -import javafx.scene.layout.HBox; -import javafx.stage.Stage; - -import java.lang.ref.Reference; -import java.lang.ref.WeakReference; -import java.util.ArrayList; -import java.util.Collections; -import java.util.Iterator; -import java.util.List; -import java.util.Map; -import java.util.WeakHashMap; - -import com.sun.javafx.menu.MenuBase; -import com.sun.javafx.scene.SceneHelper; -import com.sun.javafx.scene.control.GlobalMenuAdapter; -import com.sun.javafx.scene.control.behavior.BehaviorBase; -import com.sun.javafx.scene.traversal.TraverseListener; -import com.sun.javafx.stage.StageHelper; -import com.sun.javafx.tk.Toolkit; -import javafx.stage.Window; - - -/** - * The skin for the MenuBar. In essence it is a simple toolbar. For the time - * being there is no overflow behavior and we just hide nodes which fall - * outside the bounds. - */ -public class MenuBarSkin extends BehaviorSkinBase> implements TraverseListener { - - private final HBox container; - - private Menu openMenu; - private MenuBarButton openMenuButton; - private int focusedMenuIndex = -1; - - private static WeakHashMap> systemMenuMap; - private static List wrappedDefaultMenus = new ArrayList(); - private static Stage currentMenuBarStage; - private List wrappedMenus; - - public static void setDefaultSystemMenuBar(final MenuBar menuBar) { - if (Toolkit.getToolkit().getSystemMenu().isSupported()) { - wrappedDefaultMenus.clear(); - for (Menu menu : menuBar.getMenus()) { - wrappedDefaultMenus.add(GlobalMenuAdapter.adapt(menu)); - } - menuBar.getMenus().addListener((ListChangeListener) c -> { - wrappedDefaultMenus.clear(); - for (Menu menu : menuBar.getMenus()) { - wrappedDefaultMenus.add(GlobalMenuAdapter.adapt(menu)); - } - }); - } - } - - private static MenuBarSkin getMenuBarSkin(Stage stage) { - if (systemMenuMap == null) return null; - Reference skinRef = systemMenuMap.get(stage); - return skinRef == null ? null : skinRef.get(); - } - - private static void setSystemMenu(Stage stage) { - if (stage != null && stage.isFocused()) { - while (stage != null && stage.getOwner() instanceof Stage) { - MenuBarSkin skin = getMenuBarSkin(stage); - if (skin != null && skin.wrappedMenus != null) { - break; - } else { - // This is a secondary stage (dialog) that doesn't - // have own menu bar. - // - // Continue looking for a menu bar in the parent stage. - stage = (Stage)stage.getOwner(); - } - } - } else { - stage = null; - } - - if (stage != currentMenuBarStage) { - List menuList = null; - if (stage != null) { - MenuBarSkin skin = getMenuBarSkin(stage); - if (skin != null) { - menuList = skin.wrappedMenus; - } - } - if (menuList == null) { - menuList = wrappedDefaultMenus; - } - Toolkit.getToolkit().getSystemMenu().setMenus(menuList); - currentMenuBarStage = stage; - } - } - - private static void initSystemMenuBar() { - systemMenuMap = new WeakHashMap<>(); - - final InvalidationListener focusedStageListener = ov -> { - setSystemMenu((Stage)((ReadOnlyProperty)ov).getBean()); - }; - - final ObservableList stages = StageHelper.getStages(); - for (Stage stage : stages) { - stage.focusedProperty().addListener(focusedStageListener); - } - stages.addListener((ListChangeListener) c -> { - while (c.next()) { - for (Stage stage : c.getRemoved()) { - stage.focusedProperty().removeListener(focusedStageListener); - } - for (Stage stage : c.getAddedSubList()) { - stage.focusedProperty().addListener(focusedStageListener); - setSystemMenu(stage); - } - } - }); - } - - private WeakEventHandler weakSceneKeyEventHandler; - private WeakEventHandler weakSceneMouseEventHandler; - private WeakChangeListener weakWindowFocusListener; - private WeakChangeListener weakWindowSceneListener; - private EventHandler keyEventHandler; - private EventHandler mouseEventHandler; - private ChangeListener menuBarFocusedPropertyListener; - private ChangeListener sceneChangeListener; - - EventHandler getKeyEventHandler() { - return keyEventHandler; - } - - /*************************************************************************** - * * - * Constructors * - * * - **************************************************************************/ - - public MenuBarSkin(final MenuBar control) { - super(control, new BehaviorBase<>(control, Collections.emptyList())); - - container = new HBox(); - container.getStyleClass().add("container"); - getChildren().add(container); - - // Key navigation - keyEventHandler = event -> { - // process right left and may be tab key events - if (openMenu != null) { - switch (event.getCode()) { - case LEFT: { - boolean isRTL = control.getEffectiveNodeOrientation() == NodeOrientation.RIGHT_TO_LEFT; - if (control.getScene().getWindow().isFocused()) { - if (openMenu == null) return; - if ( !openMenu.isShowing()) { - if (isRTL) { - selectNextMenu(); // just move the selection bar - } else { - selectPrevMenu(); // just move the selection bar - } - event.consume(); - return; - } - if (isRTL) { - showNextMenu(); - } else { - showPrevMenu(); - } - } - event.consume(); - break; - } - case RIGHT: - { - boolean isRTL = control.getEffectiveNodeOrientation() == NodeOrientation.RIGHT_TO_LEFT; - if (control.getScene().getWindow().isFocused()) { - if (openMenu == null) return; - if (! openMenu.isShowing()) { - if (isRTL) { - selectPrevMenu(); // just move the selection bar - } else { - selectNextMenu(); // just move the selection bar - } - event.consume(); - return; - } - if (isRTL) { - showPrevMenu(); - } else { - showNextMenu(); - } - } - event.consume(); - break; - } - case DOWN: - //case SPACE: - //case ENTER: - // RT-18859: Doing nothing for space and enter - if (control.getScene().getWindow().isFocused()) { - if (focusedMenuIndex != -1 && openMenu != null) { - openMenu = getSkinnable().getMenus().get(focusedMenuIndex); - if (!isMenuEmpty(getSkinnable().getMenus().get(focusedMenuIndex))) { - openMenu.show(); - } - event.consume(); - } - } - break; - case ESCAPE: - unSelectMenus(); - event.consume(); - break; - default: - break; - } - } - }; - menuBarFocusedPropertyListener = (ov, t, t1) -> { - if (t1) { - // RT-23147 when MenuBar's focusTraversable is true the first - // menu will visually indicate focus - unSelectMenus(); - menuModeStart(0); - openMenuButton = ((MenuBarButton)container.getChildren().get(0)); - openMenu = getSkinnable().getMenus().get(0); - openMenuButton.setHover(); - } else { - unSelectMenus(); - } - }; - weakSceneKeyEventHandler = new WeakEventHandler(keyEventHandler); - Utils.executeOnceWhenPropertyIsNonNull(control.sceneProperty(), (Scene scene) -> { - scene.addEventFilter(KeyEvent.KEY_PRESSED, weakSceneKeyEventHandler); - }); - - // When we click else where in the scene - menu selection should be cleared. - mouseEventHandler = t -> { - if (!container.localToScreen(container.getLayoutBounds()).contains(t.getScreenX(), t.getScreenY())) { - unSelectMenus(); - } - }; - weakSceneMouseEventHandler = new WeakEventHandler(mouseEventHandler); - Utils.executeOnceWhenPropertyIsNonNull(control.sceneProperty(), (Scene scene) -> { - scene.addEventFilter(MouseEvent.MOUSE_CLICKED, weakSceneMouseEventHandler); - }); - - weakWindowFocusListener = new WeakChangeListener((ov, t, t1) -> { - if (!t1) { - unSelectMenus(); - } - }); - // When the parent window looses focus - menu selection should be cleared - Utils.executeOnceWhenPropertyIsNonNull(control.sceneProperty(), (Scene scene) -> { - if (scene.getWindow() != null) { - scene.getWindow().focusedProperty().addListener(weakWindowFocusListener); - } else { - ChangeListener sceneWindowListener = (observable, oldValue, newValue) -> { - if (oldValue != null) - oldValue.focusedProperty().removeListener(weakWindowFocusListener); - if (newValue != null) - newValue.focusedProperty().addListener(weakWindowFocusListener); - }; - weakWindowSceneListener = new WeakChangeListener<>(sceneWindowListener); - scene.windowProperty().addListener(weakWindowSceneListener); - } - }); - - rebuildUI(); - control.getMenus().addListener((ListChangeListener) c -> { - rebuildUI(); - }); - for (final Menu menu : getSkinnable().getMenus()) { - menu.visibleProperty().addListener((ov, t, t1) -> { - rebuildUI(); - }); - } - - if (Toolkit.getToolkit().getSystemMenu().isSupported()) { - control.useSystemMenuBarProperty().addListener(valueModel -> { - rebuildUI(); - }); - } - - // When the mouse leaves the menu, the last hovered item should lose - // it's focus so that it is no longer selected. This code returns focus - // to the MenuBar itself, such that keyboard navigation can continue. - // fix RT-12254 : menu bar should not request focus on mouse exit. -// addEventFilter(MouseEvent.MOUSE_EXITED, new EventHandler() { -// @Override -// public void handle(MouseEvent event) { -// requestFocus(); -// } -// }); - - /* - ** add an accelerator for F10 on windows and ctrl+F10 on mac/linux - ** pressing f10 will select the first menu button on a menubar - */ - final KeyCombination acceleratorKeyCombo; - if (com.sun.javafx.util.Utils.isMac()) { - acceleratorKeyCombo = KeyCombination.keyCombination("ctrl+F10"); - } else { - acceleratorKeyCombo = KeyCombination.keyCombination("F10"); - } - Utils.executeOnceWhenPropertyIsNonNull(control.sceneProperty(), (Scene scene) -> { - scene.getAccelerators().put(acceleratorKeyCombo, firstMenuRunnable); - - // put focus on the first menu when the alt key is pressed - scene.addEventHandler(KeyEvent.KEY_PRESSED, e -> { - if (e.isAltDown() && !e.isConsumed()) { - firstMenuRunnable.run(); - } - }); - }); - - ParentTraversalEngine engine = new ParentTraversalEngine(getSkinnable()); - engine.addTraverseListener(this); - getSkinnable().setImpl_traversalEngine(engine); - - control.sceneProperty().addListener((ov, t, t1) -> { - if (weakSceneKeyEventHandler != null) { - // remove event filter from the old scene (t) - if (t != null) - t.removeEventFilter(KeyEvent.KEY_PRESSED, weakSceneKeyEventHandler); - } - if (weakSceneMouseEventHandler != null) { - // remove event filter from the old scene (t) - if (t != null) - t.removeEventFilter(MouseEvent.MOUSE_CLICKED, weakSceneMouseEventHandler); - } - - /** - * remove the f10 accelerator from the old scene - * add it to the new scene - */ - if (t != null) { - t.getAccelerators().remove(acceleratorKeyCombo); - } - if (t1 != null ) { - t1.getAccelerators().put(acceleratorKeyCombo, firstMenuRunnable); - } - }); - } - - - Runnable firstMenuRunnable = new Runnable() { - public void run() { - /* - ** check that this menubar's container has contents, - ** and that the first item is a MenuButton.... - ** otherwise the transfer is off! - */ - if (container.getChildren().size() > 0) { - if (container.getChildren().get(0) instanceof MenuButton) { -// container.getChildren().get(0).requestFocus(); - if (focusedMenuIndex != 0) { - unSelectMenus(); - menuModeStart(0); - openMenuButton = ((MenuBarButton)container.getChildren().get(0)); - openMenu = getSkinnable().getMenus().get(0); - openMenuButton.setHover(); - } - else { - unSelectMenus(); - } - } - } - } - }; - - - private boolean pendingDismiss = false; - - // For testing purpose only. - MenuButton getNodeForMenu(int i) { - if (i < container.getChildren().size()) { - return (MenuBarButton)container.getChildren().get(i); - } - return null; - } - - int getFocusedMenuIndex() { - return focusedMenuIndex; - } - - private boolean menusContainCustomMenuItem() { - for (Menu menu : getSkinnable().getMenus()) { - if (menuContainsCustomMenuItem(menu)) { - System.err.println("Warning: MenuBar ignored property useSystemMenuBar because menus contain CustomMenuItem"); - return true; - } - } - return false; - } - - private boolean menuContainsCustomMenuItem(Menu menu) { - for (MenuItem mi : menu.getItems()) { - if (mi instanceof CustomMenuItem && !(mi instanceof SeparatorMenuItem)) { - return true; - } else if (mi instanceof Menu) { - if (menuContainsCustomMenuItem((Menu)mi)) { - return true; - } - } - } - return false; - } - - private int getMenuBarButtonIndex(MenuBarButton m) { - for (int i= 0; i < container.getChildren().size(); i++) { - MenuBarButton menuButton = (MenuBarButton)container.getChildren().get(i); - if (m == menuButton) { - return i; - } - } - return -1; - } - - // RT-20411 : reset menu selected/focused state - private EventHandler menuActionEventHandler = t -> { - if (t.getSource() instanceof CustomMenuItem) { - // RT-29614 If CustomMenuItem hideOnClick is false, dont hide - CustomMenuItem cmi = (CustomMenuItem)t.getSource(); - if (!cmi.isHideOnClick()) return; - } - unSelectMenus(); - }; - - private ListChangeListener menuItemListener = (c) -> { - while (c.next()) { - for (MenuItem mi : c.getAddedSubList()) { - mi.addEventHandler(ActionEvent.ACTION, menuActionEventHandler); - } - for (MenuItem mi: c.getRemoved()) { - mi.removeEventHandler(ActionEvent.ACTION, menuActionEventHandler); - } - } - }; - - private void updateActionListeners(Menu m, boolean add) { - if (add) { - m.getItems().addListener(menuItemListener); - } else { - m.getItems().removeListener(menuItemListener); - } - for (MenuItem mi : m.getItems()) { - if (mi instanceof Menu) { - updateActionListeners((Menu)mi, add); - } else { - if (add) { - mi.addEventHandler(ActionEvent.ACTION, menuActionEventHandler); - } else { - mi.removeEventHandler(ActionEvent.ACTION, menuActionEventHandler); - } - } - } - } - - private void rebuildUI() { - getSkinnable().focusedProperty().removeListener(menuBarFocusedPropertyListener); - for (Menu m : getSkinnable().getMenus()) { - // remove action listeners - updateActionListeners(m, false); - } - for (Node n : container.getChildren()) { - // Stop observing menu's showing & disable property for changes. - // Need to unbind before clearing container's children. - MenuBarButton menuButton = (MenuBarButton)n; - menuButton.hide(); - menuButton.menu.showingProperty().removeListener(menuButton.menuListener); - menuButton.disableProperty().unbind(); - menuButton.textProperty().unbind(); - menuButton.graphicProperty().unbind(); - menuButton.styleProperty().unbind(); - - menuButton.dispose(); - - // RT-29729 : old instance of context menu window/popup for this MenuButton needs - // to be cleaned up. Setting the skin to null - results in a call to dispose() - // on the skin which in this case MenuButtonSkinBase - does the subsequent - // clean up to ContextMenu/popup window. - menuButton.setSkin(null); - menuButton = null; - } - container.getChildren().clear(); - - if (Toolkit.getToolkit().getSystemMenu().isSupported()) { - - final Scene scene = getSkinnable().getScene(); - if (scene != null) { - // RT-36554 - make sure system menu is updated when this MenuBar's scene changes. - if (sceneChangeListener == null) { - sceneChangeListener = (observable, oldValue, newValue) -> { - - if (oldValue != null) { - if (oldValue.getWindow() instanceof Stage) { - final Stage stage = (Stage) oldValue.getWindow(); - final MenuBarSkin curMBSkin = getMenuBarSkin(stage); - if (curMBSkin == MenuBarSkin.this) { - curMBSkin.wrappedMenus = null; - systemMenuMap.remove(stage); - if (currentMenuBarStage == stage) { - currentMenuBarStage = null; - setSystemMenu(stage); - } - } else { - if (curMBSkin != null && curMBSkin.getSkinnable() != null && - curMBSkin.getSkinnable().isUseSystemMenuBar()) { - curMBSkin.getSkinnable().setUseSystemMenuBar(false); - } - } - } - } - - if (newValue != null) { - if (getSkinnable().isUseSystemMenuBar() && !menusContainCustomMenuItem()) { - if (newValue.getWindow() instanceof Stage) { - final Stage stage = (Stage) newValue.getWindow(); - if (systemMenuMap == null) { - initSystemMenuBar(); - } - wrappedMenus = new ArrayList<>(); - systemMenuMap.put(stage, new WeakReference<>(this)); - for (Menu menu : getSkinnable().getMenus()) { - wrappedMenus.add(GlobalMenuAdapter.adapt(menu)); - } - currentMenuBarStage = null; - setSystemMenu(stage); - - // TODO: Why two request layout calls here? - getSkinnable().requestLayout(); - javafx.application.Platform.runLater(() -> getSkinnable().requestLayout()); - } - } - } - }; - getSkinnable().sceneProperty().addListener(sceneChangeListener); - } - - // Fake a change event to trigger an update to the system menu. - sceneChangeListener.changed(getSkinnable().sceneProperty(), scene, scene); - - // If the system menu references this MenuBarSkin, then we're done with rebuilding the UI. - // If the system menu does not reference this MenuBarSkin, then the MenuBar is a child of the scene - // and we continue with the update. - // If there is no system menu but this skinnable uses the system menu bar, then the - // stage just isn't focused yet (see setSystemMenu) and we're done rebuilding the UI. - if (currentMenuBarStage != null ? getMenuBarSkin(currentMenuBarStage) == MenuBarSkin.this : getSkinnable().isUseSystemMenuBar()) { - return; - } - - } else { - // if scene is null, make sure this MenuBarSkin isn't left behind as the system menu - if (currentMenuBarStage != null) { - final MenuBarSkin curMBSkin = getMenuBarSkin(currentMenuBarStage); - if (curMBSkin == MenuBarSkin.this) { - setSystemMenu(null); - } - } - } - } - - getSkinnable().focusedProperty().addListener(menuBarFocusedPropertyListener); - for (final Menu menu : getSkinnable().getMenus()) { - if (!menu.isVisible()) continue; - final MenuBarButton menuButton = new MenuBarButton(this, menu); - menuButton.setFocusTraversable(false); - menuButton.getStyleClass().add("menu"); - menuButton.setStyle(menu.getStyle()); // copy style - - menuButton.getItems().setAll(menu.getItems()); - container.getChildren().add(menuButton); - - menuButton.menuListener = (observable, oldValue, newValue) -> { - if (menu.isShowing()) { - menuButton.show(); - menuModeStart(container.getChildren().indexOf(menuButton)); - } else { - menuButton.hide(); - } - }; - menuButton.menu = menu; - menu.showingProperty().addListener(menuButton.menuListener); - menuButton.disableProperty().bindBidirectional(menu.disableProperty()); - menuButton.textProperty().bind(menu.textProperty()); - menuButton.graphicProperty().bind(menu.graphicProperty()); - menuButton.styleProperty().bind(menu.styleProperty()); - menuButton.getProperties().addListener((MapChangeListener) c -> { - if (c.wasAdded() && MenuButtonSkin.AUTOHIDE.equals(c.getKey())) { - menuButton.getProperties().remove(MenuButtonSkin.AUTOHIDE); - menu.hide(); - } - }); - menuButton.showingProperty().addListener((observable, oldValue, isShowing) -> { - if (isShowing) { - if (openMenuButton != null && openMenuButton != menuButton) { - openMenuButton.hide(); - } - openMenuButton = menuButton; - openMenu = menu; - if (!menu.isShowing())menu.show(); - } - }); - - menuButton.setOnMousePressed(event -> { - pendingDismiss = menuButton.isShowing(); - - // check if the owner window has focus - if (menuButton.getScene().getWindow().isFocused()) { - openMenu = menu; - if (!isMenuEmpty(menu)){ - openMenu.show(); - } - // update FocusedIndex - menuModeStart(getMenuBarButtonIndex(menuButton)); - } - }); - - menuButton.setOnMouseReleased(event -> { - // check if the owner window has focus - if (menuButton.getScene().getWindow().isFocused()) { - if (pendingDismiss) { - resetOpenMenu(); -// menuButton.hide(); - } - } - pendingDismiss = false; - }); - -// menuButton. setOnKeyPressed(new EventHandler() { -// @Override public void handle(javafx.scene.input.KeyEvent ke) { -// switch (ke.getCode()) { -// case LEFT: -// if (menuButton.getScene().getWindow().isFocused()) { -// Menu prevMenu = findPreviousSibling(); -// if (openMenu == null || ! openMenu.isShowing()) { -// return; -// } -//// if (focusedMenuIndex == container.getChildren().size() - 1) { -//// ((MenuBarButton)container.getChildren().get(focusedMenuIndex)).requestFocus(); -//// } -// // hide the currently visible menu, and move to the previous one -// openMenu.hide(); -// if (!isMenuEmpty(prevMenu)) { -// openMenu = prevMenu; -// openMenu.show(); -// } else { -// openMenu = null; -// } -// } -// ke.consume(); -// break; -// case RIGHT: -// if (menuButton.getScene().getWindow().isFocused()) { -// Menu nextMenu = findNextSibling(); -// if (openMenu == null || ! openMenu.isShowing()) { -// return; -// } -//// if (focusedMenuIndex == 0) { -//// ((MenuBarButton)container.getChildren().get(focusedMenuIndex)).requestFocus(); -//// } -// // hide the currently visible menu, and move to the next one -// openMenu.hide(); -// if (!isMenuEmpty(nextMenu)) { -// openMenu = nextMenu; -// openMenu.show(); -// } else { -// openMenu = null; -// } -// } -// ke.consume(); -// break; -// -// case DOWN: -// case SPACE: -// case ENTER: -// if (menuButton.getScene().getWindow().isFocused()) { -// if (focusedMenuIndex != -1) { -// if (!isMenuEmpty(getSkinnable().getMenus().get(focusedMenuIndex))) { -// openMenu = getSkinnable().getMenus().get(focusedMenuIndex); -// openMenu.show(); -// } else { -// openMenu = null; -// } -// ke.consume(); -// } -// } -// break; -// } -// } -// }); - menuButton.setOnMouseEntered(event -> { - // check if the owner window has focus - if (menuButton.getScene() != null && menuButton.getScene().getWindow() != null && - menuButton.getScene().getWindow().isFocused()) { - if (openMenuButton != null && openMenuButton != menuButton) { - openMenuButton.clearHover(); - openMenuButton = null; - openMenuButton = menuButton; - } - updateFocusedIndex(); - if (openMenu != null && openMenu != menu) { - // hide the currently visible menu, and move to the new one - openMenu.hide(); - openMenu = menu; - updateFocusedIndex(); - if (!isMenuEmpty(menu)) { - openMenu.show(); - } - } - } - }); - updateActionListeners(menu, true); - } - getSkinnable().requestLayout(); - } - - /* - * if (openMenu == null) return; - if ( !openMenu.isShowing()) { - selectPrevMenu(); // just move the selection bar - return; - } - showPrevMenu(); - } - */ - private DoubleProperty spacing; - public final void setSpacing(double value) { - spacingProperty().set(snapSpace(value)); - } - - public final double getSpacing() { - return spacing == null ? 0.0 : snapSpace(spacing.get()); - } - - public final DoubleProperty spacingProperty() { - if (spacing == null) { - spacing = new StyleableDoubleProperty() { - - @Override - protected void invalidated() { - final double value = get(); - container.setSpacing(value); - } - - @Override - public Object getBean() { - return MenuBarSkin.this; - } - - @Override - public String getName() { - return "spacing"; - } - - @Override - public CssMetaData getCssMetaData() { - return SPACING; - } - }; - } - return spacing; - } - - private ObjectProperty containerAlignment; - public final void setContainerAlignment(Pos value) { - containerAlignmentProperty().set(value); - } - - public final Pos getContainerAlignment() { - return containerAlignment == null ? Pos.TOP_LEFT : containerAlignment.get(); - } - - public final ObjectProperty containerAlignmentProperty() { - if (containerAlignment == null) { - containerAlignment = new StyleableObjectProperty(Pos.TOP_LEFT) { - - @Override - public void invalidated() { - final Pos value = get(); - container.setAlignment(value); - } - - @Override - public Object getBean() { - return MenuBarSkin.this; - } - - @Override - public String getName() { - return "containerAlignment"; - } - - @Override - public CssMetaData getCssMetaData() { - return ALIGNMENT; - } - }; - } - return containerAlignment; - } - - @Override - public void dispose() { - cleanUpSystemMenu(); - // call super.dispose last since it sets control to null - super.dispose(); - } - - private void cleanUpSystemMenu() { - - if (sceneChangeListener != null && getSkinnable() != null) { - getSkinnable().sceneProperty().removeListener(sceneChangeListener); - // rebuildUI creates sceneChangeListener and adds sceneChangeListener to sceneProperty, - // so sceneChangeListener needs to be reset to null in the off chance that this - // skin instance is reused. - sceneChangeListener = null; - } - - if (currentMenuBarStage != null && getMenuBarSkin(currentMenuBarStage) == MenuBarSkin.this) { - setSystemMenu(null); - } - - if (systemMenuMap != null) { - Iterator>> iterator = systemMenuMap.entrySet().iterator(); - while (iterator.hasNext()) { - Map.Entry> entry = iterator.next(); - Reference ref = entry.getValue(); - MenuBarSkin skin = ref != null ? ref.get() : null; - if (skin == null || skin == MenuBarSkin.this) { - iterator.remove(); - } - } - } - } - - private boolean isMenuEmpty(Menu menu) { - boolean retVal = true; - if (menu != null) { - for (MenuItem m : menu.getItems()) { - if (m != null && m.isVisible()) retVal = false; - } - } - return retVal; - } - - private void resetOpenMenu() { - if (openMenu != null) { - openMenu.hide(); - openMenu = null; - openMenuButton = (MenuBarButton)container.getChildren().get(focusedMenuIndex); - openMenuButton.clearHover(); - openMenuButton = null; - menuModeEnd(); - } - } - - private void unSelectMenus() { - clearMenuButtonHover(); - if (focusedMenuIndex == -1) return; - if (openMenu != null) { - openMenu.hide(); - openMenu = null; - } - if (openMenuButton != null) { - openMenuButton.clearHover(); - openMenuButton = null; - } - menuModeEnd(); - } - - private void menuModeStart(int newIndex) { - if (focusedMenuIndex == -1) { - SceneHelper.getSceneAccessor().setTransientFocusContainer(getSkinnable().getScene(), getSkinnable()); - } - focusedMenuIndex = newIndex; - } - - private void menuModeEnd() { - if (focusedMenuIndex != -1) { - SceneHelper.getSceneAccessor().setTransientFocusContainer(getSkinnable().getScene(), null); - - /* Return the a11y focus to a control in the scene. */ - getSkinnable().notifyAccessibleAttributeChanged(AccessibleAttribute.FOCUS_NODE); - } - focusedMenuIndex = -1; - } - - private void selectNextMenu() { - Menu nextMenu = findNextSibling(); - if (nextMenu != null && focusedMenuIndex != -1) { - openMenuButton = (MenuBarButton)container.getChildren().get(focusedMenuIndex); - openMenuButton.setHover(); - openMenu = nextMenu; - } - } - - private void selectPrevMenu() { - Menu prevMenu = findPreviousSibling(); - if (prevMenu != null && focusedMenuIndex != -1) { - openMenuButton = (MenuBarButton)container.getChildren().get(focusedMenuIndex); - openMenuButton.setHover(); - openMenu = prevMenu; - } - } - - private void showNextMenu() { - Menu nextMenu = findNextSibling(); - // hide the currently visible menu, and move to the next one - if (openMenu != null) openMenu.hide(); - openMenu = nextMenu; - if (!isMenuEmpty(nextMenu)) { - openMenu.show(); - } - } - - private void showPrevMenu() { - Menu prevMenu = findPreviousSibling(); - // hide the currently visible menu, and move to the next one - if (openMenu != null) openMenu.hide(); - openMenu = prevMenu; - if (!isMenuEmpty(prevMenu)) { - openMenu.show(); - } - } - - private Menu findPreviousSibling() { - if (focusedMenuIndex == -1) return null; - if (focusedMenuIndex == 0) { - focusedMenuIndex = container.getChildren().size() - 1; - } else { - focusedMenuIndex--; - } - // RT-19359 - if (getSkinnable().getMenus().get(focusedMenuIndex).isDisable()) return findPreviousSibling(); - clearMenuButtonHover(); - return getSkinnable().getMenus().get(focusedMenuIndex); - } - - private Menu findNextSibling() { - if (focusedMenuIndex == -1) return null; - if (focusedMenuIndex == container.getChildren().size() - 1) { - focusedMenuIndex = 0; - } else { - focusedMenuIndex++; - } - // RT_19359 - if (getSkinnable().getMenus().get(focusedMenuIndex).isDisable()) return findNextSibling(); - clearMenuButtonHover(); - return getSkinnable().getMenus().get(focusedMenuIndex); - } - - private void updateFocusedIndex() { - int index = 0; - for(Node n : container.getChildren()) { - if (n.isHover()) { - focusedMenuIndex = index; - return; - } - index++; - } - menuModeEnd(); - } - - private void clearMenuButtonHover() { - for(Node n : container.getChildren()) { - if (n.isHover()) { - ((MenuBarButton)n).clearHover(); - return; - } - } - } - - @Override - public void onTraverse(Node node, Bounds bounds) { - if (openMenu != null) openMenu.hide(); - focusedMenuIndex = 0; - } - - static class MenuBarButton extends MenuButton { - private ChangeListener menuListener; - private MenuBarSkin menuBarSkin; - private Menu menu; - - private final ListChangeListener itemsListener; - private final ListChangeListener styleClassListener; - - public MenuBarButton(MenuBarSkin menuBarSkin, Menu menu) { - super(menu.getText(), menu.getGraphic()); - this.menuBarSkin = menuBarSkin; - setAccessibleRole(AccessibleRole.MENU); - - // listen to changes in menu items & update menuButton items - menu.getItems().addListener(itemsListener = c -> { - while (c.next()) { - getItems().removeAll(c.getRemoved()); - getItems().addAll(c.getFrom(), c.getAddedSubList()); - } - }); - menu.getStyleClass().addListener(styleClassListener = c -> { - while(c.next()) { - for(int i=c.getFrom(); i SPACING = - new CssMetaData("-fx-spacing", - SizeConverter.getInstance(), 0.0) { - - @Override - public boolean isSettable(MenuBar n) { - final MenuBarSkin skin = (MenuBarSkin) n.getSkin(); - return skin.spacing == null || !skin.spacing.isBound(); - } - - @Override - public StyleableProperty getStyleableProperty(MenuBar n) { - final MenuBarSkin skin = (MenuBarSkin) n.getSkin(); - return (StyleableProperty)(WritableValue)skin.spacingProperty(); - } - }; - - private static final CssMetaData ALIGNMENT = - new CssMetaData("-fx-alignment", - new EnumConverter(Pos.class), Pos.TOP_LEFT ) { - - @Override - public boolean isSettable(MenuBar n) { - final MenuBarSkin skin = (MenuBarSkin) n.getSkin(); - return skin.containerAlignment == null || !skin.containerAlignment.isBound(); - } - - @Override - public StyleableProperty getStyleableProperty(MenuBar n) { - final MenuBarSkin skin = (MenuBarSkin) n.getSkin(); - return (StyleableProperty)(WritableValue)skin.containerAlignmentProperty(); - } - }; - - - private static final List> STYLEABLES; - static { - - final List> styleables = - new ArrayList>(SkinBase.getClassCssMetaData()); - - // StackPane also has -fx-alignment. Replace it with - // MenuBarSkin's. - // TODO: Really should be able to reference StackPane.StyleableProperties.ALIGNMENT - final String alignmentProperty = ALIGNMENT.getProperty(); - for (int n=0, nMax=styleables.size(); n prop = styleables.get(n); - if (alignmentProperty.equals(prop.getProperty())) styleables.remove(prop); - } - - styleables.add(SPACING); - styleables.add(ALIGNMENT); - STYLEABLES = Collections.unmodifiableList(styleables); - - } - - /** - * @return The CssMetaData associated with this class, which may include the - * CssMetaData of its super classes. - */ - public static List> getClassCssMetaData() { - return STYLEABLES; - } - - /** - * {@inheritDoc} - */ - @Override - public List> getCssMetaData() { - return getClassCssMetaData(); - } - - /*************************************************************************** - * * - * Accessibility handling * - * * - **************************************************************************/ - - @Override - protected Object queryAccessibleAttribute(AccessibleAttribute attribute, Object... parameters) { - switch (attribute) { - case FOCUS_NODE: return openMenuButton; - default: return super.queryAccessibleAttribute(attribute, parameters); - } - } -} --- /dev/null 2015-09-03 15:20:10.000000000 -0700 +++ new/modules/controls/src/main/java/javafx/scene/control/skin/MenuBarSkin.java 2015-09-03 15:20:08.150096800 -0700 @@ -0,0 +1,1246 @@ +/* + * Copyright (c) 2010, 2015, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +package javafx.scene.control.skin; + +import javafx.css.converter.EnumConverter; +import javafx.css.converter.SizeConverter; +import com.sun.javafx.scene.control.MenuBarButton; +import com.sun.javafx.scene.control.skin.Utils; +import com.sun.javafx.scene.traversal.ParentTraversalEngine; +import javafx.beans.InvalidationListener; +import javafx.beans.property.DoubleProperty; +import javafx.beans.property.ObjectProperty; +import javafx.beans.property.ReadOnlyProperty; +import javafx.beans.value.ChangeListener; +import javafx.beans.value.WeakChangeListener; +import javafx.beans.value.WritableValue; +import javafx.collections.ListChangeListener; +import javafx.collections.MapChangeListener; +import javafx.collections.ObservableList; +import javafx.css.CssMetaData; +import javafx.css.Styleable; +import javafx.css.StyleableDoubleProperty; +import javafx.css.StyleableObjectProperty; +import javafx.css.StyleableProperty; +import javafx.event.ActionEvent; +import javafx.event.EventHandler; +import javafx.event.WeakEventHandler; +import javafx.geometry.NodeOrientation; +import javafx.geometry.Pos; +import javafx.scene.AccessibleAttribute; +import javafx.scene.Node; +import javafx.scene.Scene; +import javafx.scene.control.Control; +import javafx.scene.control.CustomMenuItem; +import javafx.scene.control.Menu; +import javafx.scene.control.MenuBar; +import javafx.scene.control.MenuButton; +import javafx.scene.control.MenuItem; +import javafx.scene.control.SeparatorMenuItem; +import javafx.scene.control.SkinBase; +import javafx.scene.input.KeyCombination; +import javafx.scene.input.KeyEvent; +import javafx.scene.input.MouseEvent; +import javafx.scene.layout.HBox; +import javafx.stage.Stage; + +import java.lang.ref.Reference; +import java.lang.ref.WeakReference; +import java.util.ArrayList; +import java.util.Collections; +import java.util.Iterator; +import java.util.List; +import java.util.Map; +import java.util.WeakHashMap; + +import com.sun.javafx.menu.MenuBase; +import com.sun.javafx.scene.SceneHelper; +import com.sun.javafx.scene.control.GlobalMenuAdapter; +import com.sun.javafx.stage.StageHelper; +import com.sun.javafx.tk.Toolkit; +import javafx.stage.Window; + +/** + * Default skin implementation for the {@link MenuBar} control. In essence it is + * a simple toolbar. For the time being there is no overflow behavior and we just + * hide nodes which fall outside the bounds. + * + * @see MenuBar + * @since 9 + */ +public class MenuBarSkin extends SkinBase { + + /*************************************************************************** + * * + * Private fields * + * * + **************************************************************************/ + + private final HBox container; + + private Menu openMenu; + private MenuBarButton openMenuButton; + private int focusedMenuIndex = -1; + + private static WeakHashMap> systemMenuMap; + private static List wrappedDefaultMenus = new ArrayList<>(); + private static Stage currentMenuBarStage; + private List wrappedMenus; + + private WeakEventHandler weakSceneKeyEventHandler; + private WeakEventHandler weakSceneMouseEventHandler; + private WeakChangeListener weakWindowFocusListener; + private WeakChangeListener weakWindowSceneListener; + private EventHandler keyEventHandler; + private EventHandler mouseEventHandler; + private ChangeListener menuBarFocusedPropertyListener; + private ChangeListener sceneChangeListener; + + private boolean pendingDismiss = false; + + + + /*************************************************************************** + * * + * Listeners / Callbacks * + * * + **************************************************************************/ + + // RT-20411 : reset menu selected/focused state + private EventHandler menuActionEventHandler = t -> { + if (t.getSource() instanceof CustomMenuItem) { + // RT-29614 If CustomMenuItem hideOnClick is false, dont hide + CustomMenuItem cmi = (CustomMenuItem)t.getSource(); + if (!cmi.isHideOnClick()) return; + } + unSelectMenus(); + }; + + private ListChangeListener menuItemListener = (c) -> { + while (c.next()) { + for (MenuItem mi : c.getAddedSubList()) { + mi.addEventHandler(ActionEvent.ACTION, menuActionEventHandler); + } + for (MenuItem mi: c.getRemoved()) { + mi.removeEventHandler(ActionEvent.ACTION, menuActionEventHandler); + } + } + }; + + Runnable firstMenuRunnable = new Runnable() { + public void run() { + /* + ** check that this menubar's container has contents, + ** and that the first item is a MenuButton.... + ** otherwise the transfer is off! + */ + if (container.getChildren().size() > 0) { + if (container.getChildren().get(0) instanceof MenuButton) { +// container.getChildren().get(0).requestFocus(); + if (focusedMenuIndex != 0) { + unSelectMenus(); + menuModeStart(0); + openMenuButton = ((MenuBarButton)container.getChildren().get(0)); + openMenu = getSkinnable().getMenus().get(0); + openMenuButton.setHover(); + } + else { + unSelectMenus(); + } + } + } + } + }; + + + + /*************************************************************************** + * * + * Constructors * + * * + **************************************************************************/ + + /** + * Creates a new MenuBarSkin instance, installing the necessary child + * nodes into the Control {@link Control#getChildren() children} list, as + * well as the necessary {@link Node#getInputMap() input mappings} for + * handling key, mouse, etc events. + * + * @param control The control that this skin should be installed onto. + */ + public MenuBarSkin(final MenuBar control) { + super(control); + + container = new HBox(); + container.getStyleClass().add("container"); + getChildren().add(container); + + // Key navigation + keyEventHandler = event -> { + // process right left and may be tab key events + if (openMenu != null) { + switch (event.getCode()) { + case LEFT: { + boolean isRTL = control.getEffectiveNodeOrientation() == NodeOrientation.RIGHT_TO_LEFT; + if (control.getScene().getWindow().isFocused()) { + if (openMenu == null) return; + if ( !openMenu.isShowing()) { + if (isRTL) { + selectNextMenu(); // just move the selection bar + } else { + selectPrevMenu(); // just move the selection bar + } + event.consume(); + return; + } + if (isRTL) { + showNextMenu(); + } else { + showPrevMenu(); + } + } + event.consume(); + break; + } + case RIGHT: + { + boolean isRTL = control.getEffectiveNodeOrientation() == NodeOrientation.RIGHT_TO_LEFT; + if (control.getScene().getWindow().isFocused()) { + if (openMenu == null) return; + if (! openMenu.isShowing()) { + if (isRTL) { + selectPrevMenu(); // just move the selection bar + } else { + selectNextMenu(); // just move the selection bar + } + event.consume(); + return; + } + if (isRTL) { + showPrevMenu(); + } else { + showNextMenu(); + } + } + event.consume(); + break; + } + case DOWN: + //case SPACE: + //case ENTER: + // RT-18859: Doing nothing for space and enter + if (control.getScene().getWindow().isFocused()) { + if (focusedMenuIndex != -1 && openMenu != null) { + openMenu = getSkinnable().getMenus().get(focusedMenuIndex); + if (!isMenuEmpty(getSkinnable().getMenus().get(focusedMenuIndex))) { + openMenu.show(); + } + event.consume(); + } + } + break; + case ESCAPE: + unSelectMenus(); + event.consume(); + break; + default: + break; + } + } + }; + menuBarFocusedPropertyListener = (ov, t, t1) -> { + if (t1) { + // RT-23147 when MenuBar's focusTraversable is true the first + // menu will visually indicate focus + unSelectMenus(); + menuModeStart(0); + openMenuButton = ((MenuBarButton)container.getChildren().get(0)); + openMenu = getSkinnable().getMenus().get(0); + openMenuButton.setHover(); + } else { + unSelectMenus(); + } + }; + weakSceneKeyEventHandler = new WeakEventHandler(keyEventHandler); + Utils.executeOnceWhenPropertyIsNonNull(control.sceneProperty(), (Scene scene) -> { + scene.addEventFilter(KeyEvent.KEY_PRESSED, weakSceneKeyEventHandler); + }); + + // When we click else where in the scene - menu selection should be cleared. + mouseEventHandler = t -> { + if (!container.localToScreen(container.getLayoutBounds()).contains(t.getScreenX(), t.getScreenY())) { + unSelectMenus(); + } + }; + weakSceneMouseEventHandler = new WeakEventHandler(mouseEventHandler); + Utils.executeOnceWhenPropertyIsNonNull(control.sceneProperty(), (Scene scene) -> { + scene.addEventFilter(MouseEvent.MOUSE_CLICKED, weakSceneMouseEventHandler); + }); + + weakWindowFocusListener = new WeakChangeListener((ov, t, t1) -> { + if (!t1) { + unSelectMenus(); + } + }); + // When the parent window looses focus - menu selection should be cleared + Utils.executeOnceWhenPropertyIsNonNull(control.sceneProperty(), (Scene scene) -> { + if (scene.getWindow() != null) { + scene.getWindow().focusedProperty().addListener(weakWindowFocusListener); + } else { + ChangeListener sceneWindowListener = (observable, oldValue, newValue) -> { + if (oldValue != null) + oldValue.focusedProperty().removeListener(weakWindowFocusListener); + if (newValue != null) + newValue.focusedProperty().addListener(weakWindowFocusListener); + }; + weakWindowSceneListener = new WeakChangeListener<>(sceneWindowListener); + scene.windowProperty().addListener(weakWindowSceneListener); + } + }); + + rebuildUI(); + control.getMenus().addListener((ListChangeListener) c -> { + rebuildUI(); + }); + for (final Menu menu : getSkinnable().getMenus()) { + menu.visibleProperty().addListener((ov, t, t1) -> { + rebuildUI(); + }); + } + + if (Toolkit.getToolkit().getSystemMenu().isSupported()) { + control.useSystemMenuBarProperty().addListener(valueModel -> { + rebuildUI(); + }); + } + + // When the mouse leaves the menu, the last hovered item should lose + // it's focus so that it is no longer selected. This code returns focus + // to the MenuBar itself, such that keyboard navigation can continue. + // fix RT-12254 : menu bar should not request focus on mouse exit. +// addEventFilter(MouseEvent.MOUSE_EXITED, new EventHandler() { +// @Override +// public void handle(MouseEvent event) { +// requestFocus(); +// } +// }); + + /* + ** add an accelerator for F10 on windows and ctrl+F10 on mac/linux + ** pressing f10 will select the first menu button on a menubar + */ + final KeyCombination acceleratorKeyCombo; + if (com.sun.javafx.util.Utils.isMac()) { + acceleratorKeyCombo = KeyCombination.keyCombination("ctrl+F10"); + } else { + acceleratorKeyCombo = KeyCombination.keyCombination("F10"); + } + Utils.executeOnceWhenPropertyIsNonNull(control.sceneProperty(), (Scene scene) -> { + scene.getAccelerators().put(acceleratorKeyCombo, firstMenuRunnable); + + // put focus on the first menu when the alt key is pressed + scene.addEventHandler(KeyEvent.KEY_PRESSED, e -> { + if (e.isAltDown() && !e.isConsumed()) { + firstMenuRunnable.run(); + } + }); + }); + + ParentTraversalEngine engine = new ParentTraversalEngine(getSkinnable()); + engine.addTraverseListener((node, bounds) -> { + if (openMenu != null) openMenu.hide(); + focusedMenuIndex = 0; + }); + getSkinnable().setImpl_traversalEngine(engine); + + control.sceneProperty().addListener((ov, t, t1) -> { + if (weakSceneKeyEventHandler != null) { + // remove event filter from the old scene (t) + if (t != null) + t.removeEventFilter(KeyEvent.KEY_PRESSED, weakSceneKeyEventHandler); + } + if (weakSceneMouseEventHandler != null) { + // remove event filter from the old scene (t) + if (t != null) + t.removeEventFilter(MouseEvent.MOUSE_CLICKED, weakSceneMouseEventHandler); + } + + /** + * remove the f10 accelerator from the old scene + * add it to the new scene + */ + if (t != null) { + t.getAccelerators().remove(acceleratorKeyCombo); + } + if (t1 != null ) { + t1.getAccelerators().put(acceleratorKeyCombo, firstMenuRunnable); + } + }); + } + + + + /*************************************************************************** + * * + * Static methods * + * * + **************************************************************************/ + + // RT-22480: This is intended as private API for SceneBuilder, + // pending fix for RT-19857: Keeping menu in the Mac menu bar when + // there is no more stage + public static void setDefaultSystemMenuBar(final MenuBar menuBar) { + if (Toolkit.getToolkit().getSystemMenu().isSupported()) { + wrappedDefaultMenus.clear(); + for (Menu menu : menuBar.getMenus()) { + wrappedDefaultMenus.add(GlobalMenuAdapter.adapt(menu)); + } + menuBar.getMenus().addListener((ListChangeListener) c -> { + wrappedDefaultMenus.clear(); + for (Menu menu : menuBar.getMenus()) { + wrappedDefaultMenus.add(GlobalMenuAdapter.adapt(menu)); + } + }); + } + } + + private static MenuBarSkin getMenuBarSkin(Stage stage) { + if (systemMenuMap == null) return null; + Reference skinRef = systemMenuMap.get(stage); + return skinRef == null ? null : skinRef.get(); + } + + private static void setSystemMenu(Stage stage) { + if (stage != null && stage.isFocused()) { + while (stage != null && stage.getOwner() instanceof Stage) { + MenuBarSkin skin = getMenuBarSkin(stage); + if (skin != null && skin.wrappedMenus != null) { + break; + } else { + // This is a secondary stage (dialog) that doesn't + // have own menu bar. + // + // Continue looking for a menu bar in the parent stage. + stage = (Stage)stage.getOwner(); + } + } + } else { + stage = null; + } + + if (stage != currentMenuBarStage) { + List menuList = null; + if (stage != null) { + MenuBarSkin skin = getMenuBarSkin(stage); + if (skin != null) { + menuList = skin.wrappedMenus; + } + } + if (menuList == null) { + menuList = wrappedDefaultMenus; + } + Toolkit.getToolkit().getSystemMenu().setMenus(menuList); + currentMenuBarStage = stage; + } + } + + private static void initSystemMenuBar() { + systemMenuMap = new WeakHashMap<>(); + + final InvalidationListener focusedStageListener = ov -> { + setSystemMenu((Stage)((ReadOnlyProperty)ov).getBean()); + }; + + final ObservableList stages = StageHelper.getStages(); + for (Stage stage : stages) { + stage.focusedProperty().addListener(focusedStageListener); + } + stages.addListener((ListChangeListener) c -> { + while (c.next()) { + for (Stage stage : c.getRemoved()) { + stage.focusedProperty().removeListener(focusedStageListener); + } + for (Stage stage : c.getAddedSubList()) { + stage.focusedProperty().addListener(focusedStageListener); + setSystemMenu(stage); + } + } + }); + } + + + + /*************************************************************************** + * * + * Properties * + * * + **************************************************************************/ + + /** + * Specifies the spacing between menu buttons on the MenuBar. + */ + // --- spacing + private DoubleProperty spacing; + public final void setSpacing(double value) { + spacingProperty().set(snapSpace(value)); + } + + public final double getSpacing() { + return spacing == null ? 0.0 : snapSpace(spacing.get()); + } + + public final DoubleProperty spacingProperty() { + if (spacing == null) { + spacing = new StyleableDoubleProperty() { + + @Override + protected void invalidated() { + final double value = get(); + container.setSpacing(value); + } + + @Override + public Object getBean() { + return MenuBarSkin.this; + } + + @Override + public String getName() { + return "spacing"; + } + + @Override + public CssMetaData getCssMetaData() { + return SPACING; + } + }; + } + return spacing; + } + + /** + * Specifies the alignment of the menu buttons inside the MenuBar (by default + * it is Pos.TOP_LEFT). + */ + // --- container alignment + private ObjectProperty containerAlignment; + public final void setContainerAlignment(Pos value) { + containerAlignmentProperty().set(value); + } + + public final Pos getContainerAlignment() { + return containerAlignment == null ? Pos.TOP_LEFT : containerAlignment.get(); + } + + public final ObjectProperty containerAlignmentProperty() { + if (containerAlignment == null) { + containerAlignment = new StyleableObjectProperty(Pos.TOP_LEFT) { + + @Override + public void invalidated() { + final Pos value = get(); + container.setAlignment(value); + } + + @Override + public Object getBean() { + return MenuBarSkin.this; + } + + @Override + public String getName() { + return "containerAlignment"; + } + + @Override + public CssMetaData getCssMetaData() { + return ALIGNMENT; + } + }; + } + return containerAlignment; + } + + + + /*************************************************************************** + * * + * Public API * + * * + **************************************************************************/ + + /** {@inheritDoc} */ + @Override public void dispose() { + cleanUpSystemMenu(); + // call super.dispose last since it sets control to null + super.dispose(); + } + + // Return empty insets when "container" is empty, which happens + // when using the system menu bar. + + /** {@inheritDoc} */ + @Override protected double snappedTopInset() { + return container.getChildren().isEmpty() ? 0 : super.snappedTopInset(); + } + /** {@inheritDoc} */ + @Override protected double snappedBottomInset() { + return container.getChildren().isEmpty() ? 0 : super.snappedBottomInset(); + } + /** {@inheritDoc} */ + @Override protected double snappedLeftInset() { + return container.getChildren().isEmpty() ? 0 : super.snappedLeftInset(); + } + /** {@inheritDoc} */ + @Override protected double snappedRightInset() { + return container.getChildren().isEmpty() ? 0 : super.snappedRightInset(); + } + + /** + * Layout the menu bar. This is a simple horizontal layout like an hbox. + * Any menu items which don't fit into it will simply be made invisible. + */ + /** {@inheritDoc} */ + @Override protected void layoutChildren(final double x, final double y, + final double w, final double h) { + // layout the menus one after another + container.resizeRelocate(x, y, w, h); + } + + /** {@inheritDoc} */ + @Override protected double computeMinWidth(double height, double topInset, double rightInset, double bottomInset, double leftInset) { + return container.minWidth(height) + snappedLeftInset() + snappedRightInset(); + } + + /** {@inheritDoc} */ + @Override protected double computePrefWidth(double height, double topInset, double rightInset, double bottomInset, double leftInset) { + return container.prefWidth(height) + snappedLeftInset() + snappedRightInset(); + } + + /** {@inheritDoc} */ + @Override protected double computeMinHeight(double width, double topInset, double rightInset, double bottomInset, double leftInset) { + return container.minHeight(width) + snappedTopInset() + snappedBottomInset(); + } + + /** {@inheritDoc} */ + @Override protected double computePrefHeight(double width, double topInset, double rightInset, double bottomInset, double leftInset) { + return container.prefHeight(width) + snappedTopInset() + snappedBottomInset(); + } + + // grow horizontally, but not vertically + /** {@inheritDoc} */ + @Override protected double computeMaxHeight(double width, double topInset, double rightInset, double bottomInset, double leftInset) { + return getSkinnable().prefHeight(-1); + } + + + + /*************************************************************************** + * * + * Private implementation * + * * + **************************************************************************/ + + // For testing purpose only. + MenuButton getNodeForMenu(int i) { + if (i < container.getChildren().size()) { + return (MenuBarButton)container.getChildren().get(i); + } + return null; + } + + int getFocusedMenuIndex() { + return focusedMenuIndex; + } + + private boolean menusContainCustomMenuItem() { + for (Menu menu : getSkinnable().getMenus()) { + if (menuContainsCustomMenuItem(menu)) { + System.err.println("Warning: MenuBar ignored property useSystemMenuBar because menus contain CustomMenuItem"); + return true; + } + } + return false; + } + + private boolean menuContainsCustomMenuItem(Menu menu) { + for (MenuItem mi : menu.getItems()) { + if (mi instanceof CustomMenuItem && !(mi instanceof SeparatorMenuItem)) { + return true; + } else if (mi instanceof Menu) { + if (menuContainsCustomMenuItem((Menu)mi)) { + return true; + } + } + } + return false; + } + + private int getMenuBarButtonIndex(MenuBarButton m) { + for (int i= 0; i < container.getChildren().size(); i++) { + MenuBarButton menuButton = (MenuBarButton)container.getChildren().get(i); + if (m == menuButton) { + return i; + } + } + return -1; + } + + private void updateActionListeners(Menu m, boolean add) { + if (add) { + m.getItems().addListener(menuItemListener); + } else { + m.getItems().removeListener(menuItemListener); + } + for (MenuItem mi : m.getItems()) { + if (mi instanceof Menu) { + updateActionListeners((Menu)mi, add); + } else { + if (add) { + mi.addEventHandler(ActionEvent.ACTION, menuActionEventHandler); + } else { + mi.removeEventHandler(ActionEvent.ACTION, menuActionEventHandler); + } + } + } + } + + private void rebuildUI() { + getSkinnable().focusedProperty().removeListener(menuBarFocusedPropertyListener); + for (Menu m : getSkinnable().getMenus()) { + // remove action listeners + updateActionListeners(m, false); + } + for (Node n : container.getChildren()) { + // Stop observing menu's showing & disable property for changes. + // Need to unbind before clearing container's children. + MenuBarButton menuButton = (MenuBarButton)n; + menuButton.hide(); + menuButton.menu.showingProperty().removeListener(menuButton.menuListener); + menuButton.disableProperty().unbind(); + menuButton.textProperty().unbind(); + menuButton.graphicProperty().unbind(); + menuButton.styleProperty().unbind(); + + menuButton.dispose(); + + // RT-29729 : old instance of context menu window/popup for this MenuButton needs + // to be cleaned up. Setting the skin to null - results in a call to dispose() + // on the skin which in this case MenuButtonSkinBase - does the subsequent + // clean up to ContextMenu/popup window. + menuButton.setSkin(null); + menuButton = null; + } + container.getChildren().clear(); + + if (Toolkit.getToolkit().getSystemMenu().isSupported()) { + + final Scene scene = getSkinnable().getScene(); + if (scene != null) { + // RT-36554 - make sure system menu is updated when this MenuBar's scene changes. + if (sceneChangeListener == null) { + sceneChangeListener = (observable, oldValue, newValue) -> { + + if (oldValue != null) { + if (oldValue.getWindow() instanceof Stage) { + final Stage stage = (Stage) oldValue.getWindow(); + final MenuBarSkin curMBSkin = getMenuBarSkin(stage); + if (curMBSkin == MenuBarSkin.this) { + curMBSkin.wrappedMenus = null; + systemMenuMap.remove(stage); + if (currentMenuBarStage == stage) { + currentMenuBarStage = null; + setSystemMenu(stage); + } + } else { + if (curMBSkin != null && curMBSkin.getSkinnable() != null && + curMBSkin.getSkinnable().isUseSystemMenuBar()) { + curMBSkin.getSkinnable().setUseSystemMenuBar(false); + } + } + } + } + + if (newValue != null) { + if (getSkinnable().isUseSystemMenuBar() && !menusContainCustomMenuItem()) { + if (newValue.getWindow() instanceof Stage) { + final Stage stage = (Stage) newValue.getWindow(); + if (systemMenuMap == null) { + initSystemMenuBar(); + } + wrappedMenus = new ArrayList<>(); + systemMenuMap.put(stage, new WeakReference<>(this)); + for (Menu menu : getSkinnable().getMenus()) { + wrappedMenus.add(GlobalMenuAdapter.adapt(menu)); + } + currentMenuBarStage = null; + setSystemMenu(stage); + + // TODO: Why two request layout calls here? + getSkinnable().requestLayout(); + javafx.application.Platform.runLater(() -> getSkinnable().requestLayout()); + } + } + } + }; + getSkinnable().sceneProperty().addListener(sceneChangeListener); + } + + // Fake a change event to trigger an update to the system menu. + sceneChangeListener.changed(getSkinnable().sceneProperty(), scene, scene); + + // If the system menu references this MenuBarSkin, then we're done with rebuilding the UI. + // If the system menu does not reference this MenuBarSkin, then the MenuBar is a child of the scene + // and we continue with the update. + // If there is no system menu but this skinnable uses the system menu bar, then the + // stage just isn't focused yet (see setSystemMenu) and we're done rebuilding the UI. + if (currentMenuBarStage != null ? getMenuBarSkin(currentMenuBarStage) == MenuBarSkin.this : getSkinnable().isUseSystemMenuBar()) { + return; + } + + } else { + // if scene is null, make sure this MenuBarSkin isn't left behind as the system menu + if (currentMenuBarStage != null) { + final MenuBarSkin curMBSkin = getMenuBarSkin(currentMenuBarStage); + if (curMBSkin == MenuBarSkin.this) { + setSystemMenu(null); + } + } + } + } + + getSkinnable().focusedProperty().addListener(menuBarFocusedPropertyListener); + for (final Menu menu : getSkinnable().getMenus()) { + if (!menu.isVisible()) continue; + final MenuBarButton menuButton = new MenuBarButton(this, menu); + menuButton.setFocusTraversable(false); + menuButton.getStyleClass().add("menu"); + menuButton.setStyle(menu.getStyle()); // copy style + + menuButton.getItems().setAll(menu.getItems()); + container.getChildren().add(menuButton); + + menuButton.menuListener = (observable, oldValue, newValue) -> { + if (menu.isShowing()) { + menuButton.show(); + menuModeStart(container.getChildren().indexOf(menuButton)); + } else { + menuButton.hide(); + } + }; + menuButton.menu = menu; + menu.showingProperty().addListener(menuButton.menuListener); + menuButton.disableProperty().bindBidirectional(menu.disableProperty()); + menuButton.textProperty().bind(menu.textProperty()); + menuButton.graphicProperty().bind(menu.graphicProperty()); + menuButton.styleProperty().bind(menu.styleProperty()); + menuButton.getProperties().addListener((MapChangeListener) c -> { + if (c.wasAdded() && MenuButtonSkin.AUTOHIDE.equals(c.getKey())) { + menuButton.getProperties().remove(MenuButtonSkin.AUTOHIDE); + menu.hide(); + } + }); + menuButton.showingProperty().addListener((observable, oldValue, isShowing) -> { + if (isShowing) { + if (openMenuButton != null && openMenuButton != menuButton) { + openMenuButton.hide(); + } + openMenuButton = menuButton; + openMenu = menu; + if (!menu.isShowing())menu.show(); + } + }); + + menuButton.setOnMousePressed(event -> { + pendingDismiss = menuButton.isShowing(); + + // check if the owner window has focus + if (menuButton.getScene().getWindow().isFocused()) { + openMenu = menu; + if (!isMenuEmpty(menu)){ + openMenu.show(); + } + // update FocusedIndex + menuModeStart(getMenuBarButtonIndex(menuButton)); + } + }); + + menuButton.setOnMouseReleased(event -> { + // check if the owner window has focus + if (menuButton.getScene().getWindow().isFocused()) { + if (pendingDismiss) { + resetOpenMenu(); +// menuButton.hide(); + } + } + pendingDismiss = false; + }); + +// menuButton. setOnKeyPressed(new EventHandler() { +// @Override public void handle(javafx.scene.input.KeyEvent ke) { +// switch (ke.getCode()) { +// case LEFT: +// if (menuButton.getScene().getWindow().isFocused()) { +// Menu prevMenu = findPreviousSibling(); +// if (openMenu == null || ! openMenu.isShowing()) { +// return; +// } +//// if (focusedMenuIndex == container.getChildren().size() - 1) { +//// ((MenuBarButton)container.getChildren().get(focusedMenuIndex)).requestFocus(); +//// } +// // hide the currently visible menu, and move to the previous one +// openMenu.hide(); +// if (!isMenuEmpty(prevMenu)) { +// openMenu = prevMenu; +// openMenu.show(); +// } else { +// openMenu = null; +// } +// } +// ke.consume(); +// break; +// case RIGHT: +// if (menuButton.getScene().getWindow().isFocused()) { +// Menu nextMenu = findNextSibling(); +// if (openMenu == null || ! openMenu.isShowing()) { +// return; +// } +//// if (focusedMenuIndex == 0) { +//// ((MenuBarButton)container.getChildren().get(focusedMenuIndex)).requestFocus(); +//// } +// // hide the currently visible menu, and move to the next one +// openMenu.hide(); +// if (!isMenuEmpty(nextMenu)) { +// openMenu = nextMenu; +// openMenu.show(); +// } else { +// openMenu = null; +// } +// } +// ke.consume(); +// break; +// +// case DOWN: +// case SPACE: +// case ENTER: +// if (menuButton.getScene().getWindow().isFocused()) { +// if (focusedMenuIndex != -1) { +// if (!isMenuEmpty(getSkinnable().getMenus().get(focusedMenuIndex))) { +// openMenu = getSkinnable().getMenus().get(focusedMenuIndex); +// openMenu.show(); +// } else { +// openMenu = null; +// } +// ke.consume(); +// } +// } +// break; +// } +// } +// }); + menuButton.setOnMouseEntered(event -> { + // check if the owner window has focus + if (menuButton.getScene() != null && menuButton.getScene().getWindow() != null && + menuButton.getScene().getWindow().isFocused()) { + if (openMenuButton != null && openMenuButton != menuButton) { + openMenuButton.clearHover(); + openMenuButton = null; + openMenuButton = menuButton; + } + updateFocusedIndex(); + if (openMenu != null && openMenu != menu) { + // hide the currently visible menu, and move to the new one + openMenu.hide(); + openMenu = menu; + updateFocusedIndex(); + if (!isMenuEmpty(menu)) { + openMenu.show(); + } + } + } + }); + updateActionListeners(menu, true); + } + getSkinnable().requestLayout(); + } + + private void cleanUpSystemMenu() { + if (sceneChangeListener != null && getSkinnable() != null) { + getSkinnable().sceneProperty().removeListener(sceneChangeListener); + // rebuildUI creates sceneChangeListener and adds sceneChangeListener to sceneProperty, + // so sceneChangeListener needs to be reset to null in the off chance that this + // skin instance is reused. + sceneChangeListener = null; + } + + if (currentMenuBarStage != null && getMenuBarSkin(currentMenuBarStage) == MenuBarSkin.this) { + setSystemMenu(null); + } + + if (systemMenuMap != null) { + Iterator>> iterator = systemMenuMap.entrySet().iterator(); + while (iterator.hasNext()) { + Map.Entry> entry = iterator.next(); + Reference ref = entry.getValue(); + MenuBarSkin skin = ref != null ? ref.get() : null; + if (skin == null || skin == MenuBarSkin.this) { + iterator.remove(); + } + } + } + } + + private boolean isMenuEmpty(Menu menu) { + boolean retVal = true; + if (menu != null) { + for (MenuItem m : menu.getItems()) { + if (m != null && m.isVisible()) retVal = false; + } + } + return retVal; + } + + private void resetOpenMenu() { + if (openMenu != null) { + openMenu.hide(); + openMenu = null; + openMenuButton = (MenuBarButton)container.getChildren().get(focusedMenuIndex); + openMenuButton.clearHover(); + openMenuButton = null; + menuModeEnd(); + } + } + + private void unSelectMenus() { + clearMenuButtonHover(); + if (focusedMenuIndex == -1) return; + if (openMenu != null) { + openMenu.hide(); + openMenu = null; + } + if (openMenuButton != null) { + openMenuButton.clearHover(); + openMenuButton = null; + } + menuModeEnd(); + } + + private void menuModeStart(int newIndex) { + if (focusedMenuIndex == -1) { + SceneHelper.getSceneAccessor().setTransientFocusContainer(getSkinnable().getScene(), getSkinnable()); + } + focusedMenuIndex = newIndex; + } + + private void menuModeEnd() { + if (focusedMenuIndex != -1) { + SceneHelper.getSceneAccessor().setTransientFocusContainer(getSkinnable().getScene(), null); + + /* Return the a11y focus to a control in the scene. */ + getSkinnable().notifyAccessibleAttributeChanged(AccessibleAttribute.FOCUS_NODE); + } + focusedMenuIndex = -1; + } + + private void selectNextMenu() { + Menu nextMenu = findNextSibling(); + if (nextMenu != null && focusedMenuIndex != -1) { + openMenuButton = (MenuBarButton)container.getChildren().get(focusedMenuIndex); + openMenuButton.setHover(); + openMenu = nextMenu; + } + } + + private void selectPrevMenu() { + Menu prevMenu = findPreviousSibling(); + if (prevMenu != null && focusedMenuIndex != -1) { + openMenuButton = (MenuBarButton)container.getChildren().get(focusedMenuIndex); + openMenuButton.setHover(); + openMenu = prevMenu; + } + } + + private void showNextMenu() { + Menu nextMenu = findNextSibling(); + // hide the currently visible menu, and move to the next one + if (openMenu != null) openMenu.hide(); + openMenu = nextMenu; + if (!isMenuEmpty(nextMenu)) { + openMenu.show(); + } + } + + private void showPrevMenu() { + Menu prevMenu = findPreviousSibling(); + // hide the currently visible menu, and move to the next one + if (openMenu != null) openMenu.hide(); + openMenu = prevMenu; + if (!isMenuEmpty(prevMenu)) { + openMenu.show(); + } + } + + private Menu findPreviousSibling() { + if (focusedMenuIndex == -1) return null; + if (focusedMenuIndex == 0) { + focusedMenuIndex = container.getChildren().size() - 1; + } else { + focusedMenuIndex--; + } + // RT-19359 + if (getSkinnable().getMenus().get(focusedMenuIndex).isDisable()) return findPreviousSibling(); + clearMenuButtonHover(); + return getSkinnable().getMenus().get(focusedMenuIndex); + } + + private Menu findNextSibling() { + if (focusedMenuIndex == -1) return null; + if (focusedMenuIndex == container.getChildren().size() - 1) { + focusedMenuIndex = 0; + } else { + focusedMenuIndex++; + } + // RT_19359 + if (getSkinnable().getMenus().get(focusedMenuIndex).isDisable()) return findNextSibling(); + clearMenuButtonHover(); + return getSkinnable().getMenus().get(focusedMenuIndex); + } + + private void updateFocusedIndex() { + int index = 0; + for(Node n : container.getChildren()) { + if (n.isHover()) { + focusedMenuIndex = index; + return; + } + index++; + } + menuModeEnd(); + } + + private void clearMenuButtonHover() { + for(Node n : container.getChildren()) { + if (n.isHover()) { + ((MenuBarButton)n).clearHover(); + return; + } + } + } + + + + /*************************************************************************** + * * + * CSS * + * * + **************************************************************************/ + + private static final CssMetaData SPACING = + new CssMetaData("-fx-spacing", + SizeConverter.getInstance(), 0.0) { + + @Override + public boolean isSettable(MenuBar n) { + final MenuBarSkin skin = (MenuBarSkin) n.getSkin(); + return skin.spacing == null || !skin.spacing.isBound(); + } + + @Override + public StyleableProperty getStyleableProperty(MenuBar n) { + final MenuBarSkin skin = (MenuBarSkin) n.getSkin(); + return (StyleableProperty)(WritableValue)skin.spacingProperty(); + } + }; + + private static final CssMetaData ALIGNMENT = + new CssMetaData("-fx-alignment", + new EnumConverter(Pos.class), Pos.TOP_LEFT ) { + + @Override + public boolean isSettable(MenuBar n) { + final MenuBarSkin skin = (MenuBarSkin) n.getSkin(); + return skin.containerAlignment == null || !skin.containerAlignment.isBound(); + } + + @Override + public StyleableProperty getStyleableProperty(MenuBar n) { + final MenuBarSkin skin = (MenuBarSkin) n.getSkin(); + return (StyleableProperty)(WritableValue)skin.containerAlignmentProperty(); + } + }; + + + private static final List> STYLEABLES; + static { + + final List> styleables = + new ArrayList>(SkinBase.getClassCssMetaData()); + + // StackPane also has -fx-alignment. Replace it with + // MenuBarSkin's. + // TODO: Really should be able to reference StackPane.StyleableProperties.ALIGNMENT + final String alignmentProperty = ALIGNMENT.getProperty(); + for (int n=0, nMax=styleables.size(); n prop = styleables.get(n); + if (alignmentProperty.equals(prop.getProperty())) styleables.remove(prop); + } + + styleables.add(SPACING); + styleables.add(ALIGNMENT); + STYLEABLES = Collections.unmodifiableList(styleables); + + } + + /** + * Returns the CssMetaData associated with this class, which may include the + * CssMetaData of its super classes. + */ + public static List> getClassCssMetaData() { + return STYLEABLES; + } + + /** + * {@inheritDoc} + */ + @Override + public List> getCssMetaData() { + return getClassCssMetaData(); + } + + /*************************************************************************** + * * + * Accessibility handling * + * * + **************************************************************************/ + + /** {@inheritDoc} */ + @Override protected Object queryAccessibleAttribute(AccessibleAttribute attribute, Object... parameters) { + switch (attribute) { + case FOCUS_NODE: return openMenuButton; + default: return super.queryAccessibleAttribute(attribute, parameters); + } + } +}