// **********************************************************************
//
// <copyright>
//
// BBN Technologies
// 10 Moulton Street
// Cambridge, MA 02138
// (617) 873-8000
//
// Copyright (C) BBNT Solutions LLC. All rights reserved.
//
// </copyright>
// **********************************************************************
//
// $Source: /cvs/distapps/openmap/src/openmap/com/bbn/openmap/omGraphics/event/StandardMapMouseInterpreter.java,v $
// $RCSfile: StandardMapMouseInterpreter.java,v $
// $Revision: 1.18 $
// $Date: 2007/10/01 21:43:38 $
// $Author: epgordon $
//
// **********************************************************************
package com.bbn.openmap.omGraphics.event;
import java.awt.Component;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.MouseEvent;
import java.awt.geom.Point2D;
import java.util.List;
import javax.swing.JPopupMenu;
import javax.swing.SwingUtilities;
import javax.swing.Timer;
import javax.swing.ToolTipManager;
import com.bbn.openmap.event.MapMouseEvent;
import com.bbn.openmap.layer.OMGraphicHandlerLayer;
import com.bbn.openmap.omGraphics.OMGraphic;
import com.bbn.openmap.omGraphics.OMGraphicList;
import com.bbn.openmap.omGraphics.OMPoint;
import com.bbn.openmap.util.Debug;
/**
* The StandardMapMouseInterpreter is a basic implementation of the
* MapMouseInterpreter, working with an OMGraphicHandlerLayer to handle
* MouseEvents on it. This class allows the OMGraphicHandlerLayer, which
* implements the GestureResponsePolicy, to not have to deal with MouseEvents
* and the OMGraphicList, but to just react to the meanings of the user's
* gestures.
* <p>
*
* The StandardMapMouseInterpreter uses highlighting to indicate that mouse
* movement is occurring over an OMGraphic, and gives the layer three ways to
* react to that movement. After finding out if the OMGraphic is highlightable,
* the SMMI will tell the layer to highlight the OMGraphic (which usually means
* to call select() on it), provide a tool tip string for the OMGraphic, and
* provide a string to use on the InformationDelegator info line. The layer can
* reply or ignore any and all of these notifications, depending on how it's
* supposed to act.
* <p>
*
* For left mouse clicks, the SMMI uses selection as a notification that the
* user is choosing an OMGraphic, and that the OMGraphic should be prepared to
* be moved, modified or deleted. For a single OMGraphic, this is usually
* handled by handing the OMGraphic off to the OMDrawingTool. However the
* GestureResponsPolicy handles the situation where the selection is of multiple
* OMGraphics, and the layer should prepare to handle those situations as
* movement or deletion notifications. This usually means to change the
* OMGraphic's display to indicate that the OMGraphics have been selected.
* Selection notifications can come in series, and the GestureResponsePolicy is
* expected to keep track of which OMGraphics it has been told are selected.
* Deselection notifications may come as well, or other action notifications
* such as cut or copy may arrive. For cut and copy notifications, the
* OMGraphics should be removed from any selection list. For pastings, the
* OMGraphics should be added to the selection list.
* <p>
*
* For right mouse clicks, the layer will be provided with a JPopupMenu to use
* to populate with options for actions over a OMGraphic or over the map.
* <p>
*
* The StandardMapMouseInterpreter uses a timer to pace how mouse movement
* actions are responded to. Highlight reactions only occur after the mouse has
* paused over the map for the timer interval, so the application doesn't try to
* respond to constantly changing mouse locations. You can disable this delay by
* setting the timer interval to zero.
*/
public class StandardMapMouseInterpreter
implements MapMouseInterpreter {
protected boolean DEBUG = false;
protected OMGraphicHandlerLayer layer = null;
protected String[] mouseModeServiceList = null;
protected String lastToolTip = null;
protected GestureResponsePolicy grp = null;
protected GeometryOfInterest clickInterest = null;
protected GeometryOfInterest movementInterest = null;
protected boolean consumeEvents = false;
protected boolean active = true;
/**
* The OMGraphicLayer should be set at some point before use.
*/
public StandardMapMouseInterpreter() {
DEBUG = Debug.debugging("grp");
}
/**
* The standard constructor.
*/
public StandardMapMouseInterpreter(OMGraphicHandlerLayer l) {
this();
setLayer(l);
}
/**
* Helper class used to keep track of OMGraphics of interest. Interest means
* that a MouseEvent that occurred over an OMGraphic that combined with
* another MouseEvent, may be interpreted as a significant event.
*/
public class GeometryOfInterest {
OMGraphic omg;
int button;
boolean leftButton;
/**
* Create a Geometry of Interest with the OMGraphic and the first mouse
* event.
*/
public GeometryOfInterest(OMGraphic geom, MouseEvent me) {
omg = geom;
button = getButton(me);
leftButton = isLeftMouseButton(me);
}
/**
* A check to see if an OMGraphic is the same as the one of interest.
*/
public boolean appliesTo(OMGraphic geom) {
return (geom != null && geom.equals(omg));
}
/**
* A check to see if a mouse event that is occurring over an OMGraphic
* is infact occurring over the one of interest, and with the same mouse
* button.
*/
public boolean appliesTo(OMGraphic geom, MouseEvent me) {
return (geom != null && geom.equals(omg) && sameButton(me));
}
/**
* A check to see if the current mouse event concerns the same mouse
* button as the original.
*/
public boolean sameButton(MouseEvent me) {
return button == getButton(me);
}
/**
* Return the OMGraphic of interest.
*/
public OMGraphic getGeometry() {
return omg;
}
/**
* Return the button that caused the interest.
*/
public int getButton() {
return button;
}
/**
* Utility method to get around MouseEvent.getButton 1.4 requirement.
*/
protected int getButton(MouseEvent me) {
// jdk 1.4 version
// return me.getButton();
// jdk 1.3 version Don't know if the numbers are the same
// as in me.getButton, shouldn't make a difference.
if (me.isControlDown() /* PopupTrigger() */
|| SwingUtilities.isRightMouseButton(me)) {
return 1;
} else if (SwingUtilities.isLeftMouseButton(me)) {
return 0;
} else {
return 2;
}
}
/**
* Return if the current button is the left one.
*/
public boolean isLeftButton() {
return leftButton;
}
/**
* Called when the popup trigger is known to have been triggered and a
* click interest has been set by it.
*
* @param b
*/
public void setLeftButton(boolean b) {
leftButton = b;
}
}
/**
* A flag to tell the interpreter to be selfish about consuming MouseEvents
* it receives. If set to true, it will consume events so that other
* MapMouseListeners will not receive the events. If false, lower layers
* will also receive events, which will let them react too. Intended to let
* other layers provide information about what the mouse is over when
* editing is occurring.
*/
public void setConsumeEvents(boolean consume) {
consumeEvents = consume;
}
public boolean getConsumeEvents() {
return consumeEvents;
}
public void setLayer(OMGraphicHandlerLayer l) {
layer = l;
}
public OMGraphicHandlerLayer getLayer() {
return layer;
}
/**
* Set the ID's of the mouse modes that this interpreter should be listening
* to. If set to null, this SMMI won't receive MouseEvents.
*/
public void setMouseModeServiceList(String[] list) {
mouseModeServiceList = list;
}
/**
* A method to set how a left mouse button is interpreted. We count
* control-clicks as not a left mouse click.
*/
public boolean isLeftMouseButton(MouseEvent me) {
return SwingUtilities.isLeftMouseButton(me);
}
/**
* Return a list of the modes that are interesting to the MapMouseListener.
* You MUST override this with the modes you're interested in, or set the
* mouse mode service list, or you won't receive mouse events.
*/
public String[] getMouseModeServiceList() {
return mouseModeServiceList;
}
/**
* Set the GeometryOfInterest as one that could possibly be in the process
* of being clicked upon.
*/
protected void setClickInterest(GeometryOfInterest goi) {
clickInterest = goi;
}
/**
* Get the GeometryOfInterest as one that could possibly be in the process
* of being clicked upon.
*/
protected GeometryOfInterest getClickInterest() {
return clickInterest;
}
/**
* Set the GeometryOfInterest for something that the mouse is over. Prevents
* excessive modifications of the GUI if this remains constant.
*/
protected void setMovementInterest(GeometryOfInterest goi) {
movementInterest = goi;
}
/**
* Get the GeometryOfInterest for something that the mouse is over. Prevents
* excessive modifications of the GUI if this remains constant.
*/
protected GeometryOfInterest getMovementInterest() {
return movementInterest;
}
/**
* Return the OMGraphic object that is under a mouse event occurrence on the
* map, null if nothing applies.
*/
public OMGraphic getGeometryUnder(MouseEvent me) {
OMGraphic omg = null;
OMGraphicList list = null;
if (layer != null) {
list = layer.getList();
if (list != null) {
int x = me.getX();
int y = me.getY();
if (me instanceof MapMouseEvent) {
Point2D pnt = ((MapMouseEvent) me).getProjectedLocation();
x = (int) pnt.getX();
y = (int) pnt.getY();
}
omg = list.findClosest(x, y, 4);
} else {
if (DEBUG) {
Debug.output("SMMI: no layer to evaluate mouse event");
}
}
} else {
if (DEBUG) {
Debug.output("SMMI: no layer to evaluate mouse event");
}
}
return omg;
}
// Mouse Listener events
// //////////////////////
/**
* Invoked when a mouse button has been pressed on a component.
*
* @param e MouseEvent
* @return false if nothing was pressed over, or the consumeEvents setting
* if something was.
*/
public boolean mousePressed(MouseEvent e) {
if (DEBUG) {
Debug.output("SMMI:mousePressed()");
}
return setClickInterestFromMouseEvent(e);
}
/**
* Set the GeometryOfInterest based on MouseEvent. The default behavior of
* mousePressed.
*
* @param e MouseEvent
* @return whether mouse event was consumed.
*/
protected boolean setClickInterestFromMouseEvent(MouseEvent e) {
if (!active) {
return false;
}
if (DEBUG) {
Debug.output("SMMI: setClickInterestFromMouseEvent()");
}
setCurrentMouseEvent(e);
boolean ret = false;
GeometryOfInterest goi = getClickInterest();
OMGraphic omg = getGeometryUnder(e);
if (goi != null && !goi.appliesTo(omg, e)) {
// If the click doesn't match the geometry or button
// of the geometry of interest, need to tell the goi
// that is was clicked off, and set goi to null.
if (goi.isLeftButton()) {
leftClickOff(goi.getGeometry(), e);
} else {
rightClickOff(goi.getGeometry(), e);
}
setClickInterest(null);
}
if (omg != null) {
setClickInterest(new GeometryOfInterest(omg, e));
}
ret = testForAndHandlePopupTrigger(e);
if (omg != null && !ret) {
select(omg);
ret = true;
}
return ret && consumeEvents;
}
/**
* Invoked when a mouse button has been released on a component.
*
* @param e MouseEvent
* @return false
*/
public boolean mouseReleased(MouseEvent e) {
if (!active) {
return false;
}
setCurrentMouseEvent(e);
return testForAndHandlePopupTrigger(e) && consumeEvents;
}
/**
* Tests the MouseEvent to see if it's a popup trigger, and calls rightClick
* appropriately if there is an OMGraphic involved.
*
* @param e MouseEvent
* @return true if the MouseEvent is a popup trigger and has been consumed.
*/
public boolean testForAndHandlePopupTrigger(MouseEvent e) {
boolean ret = false;
if (e.isPopupTrigger()) {
GeometryOfInterest goi = getClickInterest();
// If there is a click interest
if (goi != null) {
// Tell the policy it an OMGraphic was clicked.
goi.setLeftButton(false);
ret = rightClick(goi.getGeometry(), e);
} else {
ret = rightClick(e);
}
}
return ret;
}
/**
* Invoked when the mouse has been clicked. Notifies the left click methods
* for the applicable OMGraphic or the map. Right click methods are handled
* when the testForAndHandlePopupTrigger method is called in mousePressed
* and mouseReleased.
*
* @param e MouseEvent
* @return the consumeEvents setting.
*/
public boolean mouseClicked(MouseEvent e) {
if (!active) {
return false;
}
// Should have been done already from the MousePressed, but different
// OS Java implementations have the pressed occur after click. Gah!
setClickInterestFromMouseEvent(e);
if (isLeftMouseButton(e)) {
GeometryOfInterest goi = getClickInterest();
// If there is a click interest
if (goi != null) {
// Tell the policy it an OMGraphic was clicked.
if (goi.isLeftButton()) {
leftClick(goi.getGeometry(), e);
} else {
rightClick(goi.getGeometry(), e);
}
} else {
leftClick(e);
}
return consumeEvents;
}
return false;
}
/**
* Invoked when the mouse enters a component.
*
* @param e MouseEvent
*/
public void mouseEntered(MouseEvent e) {
if (!active) {
return;
}
setCurrentMouseEvent(e);
}
/**
* Invoked when the mouse exits a component.
*
* @param e MouseEvent
*/
public void mouseExited(MouseEvent e) {
if (!active) {
return;
}
setCurrentMouseEvent(e);
}
// Mouse Motion Listener events
// /////////////////////////////
/**
* Invoked when a mouse button has been pressed and is moving. Resets the
* click geometry of interest to null.
*
* @param e MouseEvent
* @return the result from mouseMoved (also called from this method)
* combined with the consumeEvents setting.
*/
public boolean mouseDragged(MouseEvent e) {
if (!active) {
return false;
}
setCurrentMouseEvent(e);
GeometryOfInterest goi = getClickInterest();
if (goi != null) {
setClickInterest(null);
}
return mouseMoved(e) && consumeEvents;
}
/**
* Invoked when the mouse has been moved. Sets the movement geometry of
* interest and updates the movement timer.
*
* @param e MouseEvent
* @return the result of updateMouseMoved() if the timer isn't being used,
* or false.
*/
public boolean mouseMoved(MouseEvent e) {
if (!active) {
return false;
}
setCurrentMouseEvent(e);
if (getMovementInterest() == null || noTimerOverOMGraphic || mouseTimerInterval <= 0) {
return updateMouseMoved(e);
} else {
if (mouseTimer == null) {
mouseTimer = new Timer(mouseTimerInterval, mouseTimerListener);
mouseTimer.setRepeats(false);
}
mouseTimerListener.setEvent(e);
mouseTimer.restart();
return false;
}
}
protected boolean noTimerOverOMGraphic = true;
/**
* Set whether to ignore the timer when movement is occurring over an
* OMGraphic. Sometimes unhighlight can be inappropriately delayed when
* timer is enabled.
*/
public void setNoTimerOverOMGraphic(boolean val) {
noTimerOverOMGraphic = val;
}
/**
* Get whether the timer should be ignored when movement is occurring over
* an OMGraphic.
*/
public boolean getNoTimerOverOMGraphic() {
return noTimerOverOMGraphic;
}
/**
* The wait interval before a mouse over event gets triggered.
*/
protected int mouseTimerInterval = 150;
/**
* Set the time interval that the mouse timer waits before calling
* upateMouseMoved. A negative number or zero will disable the timer.
*/
public void setMouseTimerInterval(int interval) {
mouseTimerInterval = interval;
}
public int getMouseTimerInterval() {
return mouseTimerInterval;
}
/**
* The timer used to track the wait interval.
*/
protected Timer mouseTimer = null;
/**
* The timer listener that calls updateMouseMoved.
*/
protected MouseTimerListener mouseTimerListener = new MouseTimerListener();
/**
* The definition of the listener that calls updateMouseMoved when the timer
* goes off.
*/
protected class MouseTimerListener
implements ActionListener {
private MouseEvent event;
public synchronized void setEvent(MouseEvent e) {
event = e;
}
public synchronized void actionPerformed(ActionEvent ae) {
if (event != null) {
updateMouseMoved(event);
}
}
}
/**
* The real mouseMoved call, called when mouseMoved is called and, if there
* is a mouse timer interval set, that interval time has passed.
*
* @return the consumeEvents setting of the mouse event concerns an
* OMGraphic, false if it didn't.
*/
protected boolean updateMouseMoved(MouseEvent e) {
boolean ret = false;
OMGraphic omg = getGeometryUnder(e);
GeometryOfInterest goi = getMovementInterest();
boolean mouseOverCurrentGOI = (goi != null && goi.appliesTo(omg));
if (goi != null && !mouseOverCurrentGOI) {
// We already had a GOI from preious event, but it's not under the event now...
mouseNotOver(goi.getGeometry());
setMovementInterest(null);
} else {
ret = (goi != null);
}
if (omg != null) {
// Mouse over OMGraphic
if (!mouseOverCurrentGOI) {
// We get in here if the GOI should be changed to a new OMGraphic.
setMovementInterest(new GeometryOfInterest(omg, e));
// Add a specialized check for OMPoint because it shouldn't have a delayed unhighlight.
setNoTimerOverOMGraphic(!omg.shouldRenderFill() || omg instanceof OMPoint);
ret = mouseOver(omg, e);
}
} else {
// Current mouse event not over an OMGraphic
ret = mouseOver(e);
}
ret = ret && consumeEvents;
if (ret) {
e.consume();
}
return ret;
}
/**
* Handle notification that another layer consumed a mouse moved event. Sets
* movement interest to null.
*/
public void mouseMoved() {
if (!active) {
return;
}
GeometryOfInterest goi = getMovementInterest();
if (goi != null) {
mouseNotOver(goi.getGeometry());
setMovementInterest(null);
}
}
/**
* Handle a left-click on the map. Does nothing by default.
*
* @return false
*/
public boolean leftClick(MouseEvent me) {
if (DEBUG) {
Debug.output("leftClick(MAP) at " + me.getX() + ", " + me.getY());
}
if (grp != null && grp.receivesMapEvents() && me instanceof MapMouseEvent) {
return grp.leftClick((MapMouseEvent) me);
}
return false;
}
/**
* Handle a left-click on an OMGraphic. Does nothing by default.
*
* @return true
*/
public boolean leftClick(OMGraphic omg, MouseEvent me) {
if (DEBUG) {
Debug.output("leftClick(" + omg.getClass().getName() + ") at " + me.getX() + ", " + me.getY());
}
return false;
}
/**
* Notification that the user clicked on something else other than the
* provided OMGraphic that was previously left-clicked on. Calls
* deselect(omg).
*
* @return false
*/
public boolean leftClickOff(OMGraphic omg, MouseEvent me) {
if (DEBUG) {
Debug.output("leftClickOff(" + omg.getClass().getName() + ") at " + me.getX() + ", " + me.getY());
}
deselect(omg);
return false;
}
/**
* Notification that the map was right-clicked on.
*
* @return false
*/
public boolean rightClick(MouseEvent me) {
if (DEBUG) {
Debug.output("rightClick(MAP) at " + me.getX() + ", " + me.getY());
}
if (me instanceof MapMouseEvent && grp != null) {
return displayPopup(grp.getItemsForMapMenu((MapMouseEvent) me), me);
}
return false;
}
/**
* Notification that an OMGraphic was right-clicked on.
*
* @return true
*/
public boolean rightClick(OMGraphic omg, MouseEvent me) {
if (DEBUG) {
Debug.output("rightClick(" + omg.getClass().getName() + ") at " + me.getX() + ", " + me.getY());
}
if (grp != null) {
return displayPopup(grp.getItemsForOMGraphicMenu(omg), me);
}
return false;
}
/**
* Create a pop-up menu from GRP requests, over the mouse event location.
*
* @return true if pop-up was presented, false if not.
*/
protected boolean displayPopup(List<Component> contents, MouseEvent me) {
if (DEBUG) {
Debug.output("displayPopup(" + contents + ") " + me);
}
if (contents != null && !contents.isEmpty()) {
JPopupMenu jpm = new JPopupMenu();
for (Component comp : contents) {
jpm.add(comp);
}
jpm.show((Component) me.getSource(), me.getX(), me.getY());
return true;
}
return false;
}
/**
* Notification that the user clicked on something else other than the
* provided OMGraphic that was previously right-clicked on.
*
* @return false
*/
public boolean rightClickOff(OMGraphic omg, MouseEvent me) {
if (DEBUG) {
Debug.output("rightClickOff(" + omg.getClass().getName() + ") at " + me.getX() + ", " + me.getY());
}
return false;
}
/**
* Notification that the mouse is not over an OMGraphic, but over the map at
* some location.
*
* @return false
*/
public boolean mouseOver(MouseEvent me) {
if (DEBUG) {
Debug.output("mouseOver(MAP) at " + me.getX() + ", " + me.getY());
}
if (grp != null && grp.receivesMapEvents() && me instanceof MapMouseEvent) {
return grp.mouseOver((MapMouseEvent) me);
}
return false;
}
/**
* Notification that the mouse is over an OMGraphic. Makes all the highlight
* calls.
*
* @return true
*/
public boolean mouseOver(OMGraphic omg, MouseEvent me) {
if (DEBUG) {
Debug.output("mouseOver(" + omg.getClass().getName() + ") at " + me.getX() + ", " + me.getY());
}
if (grp != null && !me.isConsumed()) {
handleToolTip(grp.getToolTipTextFor(omg), me);
handleInfoLine(grp.getInfoText(omg));
if (grp.isHighlightable(omg)) {
grp.highlight(omg);
}
}
return true;
}
/**
* Given a tool tip String, use the layer to get it displayed.
*/
protected void handleToolTip(String tip, MouseEvent me) {
if (lastToolTip != null && lastToolTip.equals(tip)) {
return;
}
lastToolTip = tip;
if (layer != null) {
if (lastToolTip != null && lastToolTip.trim().length() > 0) {
layer.fireRequestToolTip(lastToolTip);
// forward the event to the tool tip manager so it will popup
// the tool tip right away, otherwise an additional event is
// required to trigger it
ToolTipManager toolTipManager = ToolTipManager.sharedInstance();
toolTipManager.mouseMoved(me);
} else {
layer.fireHideToolTip();
}
}
}
/**
* Given an information line, use the layer to get it displayed on the
* InformationDelegator.
*/
protected void handleInfoLine(String line) {
if (layer != null) {
layer.fireRequestInfoLine((line == null) ? "" : line);
}
}
/**
* Notification that the mouse has moved off of an OMGraphic.
*/
public boolean mouseNotOver(OMGraphic omg) {
if (DEBUG) {
Debug.output("mouseNotOver(" + omg.getClass().getName() + ")");
}
if (grp != null) {
grp.unhighlight(omg);
}
handleToolTip(null, null);
handleInfoLine(null);
return false;
}
/**
* Notify the GRP that the OMGraphic has been selected. Wraps the OMGraphic
* in an OMGraphicList.
*/
public void select(OMGraphic omg) {
if (grp != null && grp.isSelectable(omg)) {
OMGraphicList omgl = new OMGraphicList();
omgl.add(omg);
grp.select(omgl);
}
}
/**
* Notify the GRP that the OMGraphic has been deselected. Wraps the
* OMGraphic in an OMGraphicList.
*/
public void deselect(OMGraphic omg) {
if (grp != null && grp.isSelectable(omg)) {
OMGraphicList omgl = new OMGraphicList();
omgl.add(omg);
grp.deselect(omgl);
}
}
/**
* The last MouseEvent received, for later reference.
*/
protected MouseEvent currentMouseEvent;
/**
* Set the last MouseEvent received.
*/
protected void setCurrentMouseEvent(MouseEvent me) {
currentMouseEvent = me;
}
/**
* Get the last MouseEvent received.
*/
public MouseEvent getCurrentMouseEvent() {
return currentMouseEvent;
}
/**
* Set the GestureResponsePolicy to notify of the mouse actions over the
* layer's OMGraphicList.
*/
public void setGRP(GestureResponsePolicy grp) {
this.grp = grp;
}
/**
* Get the GestureResponsePolicy that is being notified of the mouse actions
* over the layer's OMGraphicList.
*/
public GestureResponsePolicy getGRP() {
return grp;
}
/**
* Check whether the MapMouseInterpreter is responding to events.
*
* @return true if willing to respond to MouseEvents.
*/
public boolean isActive() {
return active;
}
/**
* Set whether the MapMouseInterpreter responds
*
* @param active
*/
public void setActive(boolean active) {
this.active = active;
}
}