/*
* Copyright (c) 2010, 2017, 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 static com.sun.javafx.FXPermissions.ACCESS_WINDOW_LIST_PERMISSION;
import com.sun.javafx.scene.traversal.Direction;
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.Bounds;
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.Skin;
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 static javafx.scene.input.KeyCode.*;
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.Optional;
import java.util.WeakHashMap;
import com.sun.javafx.menu.MenuBase;
import com.sun.javafx.scene.ParentHelper;
import com.sun.javafx.scene.SceneHelper;
import com.sun.javafx.scene.control.GlobalMenuAdapter;
import com.sun.javafx.tk.Toolkit;
import java.util.function.Predicate;
import javafx.stage.Window;
import javafx.util.Pair;
import java.security.AccessController;
import java.security.PrivilegedAction;
Default skin implementation for the 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 Also: Since: 9
/**
* 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<MenuBar> {
private static final ObservableList<Window> stages;
static {
final Predicate<Window> findStage = (w) -> w instanceof Stage;
ObservableList<Window> windows = AccessController.doPrivileged(
(PrivilegedAction<ObservableList<Window>>) () -> Window.getWindows(),
null,
ACCESS_WINDOW_LIST_PERMISSION);
stages = windows.filtered(findStage);
}
*
Private fields *
*
/***************************************************************************
* *
* Private fields *
* *
**************************************************************************/
private final HBox container;
// represents the currently _open_ menu
private Menu openMenu;
private MenuBarButton openMenuButton;
// represents the currently _focused_ menu. If openMenu is non-null, this should equal
// openMenu. If openMenu is null, this can be any menu in the menu bar.
private Menu focusedMenu;
private int focusedMenuIndex = -1;
private static WeakHashMap<Stage, Reference<MenuBarSkin>> systemMenuMap;
private static List<MenuBase> wrappedDefaultMenus = new ArrayList<>();
private static Stage currentMenuBarStage;
private List<MenuBase> wrappedMenus;
private WeakEventHandler<KeyEvent> weakSceneKeyEventHandler;
private WeakEventHandler<MouseEvent> weakSceneMouseEventHandler;
private WeakEventHandler<KeyEvent> weakSceneAltKeyEventHandler;
private WeakChangeListener<Boolean> weakWindowFocusListener;
private WeakChangeListener<Window> weakWindowSceneListener;
private EventHandler<KeyEvent> keyEventHandler;
private EventHandler<KeyEvent> altKeyEventHandler;
private EventHandler<MouseEvent> mouseEventHandler;
private ChangeListener<Boolean> menuBarFocusedPropertyListener;
private ChangeListener<Scene> sceneChangeListener;
private ChangeListener<Boolean> menuVisibilityChangeListener;
private boolean pendingDismiss = false;
private boolean altKeyPressed = false;
*
Listeners / Callbacks *
*
/***************************************************************************
* *
* Listeners / Callbacks *
* *
**************************************************************************/
// RT-20411 : reset menu selected/focused state
private EventHandler<ActionEvent> 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<MenuItem> menuItemListener = (c) -> {
while (c.next()) {
for (MenuItem mi : c.getAddedSubList()) {
updateActionListeners(mi, true);
}
for (MenuItem mi: c.getRemoved()) {
updateActionListeners(mi, false);
}
}
};
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 children
list, as well as the necessary input mappings for handling key, mouse, etc events. Params: - control – The control that this skin should be installed onto.
/**
* Creates a new MenuBarSkin instance, installing the necessary child
* nodes into the Control {@link Control#getChildren() children} list, as
* well as the necessary 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 (focusedMenu != null) {
switch (event.getCode()) {
case LEFT: {
boolean isRTL = control.getEffectiveNodeOrientation() == NodeOrientation.RIGHT_TO_LEFT;
if (control.getScene().getWindow().isFocused()) {
if (openMenu != null && !openMenu.isShowing()) {
if (isRTL) {
moveToMenu(Direction.NEXT, false); // just move the selection bar
} else {
moveToMenu(Direction.PREVIOUS, false); // just move the selection bar
}
event.consume();
return;
}
if (isRTL) {
moveToMenu(Direction.NEXT, true);
} else {
moveToMenu(Direction.PREVIOUS, true);
}
}
event.consume();
break;
}
case RIGHT:
{
boolean isRTL = control.getEffectiveNodeOrientation() == NodeOrientation.RIGHT_TO_LEFT;
if (control.getScene().getWindow().isFocused()) {
if (openMenu != null && !openMenu.isShowing()) {
if (isRTL) {
moveToMenu(Direction.PREVIOUS, false); // just move the selection bar
} else {
moveToMenu(Direction.NEXT, false); // just move the selection bar
}
event.consume();
return;
}
if (isRTL) {
moveToMenu(Direction.PREVIOUS, true);
} else {
moveToMenu(Direction.NEXT, true);
}
}
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) {
Menu menuToOpen = getSkinnable().getMenus().get(focusedMenuIndex);
showMenu(menuToOpen, true);
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));
setFocusedMenuIndex(0);
openMenuButton.setHover();
} else {
unSelectMenus();
}
};
weakSceneKeyEventHandler = new WeakEventHandler<KeyEvent>(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 -> {
Bounds containerScreenBounds = container.localToScreen(container.getLayoutBounds());
if (containerScreenBounds == null || !containerScreenBounds.contains(t.getScreenX(), t.getScreenY())) {
unSelectMenus();
}
};
weakSceneMouseEventHandler = new WeakEventHandler<MouseEvent>(mouseEventHandler);
Utils.executeOnceWhenPropertyIsNonNull(control.sceneProperty(), (Scene scene) -> {
scene.addEventFilter(MouseEvent.MOUSE_CLICKED, weakSceneMouseEventHandler);
});
weakWindowFocusListener = new WeakChangeListener<Boolean>((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<Window> 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);
}
});
menuVisibilityChangeListener = (ov, t, t1) -> {
rebuildUI();
};
rebuildUI();
control.getMenus().addListener((ListChangeListener<Menu>) c -> {
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<MouseEvent>() {
// @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");
}
altKeyEventHandler = e -> {
if (e.getEventType() == KeyEvent.KEY_PRESSED) {
// Clear menu selection when ALT is pressed by itself
altKeyPressed = false;
if (e.getCode() == ALT && !e.isConsumed()) {
if (focusedMenuIndex == -1) {
altKeyPressed = true;
}
unSelectMenus();
}
} else if (e.getEventType() == KeyEvent.KEY_RELEASED) {
// Put focus on the first menu when ALT is released
// directly after being pressed by itself
if (altKeyPressed && e.getCode() == ALT && !e.isConsumed()) {
firstMenuRunnable.run();
}
altKeyPressed = false;
}
};
weakSceneAltKeyEventHandler = new WeakEventHandler<>(altKeyEventHandler);
Utils.executeOnceWhenPropertyIsNonNull(control.sceneProperty(), (Scene scene) -> {
scene.getAccelerators().put(acceleratorKeyCombo, firstMenuRunnable);
scene.addEventHandler(KeyEvent.ANY, weakSceneAltKeyEventHandler);
});
ParentTraversalEngine engine = new ParentTraversalEngine(getSkinnable());
engine.addTraverseListener((node, bounds) -> {
if (openMenu != null) openMenu.hide();
setFocusedMenuIndex(0);
});
ParentHelper.setTraversalEngine(getSkinnable(), engine);
control.sceneProperty().addListener((ov, t, t1) -> {
// remove event handlers / filters from the old scene (t)
if (t != null) {
if (weakSceneKeyEventHandler != null) {
t.removeEventFilter(KeyEvent.KEY_PRESSED, weakSceneKeyEventHandler);
}
if (weakSceneMouseEventHandler != null) {
t.removeEventFilter(MouseEvent.MOUSE_CLICKED, weakSceneMouseEventHandler);
}
if (weakSceneAltKeyEventHandler != null) {
t.removeEventHandler(KeyEvent.ANY, weakSceneAltKeyEventHandler);
}
}
/**
* 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);
}
});
}
private void showMenu(Menu menu) {
showMenu(menu, false);
}
private void showMenu(Menu menu, boolean selectFirstItem) {
// hide the currently visible menu, and move to the next one
if (openMenu == menu) return;
if (openMenu != null) {
openMenu.hide();
}
openMenu = menu;
if (!menu.isShowing() && !isMenuEmpty(menu)) {
if (selectFirstItem) {
// put selection / focus on first item in menu
MenuButton menuButton = getNodeForMenu(focusedMenuIndex);
Skin<?> skin = menuButton.getSkin();
if (skin instanceof MenuButtonSkinBase) {
((MenuButtonSkinBase)skin).requestFocusOnFirstMenuItem();
}
}
openMenu.show();
}
}
private void setFocusedMenuIndex(int index) {
this.focusedMenuIndex = index;
focusedMenu = index == -1 ? null : getSkinnable().getMenus().get(index);
if (focusedMenu != null && focusedMenuIndex != -1) {
openMenuButton = (MenuBarButton)container.getChildren().get(focusedMenuIndex);
openMenuButton.setHover();
}
}
/***************************************************************************
* *
* 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
Set the default system menu bar. This allows an application to keep menu
in the system menu bar after the last Window is closed.
Params: - menuBar – the menu bar
/**
* Set the default system menu bar. This allows an application to keep menu
* in the system menu bar after the last Window is closed.
* @param menuBar the menu bar
*/
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<Menu>) 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<MenuBarSkin> 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<MenuBase> 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());
};
for (Window stage : stages) {
stage.focusedProperty().addListener(focusedStageListener);
}
stages.addListener((ListChangeListener<Window>) c -> {
while (c.next()) {
for (Window stage : c.getRemoved()) {
stage.focusedProperty().removeListener(focusedStageListener);
}
for (Window stage : c.getAddedSubList()) {
stage.focusedProperty().addListener(focusedStageListener);
setSystemMenu((Stage) stage);
}
}
});
}
/***************************************************************************
* *
* Properties *
* *
**************************************************************************/
Specifies the spacing between menu buttons on the MenuBar.
/**
* Specifies the spacing between menu buttons on the MenuBar.
*/
// --- spacing
private DoubleProperty spacing;
public final void setSpacing(double value) {
spacingProperty().set(snapSpaceX(value));
}
public final double getSpacing() {
return spacing == null ? 0.0 : snapSpaceX(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<MenuBar,Number> getCssMetaData() {
return SPACING;
}
};
}
return spacing;
}
Specifies the alignment of the menu buttons inside the MenuBar (by default
it is Pos.TOP_LEFT).
/**
* Specifies the alignment of the menu buttons inside the MenuBar (by default
* it is Pos.TOP_LEFT).
*/
// --- container alignment
private ObjectProperty<Pos> 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<Pos> containerAlignmentProperty() {
if (containerAlignment == null) {
containerAlignment = new StyleableObjectProperty<Pos>(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<MenuBar,Pos> getCssMetaData() {
return ALIGNMENT;
}
};
}
return containerAlignment;
}
/***************************************************************************
* *
* Public API *
* *
**************************************************************************/
{@inheritDoc} /** {@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} /** {@inheritDoc} */
@Override protected double snappedTopInset() {
return container.getChildren().isEmpty() ? 0 : super.snappedTopInset();
}
{@inheritDoc} /** {@inheritDoc} */
@Override protected double snappedBottomInset() {
return container.getChildren().isEmpty() ? 0 : super.snappedBottomInset();
}
{@inheritDoc} /** {@inheritDoc} */
@Override protected double snappedLeftInset() {
return container.getChildren().isEmpty() ? 0 : super.snappedLeftInset();
}
{@inheritDoc} /** {@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} /** {@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} /** {@inheritDoc} */
@Override protected double computeMinWidth(double height, double topInset, double rightInset, double bottomInset, double leftInset) {
return container.minWidth(height) + snappedLeftInset() + snappedRightInset();
}
{@inheritDoc} /** {@inheritDoc} */
@Override protected double computePrefWidth(double height, double topInset, double rightInset, double bottomInset, double leftInset) {
return container.prefWidth(height) + snappedLeftInset() + snappedRightInset();
}
{@inheritDoc} /** {@inheritDoc} */
@Override protected double computeMinHeight(double width, double topInset, double rightInset, double bottomInset, double leftInset) {
return container.minHeight(width) + snappedTopInset() + snappedBottomInset();
}
{@inheritDoc} /** {@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} /** {@inheritDoc} */
@Override protected double computeMaxHeight(double width, double topInset, double rightInset, double bottomInset, double leftInset) {
return getSkinnable().prefHeight(-1);
}
*
Private implementation *
*
/***************************************************************************
* *
* 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(MenuItem item, boolean add) {
if (item instanceof Menu) {
Menu menu = (Menu) item;
if (add) {
menu.getItems().addListener(menuItemListener);
} else {
menu.getItems().removeListener(menuItemListener);
}
for (MenuItem mi : menu.getItems()) {
updateActionListeners(mi, add);
}
} else {
if (add) {
item.addEventHandler(ActionEvent.ACTION, menuActionEventHandler);
} else {
item.removeEventHandler(ActionEvent.ACTION, menuActionEventHandler);
}
}
}
private void rebuildUI() {
getSkinnable().focusedProperty().removeListener(menuBarFocusedPropertyListener);
for (Menu m : getSkinnable().getMenus()) {
// remove action listeners
updateActionListeners(m, false);
m.visibleProperty().removeListener(menuVisibilityChangeListener);
}
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()) {
menu.visibleProperty().addListener(menuVisibilityChangeListener);
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<Object, Object>) 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 && focusedMenuIndex != -1)
openMenuButton = (MenuBarButton)container.getChildren().get(focusedMenuIndex);
if (openMenuButton != null && openMenuButton != menuButton) {
openMenuButton.clearHover();
}
openMenuButton = menuButton;
showMenu(menu);
} else {
// Fix for JDK-8167138 - we need to clear out the openMenu / openMenuButton
// when the menu is hidden (e.g. via autoHide), so that we can open it again
// the next time (if it is the first menu requested to show)
openMenu = null;
openMenuButton = null;
}
});
menuButton.setOnMousePressed(event -> {
pendingDismiss = menuButton.isShowing();
// check if the owner window has focus
if (menuButton.getScene().getWindow().isFocused()) {
showMenu(menu);
// update FocusedIndex
menuModeStart(getMenuBarButtonIndex(menuButton));
}
});
menuButton.setOnMouseReleased(event -> {
// check if the owner window has focus
if (menuButton.getScene().getWindow().isFocused()) {
if (pendingDismiss) {
resetOpenMenu();
}
}
pendingDismiss = false;
});
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) {
showMenu(menu);
}
}
});
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<Map.Entry<Stage,Reference<MenuBarSkin>>> iterator = systemMenuMap.entrySet().iterator();
while (iterator.hasNext()) {
Map.Entry<Stage,Reference<MenuBarSkin>> entry = iterator.next();
Reference<MenuBarSkin> 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;
}
}
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());
}
setFocusedMenuIndex(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);
}
setFocusedMenuIndex(-1);
}
private void moveToMenu(Direction dir, boolean doShow) {
boolean showNextMenu = doShow && focusedMenu.isShowing();
findSibling(dir, focusedMenuIndex).ifPresent(p -> {
setFocusedMenuIndex(p.getValue());
if (showNextMenu) {
// we explicitly do *not* allow selection - we are moving
// to a sibling menu, and therefore selection should be reset
showMenu(p.getKey(), false);
}
});
}
private Optional<Pair<Menu,Integer>> findSibling(Direction dir, int startIndex) {
if (startIndex == -1) {
return Optional.empty();
}
final int totalMenus = getSkinnable().getMenus().size();
int i = 0;
int nextIndex = 0;
// Traverse all menus in menubar to find nextIndex
while (i < totalMenus) {
i++;
nextIndex = (startIndex + (dir.isForward() ? 1 : -1)) % totalMenus;
if (nextIndex == -1) {
// loop backwards to end
nextIndex = totalMenus - 1;
}
// if menu at nextIndex is disabled, skip it
if (getSkinnable().getMenus().get(nextIndex).isDisable()) {
// Calculate new nextIndex by continuing loop
startIndex = nextIndex;
} else {
// nextIndex is to be highlighted
break;
}
}
clearMenuButtonHover();
return Optional.of(new Pair<>(getSkinnable().getMenus().get(nextIndex), nextIndex));
}
private void updateFocusedIndex() {
int index = 0;
for(Node n : container.getChildren()) {
if (n.isHover()) {
setFocusedMenuIndex(index);
return;
}
index++;
}
menuModeEnd();
}
private void clearMenuButtonHover() {
for(Node n : container.getChildren()) {
if (n.isHover()) {
((MenuBarButton)n).clearHover();
((MenuBarButton)n).disarm();
return;
}
}
}
*
CSS *
*
/***************************************************************************
* *
* CSS *
* *
**************************************************************************/
private static final CssMetaData<MenuBar,Number> SPACING =
new CssMetaData<MenuBar,Number>("-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<Number> getStyleableProperty(MenuBar n) {
final MenuBarSkin skin = (MenuBarSkin) n.getSkin();
return (StyleableProperty<Number>)(WritableValue<Number>)skin.spacingProperty();
}
};
private static final CssMetaData<MenuBar,Pos> ALIGNMENT =
new CssMetaData<MenuBar,Pos>("-fx-alignment",
new EnumConverter<Pos>(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<Pos> getStyleableProperty(MenuBar n) {
final MenuBarSkin skin = (MenuBarSkin) n.getSkin();
return (StyleableProperty<Pos>)(WritableValue<Pos>)skin.containerAlignmentProperty();
}
};
private static final List<CssMetaData<? extends Styleable, ?>> STYLEABLES;
static {
final List<CssMetaData<? extends Styleable, ?>> styleables =
new ArrayList<CssMetaData<? extends Styleable, ?>>(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<nMax; n++) {
final CssMetaData<?,?> 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 superclasses.
Returns: the CssMetaData associated with this class, which may include the
CssMetaData of its superclasses
/**
* Returns the CssMetaData associated with this class, which may include the
* CssMetaData of its superclasses.
* @return the CssMetaData associated with this class, which may include the
* CssMetaData of its superclasses
*/
public static List<CssMetaData<? extends Styleable, ?>> getClassCssMetaData() {
return STYLEABLES;
}
{@inheritDoc}
/**
* {@inheritDoc}
*/
@Override
public List<CssMetaData<? extends Styleable, ?>> getCssMetaData() {
return getClassCssMetaData();
}
/***************************************************************************
* *
* Accessibility handling *
* *
**************************************************************************/
{@inheritDoc} /** {@inheritDoc} */
@Override protected Object queryAccessibleAttribute(AccessibleAttribute attribute, Object... parameters) {
switch (attribute) {
case FOCUS_NODE: return openMenuButton;
default: return super.queryAccessibleAttribute(attribute, parameters);
}
}
}