// ********************************************************************** // // <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; } }