/*
 * Copyright (c) 2010, 2021, 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;

import com.sun.javafx.beans.IDProperty;
import com.sun.javafx.collections.TrackableObservableList;
import com.sun.javafx.util.Utils;

import javafx.beans.property.ObjectProperty;
import javafx.beans.property.ObjectPropertyBase;
import javafx.collections.ListChangeListener.Change;
import javafx.collections.ObservableList;
import javafx.event.ActionEvent;
import javafx.event.Event;
import javafx.event.EventHandler;
import javafx.geometry.HPos;
import javafx.geometry.Point2D;
import javafx.geometry.Side;
import javafx.geometry.VPos;
import javafx.scene.Node;
import javafx.scene.Scene;
import javafx.scene.control.skin.ContextMenuSkin;
import javafx.stage.Window;

/**
 * <p>
 * A popup control containing an ObservableList of menu items. The {@link #getItems() items}
 * ObservableList allows for any {@link MenuItem} type to be inserted,
 * including its subclasses {@link Menu}, {@link MenuItem}, {@link RadioMenuItem}, {@link CheckMenuItem} and
 * {@link CustomMenuItem}. If an arbitrary Node needs to be
 * inserted into a menu, a CustomMenuItem can be used. One exception to this general rule is that
 * {@link SeparatorMenuItem} could be used for inserting a separator.
 * <p>
 * A common use case for this class is creating and showing context menus to
 * users. To create a context menu using ContextMenu you can do the
 * following:
 *
<pre><code>final ContextMenu contextMenu = new ContextMenu();
contextMenu.setOnShowing(new EventHandler&lt;WindowEvent&gt;() {
    public void handle(WindowEvent e) {
        System.out.println("showing");
    }
});
contextMenu.setOnShown(new EventHandler&lt;WindowEvent&gt;() {
    public void handle(WindowEvent e) {
        System.out.println("shown");
    }
});

MenuItem item1 = new MenuItem("About");
item1.setOnAction(new EventHandler&lt;ActionEvent&gt;() {
    public void handle(ActionEvent e) {
        System.out.println("About");
    }
});
MenuItem item2 = new MenuItem("Preferences");
item2.setOnAction(new EventHandler&lt;ActionEvent&gt;() {
    public void handle(ActionEvent e) {
        System.out.println("Preferences");
    }
});
contextMenu.getItems().addAll(item1, item2);

final TextField textField = new TextField("Type Something");
textField.setContextMenu(contextMenu);</code></pre>
 *
 * <img src="doc-files/ContextMenu.png" alt="Image of the ContextMenu control">
 *
 * <p>{@link Control#setContextMenu(javafx.scene.control.ContextMenu) } convenience
 * method can be used to set a context menu on on any control. The example above results in the
 * context menu being displayed on the right {@link javafx.geometry.Side Side}
 * of the TextField. Alternatively, an event handler can also be set on the control
 * to invoke the context menu as shown below.
 *
<pre><code>textField.setOnAction(new EventHandler&lt;ActionEvent&gt;() {
    public void handle(ActionEvent e) {
        contextMenu.show(textField, Side.BOTTOM, 0, 0);
    }
});

Group root = (Group) scene.getRoot();
root.getChildren().add(textField);
</code></pre>
 *
 * <p>In this example, the context menu is shown when the user clicks on the
 * {@link javafx.scene.control.Button Button} (of course, you should use the
 * {@link MenuButton} control to do this rather than doing the above).</p>
 *
 * <p>Note that the show function used in the code sample
 * above will result in the ContextMenu appearing directly beneath the
 * TextField. You can vary the {@link javafx.geometry.Side Side}  to get the results you expect.</p>
 *
 * @see MenuItem
 * @see Menu
 * @since JavaFX 2.0
 */
@IDProperty("id")
public class ContextMenu extends PopupControl {

    /* *************************************************************************
     *                                                                         *
     * Fields                                                                  *
     *                                                                         *
     **************************************************************************/

    private boolean showRelativeToWindow = false;



    /* *************************************************************************
     *                                                                         *
     * Constructors                                                            *
     *                                                                         *
     **************************************************************************/

    /**
     * Create a new ContextMenu
     */
    public ContextMenu() {
        getStyleClass().setAll(DEFAULT_STYLE_CLASS);
        setAutoHide(true);
        setConsumeAutoHidingEvents(false);
    }

    /**
     * Create a new ContextMenu initialized with the given items
     * @param items the list of menu items
     */
    public ContextMenu(MenuItem... items) {
        this();
        this.items.addAll(items);
    }



    /* *************************************************************************
     *                                                                         *
     * Properties                                                              *
     *                                                                         *
     **************************************************************************/

    /**
     * Callback function to be informed when an item contained within this
     * {@code ContextMenu} has been activated. The current implementation informs
     * all parent menus as well, so that it is not necessary to listen to all
     * sub menus for events.
     */
    private ObjectProperty<EventHandler<ActionEvent>> onAction = new ObjectPropertyBase<EventHandler<ActionEvent>>() {
        @Override protected void invalidated() {
            setEventHandler(ActionEvent.ACTION, get());
       }

        @Override
        public Object getBean() {
            return ContextMenu.this;
        }

        @Override
        public String getName() {
            return "onAction";
        }
    };
    public final void setOnAction(EventHandler<ActionEvent> value) { onActionProperty().set(value); }
    public final EventHandler<ActionEvent> getOnAction() { return onActionProperty().get(); }
    public final ObjectProperty<EventHandler<ActionEvent>> onActionProperty() { return onAction; }

    private final ObservableList<MenuItem> items = new TrackableObservableList<MenuItem>() {
        @Override protected void onChanged(Change<MenuItem> c) {
            while (c.next()) {
                for (MenuItem item : c.getRemoved()) {
                    item.setParentPopup(null);
                }
                for (MenuItem item : c.getAddedSubList()) {
                    if (item.getParentPopup() != null) {
                        // we need to remove this item from its current parentPopup
                        // as a MenuItem should not exist in multiple parentPopup
                        // instances
                        item.getParentPopup().getItems().remove(item);
                    }
                    item.setParentPopup(ContextMenu.this);
                }
            }
        }
    };



    /* *************************************************************************
     *                                                                         *
     * Public API                                                              *
     *                                                                         *
     **************************************************************************/

    /**
     * The menu items on the context menu. If this ObservableList is modified at
     * runtime, the ContextMenu will update as expected.
     * @return the menu items on this context menu
     * @see MenuItem
     */
    public final ObservableList<MenuItem> getItems() { return items; }

    /**
     * Shows the {@code ContextMenu} relative to the given anchor node, on the side
     * specified by the {@code side} parameter, and offset
     * by the given {@code dx} and {@code dy} values for the x-axis and y-axis, respectively.
     * If there is not enough room, the menu is moved to the opposite side and
     * the offset is not applied.
     * <p>
     * To clarify the purpose of the {@code side} parameter,
     * consider that it is relative to the anchor node.
     * As such, a {@code side} of {@code TOP} would mean that the ContextMenu's bottom left corner
     * is set to the top left corner of the anchor.
     * <p>
     * This function is useful for finely tuning the position of a menu,
     * relative to the parent node to ensure close alignment.
     * @param anchor the anchor node
     * @param side the side
     * @param dx the dx value for the x-axis
     * @param dy the dy value for the y-axis
     */
     public void show(Node anchor, Side side, double dx, double dy) {
        if (anchor == null) return;
        if (getItems().size() == 0) return;

        getScene().setNodeOrientation(anchor.getEffectiveNodeOrientation());
        if (getScene().getStylesheets().isEmpty()) {
            getScene().getStylesheets().setAll(anchor.getScene().getStylesheets());
        }

        HPos hpos = side == Side.LEFT ? HPos.LEFT : side == Side.RIGHT ? HPos.RIGHT : HPos.CENTER;
        VPos vpos = side == Side.TOP ? VPos.TOP : side == Side.BOTTOM ? VPos.BOTTOM : VPos.CENTER;

        // translate from anchor/hpos/vpos/dx/dy into screenX/screenY
        Point2D point = Utils.pointRelativeTo(anchor,
                prefWidth(-1), prefHeight(-1),
                hpos, vpos, dx, dy, true);
        doShow(anchor, point.getX(), point.getY());
    }

    /**
     * Shows the {@code ContextMenu} at the specified screen coordinates. If there
     * is not enough room at the specified location to show the {@code ContextMenu}
     * given its size requirements, the necessary adjustments are made to bring
     * the {@code ContextMenu} back on screen. This also means that the
     * {@code ContextMenu} will not span multiple monitors.
     * @param anchor the anchor node
     * @param screenX the x position of the anchor in screen coordinates
     * @param screenY the y position of the anchor in screen coordinates
     */
    @Override
    public void show(Node anchor, double screenX, double screenY) {
        if (anchor == null) return;
        if (getItems().size() == 0) return;
        getScene().setNodeOrientation(anchor.getEffectiveNodeOrientation());
        doShow(anchor, screenX, screenY);
    }

    /**
     * Hides this {@code ContextMenu} and any visible submenus, assuming that when this function
     * is called that the {@code ContextMenu} was showing.
     * <p>
     * If this {@code ContextMenu} is not showing, then nothing happens.
     */
    @Override public void hide() {
        if (!isShowing()) return;
        Event.fireEvent(this, new Event(Menu.ON_HIDING));
        super.hide();
        Event.fireEvent(this, new Event(Menu.ON_HIDDEN));
    }

    /** {@inheritDoc} */
    @Override protected Skin<?> createDefaultSkin() {
        return new ContextMenuSkin(this);
    }



    /* *************************************************************************
     *                                                                         *
     * Private Implementation                                                  *
     *                                                                         *
     **************************************************************************/

    final boolean isShowRelativeToWindow() { return showRelativeToWindow; }
    final void setShowRelativeToWindow(boolean value) { showRelativeToWindow = value; }

    private void doShow(Node anchor, double screenX, double screenY) {
        Event.fireEvent(this, new Event(Menu.ON_SHOWING));
        if(isShowRelativeToWindow()) {
            final Scene scene = (anchor == null) ? null : anchor.getScene();
            final Window win = (scene == null) ? null : scene.getWindow();
            if (win == null) return;
            super.show(win, screenX, screenY);
        } else {
            super.show(anchor, screenX, screenY);
        }
        Event.fireEvent(this, new Event(Menu.ON_SHOWN));
    }



    /* *************************************************************************
     *                                                                         *
     *                         Stylesheet Handling                             *
     *                                                                         *
     ***************************************************************************/

    private static final String DEFAULT_STYLE_CLASS = "context-menu";
}
