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

org.abego.guitesting.swing.internal.MouseSupportImpl Maven / Gradle / Ivy

There is a newer version: 0.14.0
Show newest version
/*
 * MIT License
 *
 * Copyright (c) 2019 Udo Borkowski, ([email protected])
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in all
 * copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
 * SOFTWARE.
 */

package org.abego.guitesting.swing.internal;


import org.abego.guitesting.swing.MouseSupport;
import org.abego.guitesting.swing.WaitForIdleSupport;

import java.awt.AWTEvent;
import java.awt.Component;
import java.awt.MouseInfo;
import java.awt.Point;
import java.awt.Robot;
import java.awt.Toolkit;
import java.awt.event.AWTEventListener;
import java.awt.event.MouseEvent;
import java.text.MessageFormat;
import java.time.Duration;

import static java.awt.event.MouseEvent.MOUSE_PRESSED;
import static java.awt.event.MouseEvent.MOUSE_RELEASED;
import static java.time.Duration.ofMillis;
import static org.abego.commons.lang.ThreadUtil.sleep;
import static org.abego.commons.polling.PollingUtil.pollNoFail;
import static org.abego.guitesting.swing.internal.SwingUtil.toScreenCoordinates;

final class MouseSupportImpl implements MouseSupport {
    private static final int MULTI_CLICK_INTERVAL_MILLIS_DEFAULT = 500;
    private static final Duration MAX_WAIT_TIME_FOR_MOUSE_EVENT = ofMillis(100);

    // Both lastClickTime and lastClickPos need to be static, i.e. be shared
    // by all instances of MouseSupportImpl, to make sure subsequent clicks at
    // the same location are not interpreted as double clicks, even
    // when the second click is handled by a new MouseSupportImpl instance.
    // (This may happen when the JUnit test class creates a new XY/
    // MouseSupportImpl instance for every test).
    private static long lastClickTime = 0;
    private static Point lastClickPos = new Point();

    private final Robot robot;
    private final WaitForIdleSupport waitForIdleSupport;
    private final int multiClickIntervalMillis = multiClickIntervalMillisDefault();
    private volatile boolean gotMouseWheelEvent = false;

    private MouseSupportImpl(Robot robot, WaitForIdleSupport waitForIdleSupport) {
        this.robot = robot;
        this.waitForIdleSupport = waitForIdleSupport;
    }

    private static int multiClickIntervalMillisDefault() {
        Integer i = (Integer) Toolkit.getDefaultToolkit().
                getDesktopProperty("awt.multiClickInterval"); //NON-NLS
        return i != null ? i : MULTI_CLICK_INTERVAL_MILLIS_DEFAULT;
    }

    static MouseSupport newMouseSupport(Robot robot, WaitForIdleSupport waitForIdleSupport) {
        return new MouseSupportImpl(robot, waitForIdleSupport);
    }

    private static void runAndWaitForMouseAt(Point globalLocation, Runnable runnable) {
        new MouseLocationObserver().runAndWaitForMouseAt(globalLocation, runnable);
    }

    private static Point mousePos() {
        return MouseInfo.getPointerInfo().getLocation();
    }

    private static long getLastClickTime() {
        synchronized (MouseSupportImpl.class) {
            return lastClickTime;
        }
    }

    private static void setLastClickTime(long time) {
        synchronized (MouseSupportImpl.class) {
            lastClickTime = time;
        }
    }

    private static Point getLastClickPos() {
        synchronized (MouseSupportImpl.class) {
            return lastClickPos;
        }
    }

    private static void setLastClickPos(Point position) {
        synchronized (MouseSupportImpl.class) {
            lastClickPos = position;
        }
    }

    @Override
    public void click(int buttonsMask, int x, int y, int clickCount) {

        if (clickCount <= 0) {
            throw new IllegalArgumentException("clickCount must be > 0"); //NON-NLS
        }

        mouseMove(x, y);

        avoidMultiClickEvent(x, y);

        for (int i = 0; i < clickCount; i++) {
            mousePress(buttonsMask);
            mouseRelease(buttonsMask);
            setLastClickTime(System.currentTimeMillis());
            sleep(multiClickIntervalMillis / 2);
        }

        setLastClickPos(new Point(x, y));

        waitForIdle();
    }

    @Override
    public void click(int buttonsMask, Component component, int x, int y, int clickCount) {
        Point p = toScreenCoordinates(component, x, y);
        click(buttonsMask, p.x, p.y, clickCount);
    }

    public void drag(int buttonsMask, int x1, int y1, int x2, int y2) {
        Point startPos = new Point(x1, y1);
        Point endPos = new Point(x2, y2);

        waitForIdle();

        // make sure the start of a drag is not mistaken for a double click
        // (delay the drag start if necessary)
        avoidMultiClickEvent(x1, y1);

        // Move mouse to the start position (if necessary) and press the mouse
        runAndWaitForMouseAt(startPos, () -> {
            mouseMove(startPos);
            mousePress(buttonsMask);
        });

        mouseMove(endPos);
        runAndWaitForMouseAt(endPos, () ->
                mouseRelease(buttonsMask));

        setLastClickPos(endPos);
        setLastClickTime(System.currentTimeMillis());
    }

    private void avoidMultiClickEvent(int x, int y) {
        if (getLastClickPos().x == x && getLastClickPos().y == y) {
            // make sure a new "click sequence" starts not earlier than
            // 2 * multiClickIntervalMillis to avoid recognition as a double click
            long delay = (getLastClickTime() + 2 * multiClickIntervalMillis)
                    - System.currentTimeMillis();
            if (delay > 0) {
                sleep(delay);
            }
        }
    }

    @Override
    public void drag(
            int buttonsMask,
            Component component,
            int x1,
            int y1,
            int x2,
            int y2) {
        Point p1 = toScreenCoordinates(component, x1, y1);
        Point p2 = toScreenCoordinates(component, x2, y2);

        drag(buttonsMask, p1.x, p1.y, p2.x, p2.y);
    }

    private void waitForIdle() {
        waitForIdleSupport.waitForIdle();
    }

    @Override
    public void mouseMove(int x, int y) {
        Point newMousePos = new Point(x, y);

        if (!mousePos().equals(newMousePos)) {
            robot.mouseMove(newMousePos.x, newMousePos.y);
            waitForIdle();
        }

        // The mouse is not always immediately at the expected position. So wait...
        Point currentMousePos = pollNoFail(
                MouseSupportImpl::mousePos, v -> v.equals(newMousePos), MAX_WAIT_TIME_FOR_MOUSE_EVENT);

        if (!currentMousePos.equals(newMousePos)) {
            throw new InternalError(MessageFormat.format(
                    "Error in mouseMove: expected position {0}, got {1}", //NON-NLS
                    newMousePos, currentMousePos));
        }
    }

    @Override
    public void mousePress(int buttonsMask) {
        waitForIdle();
        runAndWaitForMouseAt(mousePos(), () -> robot.mousePress(buttonsMask));
    }

    @Override
    public void mouseRelease(int buttonsMask) {
        waitForIdle();
        runAndWaitForMouseAt(mousePos(), () -> robot.mouseRelease(buttonsMask));

    }

    @Override
    public void mouseWheel(int notchCount) {
        waitForIdle();

        gotMouseWheelEvent = false;
        AWTEventListener listener = e -> gotMouseWheelEvent = true;

        Toolkit.getDefaultToolkit().addAWTEventListener(listener, AWTEvent.MOUSE_WHEEL_EVENT_MASK);
        try {
            robot.mouseWheel(notchCount);

            pollNoFail(() -> gotMouseWheelEvent, v -> v, MAX_WAIT_TIME_FOR_MOUSE_EVENT);
        } finally {
            Toolkit.getDefaultToolkit().removeAWTEventListener(listener);
        }
    }

    /**
     * Because there is a delay between calling a Robot mouse method and its
     * effect in the environment (e.g. a changes mouse position) we sometimes
     * need to wait for the change state, e.g. check the current mouse position.
     * However this is not always sufficient, as e.g. some events will be
     * posted even after the state change. Therefore we need to observe the
     * events, too. This is what this class is for.
     */
    private static class MouseLocationObserver {
        private Point lastGlobalMouseEventPos = new Point(-1, -1);

        private void runAndWaitForMouseAt(Point globalLocation, Runnable runnable) {

            AWTEventListener listener = event -> {
                if (event instanceof MouseEvent) {
                    MouseEvent me = (MouseEvent) event;
                    if (me.getID() == MOUSE_PRESSED || me.getID() == MOUSE_RELEASED) {
                        setLastGlobalMouseEventPos(new Point(me.getXOnScreen(), me.getYOnScreen()));
                    }
                }
            };

            Toolkit.getDefaultToolkit().addAWTEventListener(listener, AWTEvent.MOUSE_EVENT_MASK);

            try {

                runnable.run();

                // Wait for a mouse pressed/release event with the given
                // globalLocation. In some situations, e.g. when moving a frame
                // by dragging in its title bar, no events will be posted.
                // In these cases the timeout will be used.
                pollNoFail(this::getLastGlobalMouseEventPos,
                        p -> p.equals(globalLocation), MAX_WAIT_TIME_FOR_MOUSE_EVENT);

            } finally {
                Toolkit.getDefaultToolkit().removeAWTEventListener(listener);
            }

        }

        private Point getLastGlobalMouseEventPos() {
            synchronized (this) {
                return lastGlobalMouseEventPos;
            }
        }

        private void setLastGlobalMouseEventPos(Point lastGlobalMouseEventPos) {
            synchronized (this) {
                this.lastGlobalMouseEventPos = lastGlobalMouseEventPos;
            }
        }
    }

}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy