All Downloads are FREE. Search and download functionalities are using the official Maven repository.

javafx.scene.control.skin.MenuBarSkin Maven / Gradle / Ivy

There is a newer version: 24-ea+15
Show newest version
/*
 * Copyright (c) 2010, 2024, 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 java.lang.ref.Reference;
import java.lang.ref.WeakReference;
import java.security.AccessController;
import java.security.PrivilegedAction;
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 java.util.function.Predicate;
import java.util.stream.Collectors;

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.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.css.converter.EnumConverter;
import javafx.css.converter.SizeConverter;
import javafx.event.ActionEvent;
import javafx.event.EventHandler;
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.KeyCode;
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 javafx.stage.Window;
import javafx.util.Pair;

import com.sun.javafx.FXPermissions;
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.scene.control.IDisconnectable;
import com.sun.javafx.scene.control.ListenerHelper;
import com.sun.javafx.scene.control.MenuBarButton;
import com.sun.javafx.scene.traversal.Direction;
import com.sun.javafx.scene.traversal.ParentTraversalEngine;
import com.sun.javafx.tk.Toolkit;

/**
 * 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 static final ObservableList stages;

    static {
        final Predicate findStage = (w) -> w instanceof Stage;
        @SuppressWarnings("removal")
        ObservableList windows = AccessController.doPrivileged(
            (PrivilegedAction>) () -> Window.getWindows(),
            null,
            FXPermissions.ACCESS_WINDOW_LIST_PERMISSION);
        stages = windows.filtered(findStage);
    }

    /* *************************************************************************
     *                                                                         *
     * Private fields                                                          *
     *                                                                         *
     **************************************************************************/

    /** contains MenuBarButton's children only */
    private final HBox container;
    /** index of *focused* MenuBarButton */
    private int focusedMenuIndex = -1;

    // represents the currently _open_ menu
    private Menu openMenu;
    private MenuBarButton openMenuButton;

    private static WeakHashMap> systemMenuMap;
    private static List wrappedDefaultMenus = new ArrayList<>();
    private static Stage currentMenuBarStage;
    private List wrappedMenus;

    private ChangeListener menuBarFocusedPropertyListener;
    private WeakChangeListener weakMenuBarFocusedPropertyListener;
    private ChangeListener sceneChangeListener;
    private ChangeListener menuVisibilityChangeListener;
    private WeakChangeListener weakMenuVisibilityChangeListener;
    private ListenerHelper sceneListenerHelper;
    private IDisconnectable windowFocusHelper;

    private boolean pendingDismiss = false;
    private boolean altKeyPressed = 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()) {
                updateActionListeners(mi, true);
            }
            for (MenuItem mi: c.getRemoved()) {
                updateActionListeners(mi, false);
            }
        }
    };

    Runnable firstMenuRunnable = new Runnable() {
        @Override
        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 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);

        menuBarFocusedPropertyListener = (ov, t, t1) -> {
            unSelectMenus();
            if (t1 && !container.getChildren().isEmpty()) {
                // RT-23147 when MenuBar's focusTraversable is true the first
                // menu will visually indicate focus
                menuModeStart(0);
                openMenuButton = ((MenuBarButton)container.getChildren().get(0));
                setFocusedMenuIndex(0);
                openMenuButton.setHover();
            }
        };
        weakMenuBarFocusedPropertyListener = new WeakChangeListener<>(menuBarFocusedPropertyListener);

        menuVisibilityChangeListener = (ov, t, t1) -> {
            rebuildUI();
        };
        weakMenuVisibilityChangeListener = new WeakChangeListener<>(menuVisibilityChangeListener);

        ListenerHelper lh = ListenerHelper.get(this);

        rebuildUI();
        lh.addListChangeListener(control.getMenus(), (v) -> {
            rebuildUI();
        });

        if (Toolkit.getToolkit().getSystemMenu().isSupported()) {
            lh.addInvalidationListener(control.useSystemMenuBarProperty(), (v) -> {
                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");
        }

        ParentTraversalEngine engine = new ParentTraversalEngine(getSkinnable());
        engine.addTraverseListener((node, bounds) -> {
            if (openMenu != null) openMenu.hide();
            setFocusedMenuIndex(0);
        });
        ParentHelper.setTraversalEngine(getSkinnable(), engine);

        lh.addChangeListener(control.sceneProperty(), true, (scene) -> {
            if (sceneListenerHelper != null) {
                sceneListenerHelper.disconnect();
                sceneListenerHelper = null;
            }

            if (scene != null ) {
                sceneListenerHelper = new ListenerHelper();

                // Key navigation
                sceneListenerHelper.addEventFilter(scene, KeyEvent.KEY_PRESSED, (ev) -> {
                    // process right left and may be tab key events
                    if (focusedMenuIndex >= 0) {
                        switch (ev.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
                                    }
                                    ev.consume();
                                    return;
                                }
                                if (isRTL) {
                                    moveToMenu(Direction.NEXT, true);
                                } else {
                                    moveToMenu(Direction.PREVIOUS, true);
                                }
                            }
                            ev.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
                                    }
                                    ev.consume();
                                    return;
                                }
                                if (isRTL) {
                                    moveToMenu(Direction.PREVIOUS, true);
                                } else {
                                    moveToMenu(Direction.NEXT, true);
                                }
                            }
                            ev.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);
                                    ev.consume();
                                }
                            }
                            break;
                        case ESCAPE:
                            unSelectMenus();
                            ev.consume();
                            break;
                        default:
                            break;
                        }
                    }
                });

                // When we click else where in the scene - menu selection should be cleared.
                sceneListenerHelper.addEventFilter(scene, MouseEvent.MOUSE_CLICKED, (ev) -> {
                    Bounds containerScreenBounds = container.localToScreen(container.getLayoutBounds());
                    if ((containerScreenBounds == null) || !containerScreenBounds.contains(ev.getScreenX(), ev.getScreenY())) {
                        unSelectMenus();
                    }
                });

                // When the parent window loses focus - menu selection should be cleared
                sceneListenerHelper.addChangeListener(scene.windowProperty(), true, (w) -> {
                    if (windowFocusHelper != null) {
                        windowFocusHelper.disconnect();
                        windowFocusHelper = null;
                    }

                    if (w != null) {
                        windowFocusHelper = sceneListenerHelper.addChangeListener(w.focusedProperty(), true, (focused) -> {
                            if (!focused) {
                                unSelectMenus();
                            }
                        });
                    }
                });

                sceneListenerHelper.addEventFilter(scene, KeyEvent.ANY, (ev) -> {
                    // Clear menu selection when ALT is pressed by itself
                    if (ev.getEventType() == KeyEvent.KEY_PRESSED) {
                        altKeyPressed = false;
                        if (ev.getCode() == KeyCode.ALT && !ev.isConsumed()) {
                            if (focusedMenuIndex == -1) {
                                altKeyPressed = true;
                            }
                            unSelectMenus();
                        }
                    } else if (ev.getEventType() == KeyEvent.KEY_RELEASED) {
                        // Put focus on the first menu when ALT is released
                        // directly after being pressed by itself
                        if (altKeyPressed && ev.getCode() == KeyCode.ALT && !ev.isConsumed()) {
                            firstMenuRunnable.run();
                        }
                        altKeyPressed = false;
                    }
                });

                // F10 accelerator
                scene.getAccelerators().put(acceleratorKeyCombo, firstMenuRunnable);
                sceneListenerHelper.addDisconnectable(() -> {
                    scene.getAccelerators().remove(acceleratorKeyCombo);
                });
            }
        });
    }

    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 = menuBarButtonAt(focusedMenuIndex);
                Skin skin = menuButton.getSkin();
                if (skin instanceof MenuButtonSkinBase) {
                    ((MenuButtonSkinBase)skin).requestFocusOnFirstMenuItem();
                }
            }

            openMenu.show();
        }
    }

    /**
     * This method is package scoped as it is used in this class as well as for testing
     */
    void setFocusedMenuIndex(int index) {
        focusedMenuIndex = (index >= -1 && index < getSkinnable().getMenus().size()) ? index : -1;

        if (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.
     * @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) 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());
        };

        for (Window stage : stages) {
            stage.focusedProperty().addListener(focusedStageListener);
        }
        stages.addListener((ListChangeListener) 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.
     */
    // --- 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 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() {
        if (getSkinnable() == null) {
            return;
        }

        if (sceneListenerHelper != null) {
            sceneListenerHelper.disconnect();
            sceneListenerHelper = null;
        }

        cleanUpListeners();
        cleanUpSystemMenu();
        getChildren().remove(container);

        // 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                                                  *
     *                                                                         *
     **************************************************************************/

    // package protected for testing purposes
    MenuBarButton menuBarButtonAt(int i) {
        return (MenuBarButton)container.getChildren().get(i);
    }

    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 cleanUpListeners() {
        getSkinnable().focusedProperty().removeListener(weakMenuBarFocusedPropertyListener);

        for (Menu m : getSkinnable().getMenus()) {
            // remove action listeners
            updateActionListeners(m, false);

            m.visibleProperty().removeListener(weakMenuVisibilityChangeListener);
        }

        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();
    }

    private void rebuildUI() {
        cleanUpListeners();

        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 (getSkinnable().isUseSystemMenuBar() &&
                                            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(weakMenuBarFocusedPropertyListener);

        for (final Menu menu : getSkinnable().getMenus()) {

            menu.visibleProperty().addListener(weakMenuVisibilityChangeListener);

            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 && 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>> 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;
        }
    }

    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) {
        Menu focusedMenu = menuBarButtonAt(focusedMenuIndex).menu;
        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> findSibling(Direction dir, int startIndex) {
        if (startIndex == -1) {
            return Optional.empty();
        }

        List visibleMenus = getSkinnable().getMenus().stream().filter(Menu::isVisible)
                .collect(Collectors.toList());
        final int totalMenus = visibleMenus.size();
        int i = 0;
        int nextIndex = 0;

        // Traverse all visible 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 (visibleMenus.get(nextIndex).isDisable()) {
                // Calculate new nextIndex by continuing loop
                startIndex = nextIndex;
            } else {
                // nextIndex is to be highlighted
                break;
            }
        }

        clearMenuButtonHover();
        return Optional.of(new Pair<>(visibleMenus.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                                                                     *
     *                                                                         *
     **************************************************************************/

    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)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)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 superclasses.
     * @return the CssMetaData associated with this class, which may include the
     * CssMetaData of its superclasses
     */
    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);
        }
    }
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy