org.assertj.swing.input.InputState Maven / Gradle / Ivy
Show all versions of assertj-swing Show documentation
/**
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on
* an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the
* specific language governing permissions and limitations under the License.
*
* Copyright 2012-2015 the original author or authors.
*/
package org.assertj.swing.input;
import static java.awt.AWTEvent.KEY_EVENT_MASK;
import static java.awt.AWTEvent.MOUSE_EVENT_MASK;
import static java.awt.AWTEvent.MOUSE_MOTION_EVENT_MASK;
import static javax.swing.SwingUtilities.getDeepestComponentAt;
import static org.assertj.core.util.Preconditions.checkNotNull;
import static org.assertj.swing.awt.AWT.locationOnScreenOf;
import static org.assertj.swing.input.MouseInfo.BUTTON_MASK;
import java.awt.AWTEvent;
import java.awt.Component;
import java.awt.IllegalComponentStateException;
import java.awt.Point;
import java.awt.Toolkit;
import java.awt.event.AWTEventListener;
import java.awt.event.InputEvent;
import java.awt.event.KeyEvent;
import java.awt.event.MouseEvent;
import javax.annotation.Nonnull;
import javax.annotation.Nullable;
import javax.annotation.concurrent.GuardedBy;
import javax.annotation.concurrent.ThreadSafe;
import org.assertj.swing.exception.UnexpectedException;
import org.assertj.swing.listener.EventDispatchThreadedEventListener;
/**
*
* Keeps track of a given input state. Includes mouse/pointer position and keyboard modifier key state.
*
*
*
* Synchronization assumes that any given instance might be called from more than one event dispatch thread (EDT.)
*
*/
// TODO: add a BitSet with the full keyboard key press state
@ThreadSafe
public class InputState {
@GuardedBy("this")
private final MouseInfo mouseInfo = new MouseInfo();
@GuardedBy("this")
private final DragDropInfo dragDropInfo = new DragDropInfo();
@GuardedBy("this")
private int modifiers;
@GuardedBy("this")
private long lastEventTime;
private EventNormalizer normalizer;
public InputState(@Nonnull Toolkit toolkit) {
long mask = MOUSE_MOTION_EVENT_MASK | MOUSE_EVENT_MASK | KEY_EVENT_MASK;
AWTEventListener listener = new EventDispatchThreadedEventListener() {
@Override
protected void processEvent(@Nonnull AWTEvent event) {
update(event);
}
};
normalizer = new EventNormalizer();
normalizer.startListening(toolkit, listener, mask);
}
public synchronized void clear() {
mouseInfo.clear();
dragDropInfo.clear();
modifiers = 0;
lastEventTime = 0;
}
public void dispose() {
normalizer.stopListening();
normalizer = null;
}
/**
* Explicitly update the internal state.
*
* @param event the event to use to update the internal state.
*/
public void update(@Nonnull AWTEvent event) {
if (event instanceof InputEvent) {
InputEvent inputEvent = (InputEvent) event;
if (inputEvent.getWhen() < lastEventTime()) {
// event is old.
return;
}
if (inputEvent instanceof KeyEvent) {
synchronized (this) {
lastEventTime(inputEvent);
modifiers(inputEvent.getModifiers());
// FIXME add state of individual keys
}
}
if (inputEvent instanceof MouseEvent) {
MouseEvent mouseEvent = (MouseEvent) inputEvent;
Point eventScreenLocation = screenLocation(mouseEvent);
synchronized (this) {
lastEventTime(mouseEvent);
dragDropInfo.update(mouseEvent);
mouseInfo.modifiers(modifiers);
mouseInfo.update(mouseEvent, eventScreenLocation);
modifiers(mouseInfo.modifiers());
}
}
}
}
private @Nullable Point screenLocation(@Nonnull MouseEvent event) {
// childAt and locationOnScreenOf want the tree lock, so be careful not to use any additional locks at the same time
// to avoid deadlock.
// Determine the current mouse position in screen coordinates
try {
Component component = checkNotNull(event.getComponent());
return locationOnScreenOf(component);
} catch (IllegalComponentStateException e) {
// component might be hidden by the time we process this event
} catch (UnexpectedException e) {
if (!(e.getCause() instanceof IllegalComponentStateException)) {
throw e;
}
}
return null;
}
private void lastEventTime(@Nonnull InputEvent event) {
lastEventTime = event.getWhen();
}
private void modifiers(int newModifiers) {
modifiers = newModifiers;
}
/**
* @return the most deeply nested AWT or Swing {@code Component} which currently contains the pointer.
*/
public synchronized @Nullable Component deepestComponentUnderMousePointer() {
Component c = mouseComponent();
if (c != null) {
Point mouseLocation = mouseLocation();
if (mouseLocation != null) {
c = childAt(c, mouseLocation);
}
}
return c;
}
/**
* Returns the last known AWT or Swing {@code Component} to contain the pointer, or {@code null} if none. Note that
* this may not correspond to the {@code Component} that actually shows up in {@code AWTEvent}s.
*
* @return the last known {@code Component} to contain the pointer, or {@code null} if none.
*/
public synchronized @Nullable Component mouseComponent() {
return mouseInfo.component();
}
/**
*
* Returns the AWT or Swing {@code Component} under the given coordinates in the given parent component. Events are
* often generated only for the outermost {@code Container}, so we have to determine if the pointer is actually within
* a child. Basically the same as {@code Component.getComponentAt}, but recurses to the lowest-level {@code Component}
* instead of only one level. Point is in {@code Component} coordinates.
*
*
*
* The default {@code Component.getComponentAt} can return invisible {@code Component}s ({@code JRootPane} has an
* invisible {@code JPanel} (glass pane?) which will otherwise swallow everything).
*
*
*
* NOTE: childAt grabs the {@code TreeLock}, so this should *only* be invoked on the event dispatch thread, preferably
* with no other locks held. Use it elsewhere at your own risk.
*
*
* @param parent the given parent.
* @param where the given coordinates.
* @return the {@code Component} under the given coordinates in the given parent {@code Component}.
*/
public static Component childAt(@Nonnull Component parent, @Nonnull Point where) {
return getDeepestComponentAt(parent, where.x, where.y);
}
/**
* Indicates there is a drag operation in progress.
*
* @return {@code true} if there is a drag operation in progress, {@code false} otherwise.
*/
public synchronized boolean dragInProgress() {
return dragDropInfo.isDragging();
}
/**
* @return the AWT or Swing {@code Component} where a drag operation started.
*/
public synchronized Component dragSource() {
return dragDropInfo.source();
}
/**
* @return the coordinates where a drag operation started.
*/
public synchronized Point dragOrigin() {
return dragDropInfo.origin();
}
/**
* @return the number of times a mouse button was clicked.
*/
public synchronized int clickCount() {
return mouseInfo.clickCount();
}
/**
* @return the time when the last input event occurred.
*/
public synchronized long lastEventTime() {
return lastEventTime;
}
/**
* @return all currently active modifiers.
*/
public synchronized int modifiers() {
return modifiers;
}
/**
* @return the currently pressed key modifiers.
*/
public synchronized int keyModifiers() {
return modifiers & ~BUTTON_MASK;
}
/**
* @return the mouse buttons used in the last input event.
*/
public synchronized int buttons() {
return mouseInfo.buttons();
}
/**
* @return the mouse location relative to the component that currently contains the pointer, or {@code null} if
* outside all components.
*/
public synchronized @Nullable Point mouseLocation() {
return mouseInfo.location();
}
/**
* @return the last known mouse location.
*/
public synchronized @Nullable Point mouseLocationOnScreen() {
return mouseInfo.locationOnScreen();
}
/**
* Indicates whether there is a native drag/drop operation in progress.
*
* @return {@code true} if there is a native drag/drop operation in progress, {@code false} otherwise.
*/
public boolean isNativeDragActive() {
return dragDropInfo.isNativeDragActive();
}
}