package org.freeplane.view.swing.map; import java.awt.Component; import java.awt.Dimension; import java.awt.KeyboardFocusManager; import java.awt.Point; import java.awt.Rectangle; import java.awt.Window; import java.awt.event.ActionEvent; import java.awt.event.ActionListener; import java.awt.event.ComponentAdapter; import java.awt.event.ComponentEvent; import java.awt.event.MouseAdapter; import java.awt.event.MouseEvent; import java.awt.event.MouseMotionListener; import java.lang.ref.WeakReference; import javax.swing.JComponent; import javax.swing.JDialog; import javax.swing.JToolTip; import javax.swing.Popup; import javax.swing.PopupFactory; import javax.swing.SwingUtilities; import javax.swing.Timer; import org.freeplane.core.extension.IExtension; import org.freeplane.core.resources.IFreeplanePropertyListener; import org.freeplane.core.resources.ResourceController; import org.freeplane.features.map.AMapChangeListenerAdapter; import org.freeplane.features.map.IMapChangeListener; import org.freeplane.features.map.INodeSelectionListener; import org.freeplane.features.map.MapController; import org.freeplane.features.map.MapModel; import org.freeplane.features.map.NodeModel; import org.freeplane.features.mode.Controller; import org.freeplane.features.mode.ModeController; public class NodeTooltipManager implements IExtension{ private static final String TOOL_TIP_MANAGER = "toolTipManager."; private static final String TOOL_TIP_MANAGER_INITIAL_DELAY = "toolTipManager.initialDelay"; private static final String RESOURCES_SHOW_NODE_TOOLTIPS = "show_node_tooltips"; private Timer enterTimer; private Timer exitTimer; private String toolTipText; private JComponent insideComponent; private MouseEvent mouseEvent; private Popup tipPopup; /** The Window tip is being displayed in. This will be non-null if * the Window tip is in differs from that of insideComponent's Window. */ private JToolTip tip; final private ComponentMouseListener componentMouseListener; private WeakReference<Component> focusOwnerRef; public static NodeTooltipManager getSharedInstance(ModeController modeController){ { final NodeTooltipManager instance = (NodeTooltipManager) modeController.getExtension(NodeTooltipManager.class); if(instance != null){ return instance; } } final NodeTooltipManager instance = new NodeTooltipManager(); final int maxWidth = ResourceController.getResourceController().getIntProperty( "toolTipManager.max_tooltip_width", Integer.MAX_VALUE); NodeTooltip.setMaximumWidth(maxWidth); setTooltipDelays(instance); ResourceController.getResourceController().addPropertyChangeListener(new IFreeplanePropertyListener() { public void propertyChanged(final String propertyName, final String newValue, final String oldValue) { if (propertyName.startsWith(TOOL_TIP_MANAGER)) { setTooltipDelays(instance); } } }); IMapChangeListener mapChangeListener = new AMapChangeListenerAdapter() { @Override public void onNodeDeleted(NodeModel parent, NodeModel child, int index) { instance.hideTipWindow(); } @Override public void onNodeInserted(NodeModel parent, NodeModel child, int newIndex) { instance.hideTipWindow(); } @Override public void onNodeMoved(NodeModel oldParent, int oldIndex, NodeModel newParent, NodeModel child, int newIndex) { instance.hideTipWindow(); } public void onSavedAs(MapModel map) { } public void onSaved(MapModel map) { } }; MapController mapController = modeController.getMapController(); mapController.addMapChangeListener(mapChangeListener); INodeSelectionListener nodeSelectionListener = new INodeSelectionListener() { public void onSelect(NodeModel node) { NodeView view = (NodeView) SwingUtilities.getAncestorOfClass(NodeView.class, instance.insideComponent); if(view != null && node.equals(view.getModel())) return; instance.hideTipWindow(); } public void onDeselect(NodeModel node) { } }; mapController.addNodeSelectionListener(nodeSelectionListener); modeController.addExtension(NodeTooltipManager.class, instance); return instance; } private static void setTooltipDelays(NodeTooltipManager instance) { final int initialDelay = ResourceController.getResourceController().getIntProperty( TOOL_TIP_MANAGER_INITIAL_DELAY, 0); instance.setInitialDelay(initialDelay); } private NodeTooltipManager() { enterTimer = new Timer(750, new insideTimerAction()); enterTimer.setRepeats(false); exitTimer = new Timer(150, new exitTimerAction()); exitTimer.setRepeats(false); componentMouseListener = new ComponentMouseListener(); } /** * Specifies the initial delay value. * * @param milliseconds the number of milliseconds to delay * (after the cursor has paused) before displaying the * tooltip * @see #getInitialDelay */ public void setInitialDelay(int milliseconds) { enterTimer.setInitialDelay(milliseconds); } /** * Returns the initial delay value. * * @return an integer representing the initial delay value, * in milliseconds * @see #setInitialDelay */ public int getInitialDelay() { return enterTimer.getInitialDelay(); } private void showTipWindow() { if (insideComponent == null || !insideComponent.isShowing()) return; tip = insideComponent.createToolTip(); tip.addComponentListener(new ComponentAdapter() { @Override public void componentResized(ComponentEvent e) { final NodeTooltip component = (NodeTooltip) e.getComponent(); component.scrollUp(); component.removeComponentListener(this); } }); tip.setTipText(toolTipText); PopupFactory popupFactory = PopupFactory.getSharedInstance(); final JComponent nearComponent; // if (insideComponent instanceof MainView) { // nearComponent = ((MainView)insideComponent).getNodeView().getContent(); // } // else{ nearComponent = insideComponent; // } final Point locationOnScreen = nearComponent.getLocationOnScreen(); final int height = nearComponent.getHeight(); Rectangle sBounds = nearComponent.getGraphicsConfiguration().getBounds(); final int minX = sBounds.x; final int maxX = sBounds.x + sBounds.width; final int minY = sBounds.y; final int maxY = sBounds.y + sBounds.height; int x = locationOnScreen.x; int y = locationOnScreen.y + height; final Dimension tipSize = tip.getPreferredSize(); final int tipWidth = tipSize.width; if(x + tipWidth > maxX){ x = maxX - tipWidth; } if(x < minX){ x = minX; } final int tipHeight = tipSize.height; if(y + tipHeight > maxY){ if(locationOnScreen.y - tipHeight > minY){ y = locationOnScreen.y - tipHeight; } else{ y = maxY - tipHeight; } } if(y < minY){ y = minY; } focusOwnerRef = new WeakReference<Component>(KeyboardFocusManager.getCurrentKeyboardFocusManager().getFocusOwner()); tipPopup = popupFactory.getPopup(nearComponent, tip, x, y); tipPopup.show(); exitTimer.start(); } private void hideTipWindow() { insideComponent = null; toolTipText = null; mouseEvent = null; if (tipPopup != null && tip != null) { final Component component; final Component focusOwner = KeyboardFocusManager.getCurrentKeyboardFocusManager().getFocusOwner(); if(focusOwner != null && SwingUtilities.isDescendingFrom(focusOwner, tip)){ component = focusOwnerRef.get(); } else component = null; tipPopup.hide(); if(component != null) component.requestFocusInWindow(); tipPopup = null; tip = null; focusOwnerRef = null; enterTimer.stop(); exitTimer.stop(); } } /** * Registers a component for tooltip management. * <p> * This will register key bindings to show and hide the tooltip text * only if <code>component</code> has focus bindings. This is done * so that components that are not normally focus traversable, such * as <code>JLabel</code>, are not made focus traversable as a result * of invoking this method. * * @param component a <code>JComponent</code> object to add * @see JComponent#isFocusTraversable */ public void registerComponent(JComponent component) { component.removeMouseListener(componentMouseListener); component.removeMouseMotionListener(componentMouseListener); component.addMouseListener(componentMouseListener); component.addMouseMotionListener(componentMouseListener); } /** * Removes a component from tooltip control. * * @param component a <code>JComponent</code> object to remove */ public void unregisterComponent(JComponent component) { component.removeMouseListener(componentMouseListener); } private class ComponentMouseListener extends MouseAdapter implements MouseMotionListener{ public void mouseEntered(MouseEvent event) { initiateToolTip(event); } public void mouseMoved(MouseEvent event) { initiateToolTip(event); } public void mouseExited(MouseEvent event) { } public void mouseDragged(MouseEvent e) { } @Override public void mousePressed(MouseEvent e) { hideTipWindow(); } } private void initiateToolTip(MouseEvent event) { JComponent component = (JComponent) event.getSource(); if(insideComponent == component){ return; } hideTipWindow(); insideComponent = component; mouseEvent = event; if(ResourceController.getResourceController().getBooleanProperty(RESOURCES_SHOW_NODE_TOOLTIPS)) enterTimer.restart(); } protected boolean mouseOverComponent() { if(insideComponent.isShowing()){ final Point mousePosition = insideComponent.getMousePosition(true); return mousePosition != null && new Rectangle(0, 0, insideComponent.getWidth(), insideComponent.getHeight()).contains(mousePosition); } return false; } private class insideTimerAction implements ActionListener { public void actionPerformed(ActionEvent e) { if (insideComponent != null){ if( mouseOverComponent()) { // Lazy lookup if (toolTipText == null && mouseEvent != null) { toolTipText = insideComponent.getToolTipText(mouseEvent); } if (toolTipText != null) { showTipWindow(); return; } } hideTipWindow(); } } } private class exitTimerAction implements ActionListener { public void actionPerformed(ActionEvent e) { if(tip == null || insideComponent == null){ return; } final KeyboardFocusManager currentKeyboardFocusManager = KeyboardFocusManager.getCurrentKeyboardFocusManager(); final Window activeWindow = currentKeyboardFocusManager.getActiveWindow(); if(activeWindow instanceof JDialog && ((JDialog) activeWindow).isModal() && ! SwingUtilities.isDescendingFrom(Controller.getCurrentController().getViewController().getMapView(), activeWindow)){ hideTipWindow(); return; } if(tip.getMousePosition(true) != null || mouseOverComponent()){ exitTimer.restart(); return; } final Component focusOwner = currentKeyboardFocusManager.getFocusOwner(); if(focusOwner != null){ if(SwingUtilities.isDescendingFrom(focusOwner, tip)){ exitTimer.restart(); return; } } hideTipWindow(); } } }