// License: GPL. Copyright 2007 by Immanuel Scholz and others package org.openstreetmap.josm.gui; import static org.openstreetmap.josm.tools.I18n.tr; import java.awt.AWTEvent; import java.awt.Cursor; import java.awt.EventQueue; import java.awt.Font; import java.awt.GridBagLayout; import java.awt.Point; import java.awt.Toolkit; import java.awt.event.AWTEventListener; import java.awt.event.InputEvent; import java.awt.event.MouseAdapter; import java.awt.event.MouseEvent; import java.awt.event.MouseMotionListener; import java.lang.reflect.InvocationTargetException; import java.util.Collection; import java.util.ConcurrentModificationException; import java.util.Map.Entry; import javax.swing.BorderFactory; import javax.swing.BoxLayout; import javax.swing.JLabel; import javax.swing.JPanel; import javax.swing.JTextField; import javax.swing.Popup; import javax.swing.PopupFactory; import org.openstreetmap.josm.Main; import org.openstreetmap.josm.actions.HelpAction.Helpful; import org.openstreetmap.josm.data.coor.LatLon; import org.openstreetmap.josm.data.osm.OsmPrimitive; import org.openstreetmap.josm.data.osm.visitor.NameVisitor; import org.openstreetmap.josm.tools.GBC; /** * A component that manages some status information display about the map. * It keeps a status line below the map up to date and displays some tooltip * information if the user hold the mouse long enough at some point. * * All this is done in background to not disturb other processes. * * The background thread does not alter any data of the map (read only thread). * Also it is rather fail safe. In case of some error in the data, it just do * nothing instead of whining and complaining. * * @author imi */ public class MapStatus extends JPanel implements Helpful { /** * The MapView this status belongs. */ final MapView mv; /** * The position of the mouse cursor. */ JTextField positionText = new JTextField("-000.00000000000000 -000.00000000000000".length()); /** * The field holding the name of the object under the mouse. */ JTextField nameText = new JTextField(30); /** * The collector class that waits for notification and then update * the display objects. * * @author imi */ private final class Collector implements Runnable { /** * The last object displayed in status line. */ Collection osmStatus; /** * The old modifiers, that was pressed the last time this collector ran. */ private int oldModifiers; /** * The popup displayed to show additional information */ private Popup popup; private MapFrame parent; public Collector(MapFrame parent) { this.parent = parent; } /** * Execution function for the Collector. */ public void run() { for (;;) { MouseState ms = new MouseState(); synchronized (this) { try {wait();} catch (InterruptedException e) {} ms.modifiers = mouseState.modifiers; ms.mousePos = mouseState.mousePos; } if (parent != Main.map) return; // exit, if new parent. if ((ms.modifiers & MouseEvent.CTRL_DOWN_MASK) != 0 || ms.mousePos == null) continue; // freeze display when holding down ctrl if (mv.center == null) continue; // This try/catch is a hack to stop the flooding bug reports about this. // The exception needed to handle with in the first place, means that this // access to the data need to be restarted, if the main thread modifies // the data. try { Collection osms = mv.getAllNearest(ms.mousePos); if (osms == null && osmStatus == null && ms.modifiers == oldModifiers) continue; if (osms != null && osms.equals(osmStatus) && ms.modifiers == oldModifiers) continue; osmStatus = osms; oldModifiers = ms.modifiers; OsmPrimitive osmNearest = null; // Set the text label in the bottom status bar osmNearest = mv.getNearest(ms.mousePos, (ms.modifiers & MouseEvent.ALT_DOWN_MASK) != 0); if (osmNearest != null) { NameVisitor visitor = new NameVisitor(); osmNearest.visit(visitor); nameText.setText(visitor.name); } else nameText.setText(""); // Popup Information if ((ms.modifiers & MouseEvent.BUTTON2_DOWN_MASK) != 0 && osms != null) { if (popup != null) { try { EventQueue.invokeAndWait(new Runnable() { public void run() { popup.hide(); } }); } catch (InterruptedException e) { } catch (InvocationTargetException e) { throw new RuntimeException(e); } } JPanel c = new JPanel(new GridBagLayout()); for (final OsmPrimitive osm : osms) { NameVisitor visitor = new NameVisitor(); osm.visit(visitor); final StringBuilder text = new StringBuilder(); if (osm.id == 0 || osm.modified) visitor.name = ""+visitor.name+"*"; text.append(visitor.name); if (osm.id != 0) text.append("
id="+osm.id); for (Entry e : osm.entrySet()) text.append("
"+e.getKey()+"="+e.getValue()); final JLabel l = new JLabel(""+text.toString()+"", visitor.icon, JLabel.HORIZONTAL); l.setFont(l.getFont().deriveFont(Font.PLAIN)); l.setVerticalTextPosition(JLabel.TOP); l.setCursor(Cursor.getPredefinedCursor(Cursor.HAND_CURSOR)); l.addMouseListener(new MouseAdapter(){ @Override public void mouseEntered(MouseEvent e) { l.setText(""+text.toString()+""); } @Override public void mouseExited(MouseEvent e) { l.setText(""+text.toString()+""); } @Override public void mouseClicked(MouseEvent e) { Main.ds.setSelected(osm); mv.repaint(); } }); c.add(l, GBC.eol()); } Point p = mv.getLocationOnScreen(); popup = PopupFactory.getSharedInstance().getPopup(mv, c, p.x+ms.mousePos.x+16, p.y+ms.mousePos.y+16); final Popup staticPopup = popup; EventQueue.invokeLater(new Runnable(){ public void run() { staticPopup.show(); } }); } else if (popup != null) { final Popup staticPopup = popup; popup = null; EventQueue.invokeLater(new Runnable(){ public void run() { staticPopup.hide(); } }); } } catch (ConcurrentModificationException x) { } catch (NullPointerException x) { } } } } /** * Everything, the collector is interested of. Access must be synchronized. * @author imi */ class MouseState { Point mousePos; int modifiers; } /** * The last sent mouse movement event. */ MouseState mouseState = new MouseState(); /** * Construct a new MapStatus and attach it to the map view. * @param mv The MapView the status line is part of. */ public MapStatus(final MapFrame mapFrame) { this.mv = mapFrame.mapView; // Listen for mouse movements and set the position text field mv.addMouseMotionListener(new MouseMotionListener(){ public void mouseDragged(MouseEvent e) { mouseMoved(e); } public void mouseMoved(MouseEvent e) { if (mv.center == null) return; // Do not update the view, if ctrl is pressed. if ((e.getModifiersEx() & MouseEvent.CTRL_DOWN_MASK) == 0) { LatLon p = mv.getLatLon(e.getX(),e.getY()); positionText.setText(p.lat()+" "+p.lon()); } } }); positionText.setEditable(false); nameText.setEditable(false); setLayout(new BoxLayout(this, BoxLayout.X_AXIS)); setBorder(BorderFactory.createEmptyBorder(5,5,5,5)); add(new JLabel(tr("Lat/Lon")+" ")); add(positionText); add(new JLabel(" "+tr("Object")+" ")); add(nameText); // The background thread final Collector collector = new Collector(mapFrame); new Thread(collector).start(); // Listen to keyboard/mouse events for pressing/releasing alt key and // inform the collector. Toolkit.getDefaultToolkit().addAWTEventListener(new AWTEventListener(){ public void eventDispatched(AWTEvent event) { synchronized (collector) { mouseState.modifiers = ((InputEvent)event).getModifiersEx(); if (event instanceof MouseEvent) mouseState.mousePos = ((MouseEvent)event).getPoint(); collector.notify(); } } }, AWTEvent.KEY_EVENT_MASK | AWTEvent.MOUSE_EVENT_MASK | AWTEvent.MOUSE_MOTION_EVENT_MASK); } public String helpTopic() { return "Statusline"; } }