/*
 * Copyright (c) 2014, 2025 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.
 */

import javax.swing.JFrame;
import javax.swing.JMenu;
import javax.swing.JMenuItem;
import javax.swing.JPanel;
import javax.swing.JPopupMenu;
import javax.swing.SwingUtilities;
import java.awt.AWTException;
import java.awt.Color;
import java.awt.Component;
import java.awt.GraphicsDevice;
import java.awt.IllegalComponentStateException;
import java.awt.MouseInfo;
import java.awt.Point;
import java.awt.Robot;
import java.awt.Toolkit;
import java.awt.event.InputEvent;
import java.awt.event.KeyEvent;

public class FocusableNestedMenu {
    static JFrame frame;

    static JPopupMenu popupMenu;
    static JMenu deeperMenu;
    static JMenuItem jMenuItem;
    static ExtendedRobot robot;

    public static void main(String[] args) throws Exception {
        initAndShowGui();

        boolean automated = false;
        // To reproduce manually
        // 0. set automated to false and run the test
        // 1. right click on the frame
        // 2. select "sub menu title" submenu
        // 3. click on the item
        // 4. try to open the menu again by right clicking on the frame
        // it will disappear immediately, but if you press the right button and then
        // away from the menu and release the button there, the menu will
        // stay on the screen.

        if (automated) {
            try {
                doTest();
            } finally {
                SwingUtilities.invokeAndWait(() -> {
                    frame.dispose();
                });
            }
        }
    }

    private static void doTest() throws Exception {
        robot = new ExtendedRobot();

        robot.waitForIdle();
        robot.delay(300);


        Point locationOnScreen = frame.getLocationOnScreen();
        Point startLocation = new Point(locationOnScreen.x + frame.getWidth() - 10, locationOnScreen.y + frame.getHeight() / 2);
        robot.mouseMove(startLocation.x, startLocation.y);

        robot.mousePress(InputEvent.BUTTON3_DOWN_MASK);
        robot.delay(50);
        robot.mouseRelease(InputEvent.BUTTON3_DOWN_MASK);
        robot.delay(50);

        waitTillShownEx(deeperMenu);
        locationOnScreen = deeperMenu.getLocationOnScreen();
        glide(locationOnScreen.x + deeperMenu.getWidth() / 2, locationOnScreen.y + deeperMenu.getHeight() / 2);

        waitTillShownEx(jMenuItem);

        locationOnScreen = jMenuItem.getLocationOnScreen();
        glide(locationOnScreen.x + jMenuItem.getWidth() / 2, locationOnScreen.y + jMenuItem.getHeight() / 2);

        robot.mousePress(InputEvent.BUTTON1_DOWN_MASK);
        robot.delay(50);
        robot.mouseRelease(InputEvent.BUTTON1_DOWN_MASK);
        robot.delay(50);

//        System.err.println("-----------------------------");

        robot.mouseMove(startLocation.x, startLocation.y + 15);

        robot.mousePress(InputEvent.BUTTON3_DOWN_MASK);
        robot.delay(50);
        robot.mouseRelease(InputEvent.BUTTON3_DOWN_MASK);
        robot.delay(50);

        robot.waitForIdle();
        robot.delay(300);

        SwingUtilities.invokeAndWait(() -> {
            try {
                Point locationOnScreen1 = deeperMenu.getLocationOnScreen();
                System.err.println("PASSED: locationOnScreen1: " + locationOnScreen1);
            } catch (IllegalComponentStateException e) {
                throw new RuntimeException("Menu is not visible");
            }
        });
    }

    public static void waitTillShownEx(final Component comp) throws InterruptedException {
        while (true) {
            try {
                Thread.sleep(100);
                comp.getLocationOnScreen();
                break;
            } catch (IllegalComponentStateException e) {}
        }
    }

    private static void glide(int i, int i1) {
        boolean useGlide = true;

        if (useGlide) {
            robot.glide(i, i1);
        } else {
            robot.mouseMove(i, i1);
        }
    }

    private static void initAndShowGui() throws Exception {
        SwingUtilities.invokeAndWait(() -> {
            frame = new JFrame("JPopupMenu Example");
            frame.setLocationRelativeTo(null);
            frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
            frame.setSize(100, 100);

            JPanel panel = new JPanel();
            panel.setBackground(Color.LIGHT_GRAY);

            popupMenu = new JPopupMenu();

            // long name to ensure that it will be a heavyweight popup
            deeperMenu = new JMenu("sub menu title");
            jMenuItem = new JMenuItem("item");
            deeperMenu.add(jMenuItem);
            popupMenu.add(deeperMenu);

            popupMenu.add(new JMenuItem("another "));

            panel.setComponentPopupMenu(popupMenu);
            frame.add(panel);
            frame.setVisible(true);
        });
    }
}

/**
 * ExtendedRobot is a subclass of {@link Robot}. It provides some convenience methods that are
 * ought to be moved to {@link Robot} class.
 * <p>
 * ExtendedRobot uses delay {@link #getSyncDelay()} to make syncing threads with {@link #waitForIdle()}
 * more stable. This delay can be set once on creating object and could not be changed throughout object
 * lifecycle. Constructor reads vm integer property {@code java.awt.robotdelay} and sets the delay value
 * equal to the property value. If the property was not set 500 milliseconds default value is used.
 * <p>
 * When using jtreg you would include this class via something like:
 * <pre>
 * {@literal @}library ../../../../lib/testlibrary
 * {@literal @}build ExtendedRobot
 * </pre>
 *
 * @author      Dmitriy Ermashov
 * @since       9
 */

class ExtendedRobot extends Robot {

    private static int DEFAULT_SPEED = 20;       // Speed for mouse glide and click
    private static int DEFAULT_SYNC_DELAY = 500; // Default Additional delay for waitForIdle()
    private static int DEFAULT_STEP_LENGTH = 2;  // Step length (in pixels) for mouse glide

    private final int syncDelay = DEFAULT_SYNC_DELAY;

    //TODO: uncomment three lines below after moving functionality to java.awt.Robot
    //{
    //    syncDelay = AccessController.doPrivileged(new GetIntegerAction("java.awt.robotdelay", DEFAULT_SYNC_DELAY));
    //}

    /**
     * Constructs an ExtendedRobot object in the coordinate system of the primary screen.
     *
     * @throws  AWTException if the platform configuration does not allow low-level input
     *          control. This exception is always thrown when
     *          GraphicsEnvironment.isHeadless() returns true
     * @throws  SecurityException if {@code createRobot} permission is not granted
     *
     * @see     java.awt.GraphicsEnvironment#isHeadless
     * @see     SecurityManager#checkPermission
     * @see     java.awt.AWTPermission
     */
    public ExtendedRobot() throws AWTException {
        super();
    }

    /**
     * Creates an ExtendedRobot for the given screen device. Coordinates passed
     * to ExtendedRobot method calls like mouseMove and createScreenCapture will
     * be interpreted as being in the same coordinate system as the specified screen.
     * Note that depending on the platform configuration, multiple screens may either:
     * <ul>
     * <li>share the same coordinate system to form a combined virtual screen</li>
     * <li>use different coordinate systems to act as independent screens</li>
     * </ul>
     * This constructor is meant for the latter case.
     * <p>
     * If screen devices are reconfigured such that the coordinate system is
     * affected, the behavior of existing ExtendedRobot objects is undefined.
     *
     * @param   screen  A screen GraphicsDevice indicating the coordinate
     *                  system the Robot will operate in.
     * @throws  AWTException if the platform configuration does not allow low-level input
     *          control. This exception is always thrown when
     *          GraphicsEnvironment.isHeadless() returns true.
     * @throws  IllegalArgumentException if {@code screen} is not a screen
     *          GraphicsDevice.
     * @throws  SecurityException if {@code createRobot} permission is not granted
     *
     * @see     java.awt.GraphicsEnvironment#isHeadless
     * @see     GraphicsDevice
     * @see     SecurityManager#checkPermission
     * @see     java.awt.AWTPermission
     */
    public ExtendedRobot(GraphicsDevice screen) throws AWTException {
        super(screen);
    }

    /**
     * Returns delay length for {@link #waitForIdle()} method
     *
     * @return  Current delay value
     *
     * @see     #waitForIdle()
     */
    public int getSyncDelay(){ return this.syncDelay; }

    /**
     * Clicks mouse button(s) by calling {@link Robot#mousePress(int)} and
     * {@link Robot#mouseRelease(int)} methods
     *
     *
     * @param   buttons The button mask; a combination of one or more mouse button masks.
     * @throws  IllegalArgumentException if the {@code buttons} mask contains the mask for
     *          extra mouse button and support for extended mouse buttons is
     *          {@link Toolkit#areExtraMouseButtonsEnabled() disabled} by Java
     * @throws  IllegalArgumentException if the {@code buttons} mask contains the mask for
     *          extra mouse button that does not exist on the mouse and support for extended
     *          mouse buttons is {@link Toolkit#areExtraMouseButtonsEnabled() enabled}
     *          by Java
     *
     * @see     #mousePress(int)
     * @see     #mouseRelease(int)
     * @see     InputEvent#getMaskForButton(int)
     * @see     Toolkit#areExtraMouseButtonsEnabled()
     * @see     java.awt.event.MouseEvent
     */
    public void click(int buttons) {
        mousePress(buttons);
        waitForIdle(DEFAULT_SPEED);
        mouseRelease(buttons);
        waitForIdle();
    }

    /**
     * Clicks mouse button 1
     *
     * @throws  IllegalArgumentException if the {@code buttons} mask contains the mask for
     *          extra mouse button and support for extended mouse buttons is
     *          {@link Toolkit#areExtraMouseButtonsEnabled() disabled} by Java
     * @throws  IllegalArgumentException if the {@code buttons} mask contains the mask for
     *          extra mouse button that does not exist on the mouse and support for extended
     *          mouse buttons is {@link Toolkit#areExtraMouseButtonsEnabled() enabled}
     *          by Java
     *
     * @see     #click(int)
     */
    public void click() {
        click(InputEvent.BUTTON1_DOWN_MASK);
    }

    /**
     * Waits until all events currently on the event queue have been processed with given
     * delay after syncing threads. It uses more advanced method of synchronizing threads
     * unlike {@link Robot#waitForIdle()}
     *
     * @param   delayValue  Additional delay length in milliseconds to wait until thread
     *                      sync been completed
     * @throws  sun.awt.SunToolkit.IllegalThreadException if called on the AWT event
     *          dispatching thread
     */
    public synchronized void waitForIdle(int delayValue) {
        super.waitForIdle();
        delay(delayValue);
    }

    /**
     * Waits until all events currently on the event queue have been processed with delay
     * {@link #getSyncDelay()} after syncing threads. It uses more advanced method of
     * synchronizing threads unlike {@link Robot#waitForIdle()}
     *
     * @throws  sun.awt.SunToolkit.IllegalThreadException if called on the AWT event
     *          dispatching thread
     *
     * @see     #waitForIdle(int)
     */
    @Override
    public synchronized void waitForIdle() {
        waitForIdle(syncDelay);
    }

    /**
     * Move the mouse in multiple steps from where it is
     * now to the destination coordinates.
     *
     * @param   x   Destination point x coordinate
     * @param   y   Destination point y coordinate
     *
     * @see     #glide(int, int, int, int)
     */
    public void glide(int x, int y) {
        Point p = MouseInfo.getPointerInfo().getLocation();
        glide(p.x, p.y, x, y);
    }

    /**
     * Move the mouse in multiple steps from where it is
     * now to the destination point.
     *
     * @param   dest    Destination point
     *
     * @see     #glide(int, int)
     */
    public void glide(Point dest) {
        glide(dest.x, dest.y);
    }

    /**
     * Move the mouse in multiple steps from source coordinates
     * to the destination coordinates.
     *
     * @param   fromX   Source point x coordinate
     * @param   fromY   Source point y coordinate
     * @param   toX     Destination point x coordinate
     * @param   toY     Destination point y coordinate
     *
     * @see     #glide(int, int, int, int, int, int)
     */
    public void glide(int fromX, int fromY, int toX, int toY) {
        glide(fromX, fromY, toX, toY, DEFAULT_STEP_LENGTH, DEFAULT_SPEED);
    }

    /**
     * Move the mouse in multiple steps from source point to the
     * destination point with default speed and step length.
     *
     * @param   src     Source point
     * @param   dest    Destination point
     *
     * @see     #glide(int, int, int, int, int, int)
     */
    public void glide(Point src, Point dest) {
        glide(src.x, src.y, dest.x, dest.y, DEFAULT_STEP_LENGTH, DEFAULT_SPEED);
    }

    /**
     * Move the mouse in multiple steps from source point to the
     * destination point with given speed and step length.
     *
     * @param   srcX        Source point x cordinate
     * @param   srcY        Source point y cordinate
     * @param   destX       Destination point x cordinate
     * @param   destY       Destination point y cordinate
     * @param   stepLength  Approximate length of one step
     * @param   speed       Delay between steps.
     *
     * @see     #mouseMove(int, int)
     * @see     #delay(int)
     */
    public void glide(int srcX, int srcY, int destX, int destY, int stepLength, int speed) {
        int stepNum;
        double tDx, tDy;
        double dx, dy, ds;
        double x, y;

        dx = (destX - srcX);
        dy = (destY - srcY);
        ds = Math.sqrt(dx*dx + dy*dy);

        tDx = dx / ds * stepLength;
        tDy = dy / ds * stepLength;

        int stepsCount = (int) ds / stepLength;

        // Walk the mouse to the destination one step at a time
        mouseMove(srcX, srcY);

        for (x = srcX, y = srcY, stepNum = 0;
             stepNum < stepsCount;
             stepNum++) {
            x += tDx;
            y += tDy;
            mouseMove((int)x, (int)y);
            delay(speed);
        }

        // Ensure the mouse moves to the right destination.
        // The steps may have led the mouse to a slightly wrong place.
        mouseMove(destX, destY);
    }

    /**
     * Moves mouse pointer to given screen coordinates.
     *
     * @param   position    Target position
     *
     * @see     Robot#mouseMove(int, int)
     */
    public synchronized void mouseMove(Point position) {
        mouseMove(position.x, position.y);
    }


    /**
     * Emulate native drag and drop process using {@code InputEvent.BUTTON1_DOWN_MASK}.
     * The method successively moves mouse cursor to point with coordinates
     * ({@code fromX}, {@code fromY}), presses mouse button 1, drag mouse to
     * point with coordinates ({@code toX}, {@code toY}) and releases mouse
     * button 1 at last.
     *
     * @param   fromX   Source point x coordinate
     * @param   fromY   Source point y coordinate
     * @param   toX     Destination point x coordinate
     * @param   toY     Destination point y coordinate
     *
     * @see     #mousePress(int)
     * @see     #glide(int, int, int, int)
     */
    public void dragAndDrop(int fromX, int fromY, int toX, int toY){
        mouseMove(fromX, fromY);
        mousePress(InputEvent.BUTTON1_DOWN_MASK);
        waitForIdle();
        glide(toX, toY);
        mouseRelease(InputEvent.BUTTON1_DOWN_MASK);
        waitForIdle();
    }

    /**
     * Emulate native drag and drop process using {@code InputEvent.BUTTON1_DOWN_MASK}.
     * The method successively moves mouse cursor to point {@code from},
     * presses mouse button 1, drag mouse to point {@code to} and releases
     * mouse button 1 at last.
     *
     * @param   from    Source point
     * @param   to      Destination point
     *
     * @see     #mousePress(int)
     * @see     #glide(int, int, int, int)
     * @see     #dragAndDrop(int, int, int, int)
     */
    public void dragAndDrop(Point from, Point to){
        dragAndDrop(from.x, from.y, to.x, to.y);
    }

    /**
     * Successively presses and releases a given key.
     * <p>
     * Key codes that have more than one physical key associated with them
     * (e.g. {@code KeyEvent.VK_SHIFT} could mean either the
     * left or right shift key) will map to the left key.
     *
     * @param   keycode Key to press (e.g. {@code KeyEvent.VK_A})
     * @throws  IllegalArgumentException if {@code keycode} is not
     *          a valid key
     *
     * @see     Robot#keyPress(int)
     * @see     Robot#keyRelease(int)
     * @see     KeyEvent
     */
    public void type(int keycode) {
        keyPress(keycode);
        waitForIdle(DEFAULT_SPEED);
        keyRelease(keycode);
        waitForIdle(DEFAULT_SPEED);
    }

    /**
     * Types given character
     *
     * @param   c   Character to be typed (e.g. {@code 'a'})
     *
     * @see     #type(int)
     * @see     KeyEvent
     */
    public void type(char c) {
        type(KeyEvent.getExtendedKeyCodeForChar(c));
    }

    /**
     * Types given array of characters one by one
     *
     * @param   symbols Array of characters to be typed
     *
     * @see     #type(char)
     */
    public void type(char[] symbols) {
        for (int i = 0; i < symbols.length; i++) {
            type(symbols[i]);
        }
    }

    /**
     * Types given string
     *
     * @param   s   String to be typed
     *
     * @see     #type(char[])
     */
    public void type(String s) {
        type(s.toCharArray());
    }
}
