/* * RapidMiner * * Copyright (C) 2001-2011 by Rapid-I and the contributors * * Complete list of developers available at our web site: * * http://rapid-i.com * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Affero General Public License for more details. * * You should have received a copy of the GNU Affero General Public License * along with this program. If not, see http://www.gnu.org/licenses/. */ package com.rapidminer.gui.tools.components; import java.awt.AWTEvent; import java.awt.BorderLayout; import java.awt.Color; import java.awt.Component; import java.awt.Dialog; import java.awt.Dimension; import java.awt.Font; import java.awt.Insets; import java.awt.MouseInfo; import java.awt.Point; import java.awt.Rectangle; import java.awt.Toolkit; import java.awt.event.AWTEventListener; import java.awt.event.ActionEvent; import java.awt.event.ActionListener; import java.awt.event.KeyEvent; import java.awt.event.MouseAdapter; import java.awt.event.MouseEvent; import java.awt.event.MouseMotionListener; import java.awt.event.WindowAdapter; import java.awt.event.WindowEvent; import java.awt.event.WindowFocusListener; import java.util.concurrent.atomic.AtomicBoolean; import javax.swing.AbstractAction; import javax.swing.Action; import javax.swing.BorderFactory; import javax.swing.JComponent; import javax.swing.JDialog; import javax.swing.JPanel; import javax.swing.JScrollPane; import javax.swing.KeyStroke; import javax.swing.SwingUtilities; import javax.swing.Timer; import javax.swing.event.HyperlinkEvent; import javax.swing.event.HyperlinkListener; import javax.swing.text.html.HTMLEditorKit; import javax.swing.text.html.StyleSheet; import com.rapidminer.gui.MainFrame; import com.rapidminer.gui.RapidMinerGUI; import com.rapidminer.gui.tools.ExtendedHTMLJEditorPane; import com.rapidminer.gui.tools.ProgressThread; import com.rapidminer.gui.tools.ResourceLabel; import com.rapidminer.gui.tools.SwingTools; import com.rapidminer.repository.Entry; import com.rapidminer.repository.IOObjectEntry; import com.rapidminer.repository.RepositoryLocation; import com.rapidminer.tools.RMUrlHandler; /** This class manages dynamic largish tool tips for JComponents. * In order to use this class, implement a {@link TipProvider} that generates * tool tip texts depending on the a mouse position relative to a component and * pass this component and the tip provider to the constructor of this class. * * This class will listen to mouse events of the specified component and will * display an undecorated scrollable dialog whenever the mouse does not move for a certain time. * The user can focus (an then resize) the dialog by pressing F3. * * @author Simon Fischer * */ public class ToolTipWindow { private static final long serialVersionUID = 1L; public interface TipProvider { /** Returns the actual tip belonging to this point. Called after {@link #getIdUnder(Point)}. */ public String getTip(Object id); /** Returns an additional tooltip component to be added below the text field. */ public Component getCustomComponent(Object id); /** Returns an ID of the object under the given mouse position. * This is only used to determine whether the mouse has left the area corresponding to the current * tool tip. We could have called {@link #getTip(Object)} directly, however this may be a too time * consuming operation. * Note: IDs are compared by == ! */ public Object getIdUnder(Point point); } private enum State { IDLE, SHOWING_TIP, IN_FOCUS, DISPOSED } /** Component observed by this object. */ private final JComponent parent; private final TipProvider tipProvider; private State state = State.IDLE; /** Point at which the tip was last displayed. Relative to {@link #parent}. */ private Point lastPoint; /** Mouse position when it was last moved. Relative to {@link #parent}. */ private Point lastMousePosition; /** Position of the mouse at the point of time when the tip was displayed.*/ private Point mousePositionAtPopup; private Object currentId; /** Panel containing the {@link #tipScrollPane} and a short label (F3 to focus). */ private final JPanel mainPanel = new JPanel(new BorderLayout()); /** Pane containing the help text. */ private final ExtendedHTMLJEditorPane tipPane = new ExtendedHTMLJEditorPane("text/html", "<html></html>"); private Component customComponent; /** Contains the {@link #tipPane}. */ private final JScrollPane tipScrollPane; /** Current (decorated or undecorated) dialog containing the main panel. May be null if state is IDLE. */ private JDialog currentDialog; /** Shows a tip after 500ms. */ private final Timer showTipTimer = new Timer(500, new ActionListener() { @Override public void actionPerformed(ActionEvent e) { showTip(); } }); // /** Hides the tip after 25s. */ // private final Timer hideTipTimer = new Timer(25000, new ActionListener() { // @Override // public void actionPerformed(ActionEvent e) { // hideTip(); // } // }); /** Decorates the tip dialog if the user presses F3. */ private final Action FOCUS_TIP_ACTION = new AbstractAction() { private static final long serialVersionUID = 1L; @Override public void actionPerformed(ActionEvent e) { focusTip(); } }; private final ResourceLabel f3Label; private final Dialog owner; private boolean mouseOnParentIsDown = false; public ToolTipWindow(TipProvider tipProvider, JComponent parent) { this(null, tipProvider, parent); tipPane.setToolTipText(null); } /** * TODO: Is there a way to find the owner elegantly? Travers ancestors? * * @param owner The owner of the tool tip dialog. If null, the {@link MainFrame} will be used. * If this tool tip is for a component in a dialog, but the owner is not set, the tool tip * will be displayed behind the dialog. * @param tipProvider Generates tool tip texts whenever needed * @param parent The component to observe */ public ToolTipWindow(Dialog owner, TipProvider tipProvider, JComponent parent) { this.owner = owner; this.tipProvider = tipProvider; this.parent = parent; showTipTimer.setRepeats(false); // hideTipTimer.setRepeats(false); tipPane.setFont(new Font("Sans-serif", Font.PLAIN, 9)); tipPane.setMargin(new Insets(4, 4, 4, 4)); tipPane.setEditable(false); tipPane.addHyperlinkListener(new HyperlinkListener() { @Override public void hyperlinkUpdate(HyperlinkEvent e) { if (e.getEventType() == HyperlinkEvent.EventType.ACTIVATED) { // if (e.getDescription().startsWith("operator?")) { // String opName = e.getDescription().substring("operator?".length()); // MainFrame mainFrame = RapidMinerGUI.getMainFrame(); // mainFrame.selectOperator(mainFrame.getProcess().getOperator(opName)); // } else { if (e.getDescription().startsWith("loadMetaData?")) { final String loc = e.getDescription().substring("loadMetaData?".length()); final Object idAtTimeOfDownload = currentId; tipPane.setText("<p>Please stand by...</p>"); final AtomicBoolean tipWasClosed = new AtomicBoolean(false); currentDialog.addWindowListener(new WindowAdapter() { @Override public void windowClosed(WindowEvent e) { tipWasClosed.set(true); } }); new ProgressThread("download_md_from_repository") { @Override public void run() { getProgressListener().setTotal(100); getProgressListener().setCompleted(10); try { Entry entry = new RepositoryLocation(loc).locateEntry(); if (entry instanceof IOObjectEntry) { ((IOObjectEntry) entry).retrieveMetaData(); } } catch (Exception e) { SwingTools.showSimpleErrorMessage("error_downloading_metadata", e, loc, e.getMessage()); } finally { getProgressListener().complete(); } SwingUtilities.invokeLater(new Runnable() { @Override public void run() { if (!tipWasClosed.get()) { refreshDialogContents(idAtTimeOfDownload); autoAdjustDialogSize(state == State.IN_FOCUS); currentDialog.pack(); } } }); } }.start(); } else { RMUrlHandler.handleUrl(e.getDescription()); } } } }); tipPane.setBorder(BorderFactory.createEmptyBorder(4, 4, 4, 4)); StyleSheet css = ((HTMLEditorKit)tipPane.getEditorKit()).getStyleSheet(); css.addRule("body {font-family:Sans;font-size:12pt}"); css.addRule("h3 {margin:0; padding:0}"); css.addRule("h4 {margin-bottom:0; margin-top:1ex; padding:0}"); css.addRule("p {margin-top:0; margin-bottom:1ex; padding:0}"); css.addRule("ul {margin-top:0; margin-bottom:1ex; list-style-image: url(" + getClass().getResource("/com/rapidminer/resources/icons/modern/help/circle.png") + ")}"); css.addRule("ul li {padding-bottom: 2px}"); css.addRule("li.outPorts {padding-bottom: 0px}"); css.addRule("ul li ul {margin-top:0; margin-bottom:1ex; list-style-image: url(" + getClass().getResource("/com/rapidminer/resources/icons/modern/help/line.png") + ")"); css.addRule("li ul li {padding-bottom:0}"); tipScrollPane = new JScrollPane(tipPane); tipScrollPane.setBorder(null); mainPanel.add(tipScrollPane, BorderLayout.CENTER); f3Label = new ResourceLabel("F3_for_focus"); f3Label.setBorder(BorderFactory.createCompoundBorder(BorderFactory.createMatteBorder(1, 0, 0, 0, Color.LIGHT_GRAY), BorderFactory.createEmptyBorder(0, 4, 0, 0))); mainPanel.add(f3Label, BorderLayout.SOUTH); //int focusCondition = JComponent.WHEN_IN_FOCUSED_WINDOW; int focusCondition = JComponent.WHEN_FOCUSED; parent.getInputMap(focusCondition).put(KeyStroke.getKeyStroke(KeyEvent.VK_F3, 0), "focusTip"); parent.getActionMap().put("focusTip", FOCUS_TIP_ACTION); mainPanel.getInputMap(focusCondition).put(KeyStroke.getKeyStroke(KeyEvent.VK_F3, 0), "focusTip"); mainPanel.getActionMap().put("focusTip", FOCUS_TIP_ACTION); tipPane.getInputMap(JComponent.WHEN_IN_FOCUSED_WINDOW).put(KeyStroke.getKeyStroke(KeyEvent.VK_F3, 0), "focusTip"); mainPanel.getActionMap().put("focusTip", FOCUS_TIP_ACTION); parent.addMouseListener(new MouseAdapter() { @Override public void mousePressed(MouseEvent e) { mouseOnParentIsDown = true; showTipTimer.stop(); if (state == State.SHOWING_TIP) { hideTip(); } } @Override public void mouseReleased(MouseEvent e) { mouseOnParentIsDown = false; if (state == State.IDLE) { showTipTimer.start(); } } @Override public void mouseExited(MouseEvent e) { mouseOnParentIsDown = false; showTipTimer.stop(); } @Override public void mouseEntered(MouseEvent e) { mouseOnParentIsDown = false; if (state == State.IDLE) { showTipTimer.start(); } } }); parent.addMouseMotionListener(new MouseMotionListener() { @Override public void mouseMoved(MouseEvent e) { parentIsActive(e); } @Override public void mouseDragged(MouseEvent e) { parentIsActive(e); } }); // We use this approach for tracking the mouse exited event from the dialog. // http://weblogs.java.net/blog/alexfromsun/archive/2006/09/a_wellbehaved_g.html // The method described in the Java Tutorial on using glasspanes described here: // http://java.sun.com/docs/books/tutorial/uiswing/components/rootpane.html // fails for various reasons. Most importantly, it makes the scroll bars unusable. Toolkit.getDefaultToolkit().addAWTEventListener(new AWTEventListener() { @Override public void eventDispatched(AWTEvent event) { if (event instanceof MouseEvent) { MouseEvent me = (MouseEvent) event; if ((me.getID() != MouseEvent.MOUSE_EXITED) || (state != State.SHOWING_TIP) || !SwingUtilities.isDescendingFrom(me.getComponent(), currentDialog)) { return; } Point origin = currentDialog.getLocationOnScreen(); Point mep = me.getLocationOnScreen(); if (mep.getX() < origin.getX() || mep.getY() < origin.getY() || mep.getX() > origin.getX() + currentDialog.getWidth() || mep.getY() > origin.getY() + currentDialog.getHeight()) { hideTip(); } } } }, AWTEvent.MOUSE_MOTION_EVENT_MASK | AWTEvent.MOUSE_EVENT_MASK); } private void parentIsActive(MouseEvent e) { lastMousePosition = e.getPoint(); switch (state) { case IDLE: if (!mouseOnParentIsDown ) { showTipTimer.restart(); } break; case SHOWING_TIP: Object id = ToolTipWindow.this.tipProvider.getIdUnder(e.getPoint()); if (id == currentId) { return; } else { double dx = e.getX() - mousePositionAtPopup.getX(); double dy = e.getY() - mousePositionAtPopup.getY(); double dist = dx*dx + dy*dy; if (dist > 100) { hideTip(); } } break; case DISPOSED: state = State.IDLE; break; } } private void makeDialog(boolean undecorated, Point point) { currentDialog = new JDialog(owner != null ? owner : RapidMinerGUI.getMainFrame()); if (undecorated) { //currentDialog.setModal(true); currentDialog.setUndecorated(true); f3Label.setVisible(true); currentDialog.getRootPane().getInputMap(JComponent.WHEN_IN_FOCUSED_WINDOW).put(KeyStroke.getKeyStroke(KeyEvent.VK_F3, 0), "focusTip"); currentDialog.getRootPane().getActionMap().put("focusTip", FOCUS_TIP_ACTION); } else { f3Label.setVisible(false); } currentDialog.getRootPane().setBorder(BorderFactory.createLineBorder(Color.BLACK)); currentDialog.getContentPane().setLayout(new BorderLayout()); currentDialog.getContentPane().add(mainPanel); SwingTools.setDialogIcon(currentDialog); // dispose focused if focus lost if (state == State.IN_FOCUS) { currentDialog.addWindowFocusListener(new WindowFocusListener() { @Override public void windowGainedFocus(WindowEvent e) { } @Override public void windowLostFocus(WindowEvent e) { hideTip(); } }); } currentDialog.addWindowListener(new WindowAdapter() { @Override public void windowOpened(WindowEvent e) { if (state == State.SHOWING_TIP) { tipScrollPane.getVerticalScrollBar().setValue(0); } } @Override public void windowClosed(WindowEvent e) { state = State.IDLE; } }); autoAdjustDialogSize(undecorated); currentDialog.pack(); if (undecorated) { currentDialog.setLocation(new Point((int)(parent.getLocationOnScreen().getX() + point.getX()), (int)(parent.getLocationOnScreen().getY() + point.getY()))); } else { Rectangle innerBounds = currentDialog.getComponent(0).getBounds(); currentDialog.setLocation(new Point((int)(parent.getLocationOnScreen().getX() + point.getX() - innerBounds.x), (int)(parent.getLocationOnScreen().getY() + point.getY() - innerBounds.y))); int dx = currentDialog.getSize().width - tipScrollPane.getSize().width; int dy = currentDialog.getSize().height - tipScrollPane.getSize().height; currentDialog.setPreferredSize(new Dimension(tipScrollPane.getPreferredSize().width + dx, tipScrollPane.getPreferredSize().height + dy)); } Dimension screenSize = Toolkit.getDefaultToolkit().getScreenSize(); Rectangle bounds = currentDialog.getBounds(); if (bounds.getMaxX() > screenSize.getWidth()) { currentDialog.setLocation(new Point((int)(screenSize.getWidth() - bounds.getWidth()), (int)(bounds.getY()))); bounds = currentDialog.getBounds(); } if (bounds.getMaxY() > screenSize.getHeight()) { currentDialog.setLocation(new Point((int)(bounds.getX()), (int)(screenSize.getHeight() - bounds.getHeight()))); } currentDialog.getRootPane().getInputMap(JComponent.WHEN_IN_FOCUSED_WINDOW).put(KeyStroke.getKeyStroke(KeyEvent.VK_ESCAPE, 0, false), "CLOSE"); currentDialog.getRootPane().getActionMap().put("CLOSE", new AbstractAction() { private static final long serialVersionUID = 1373293026453738733L; @Override public void actionPerformed(ActionEvent e) { currentDialog.dispose(); } }); currentDialog.setVisible(true); } private void autoAdjustDialogSize(boolean undecorated) { int tipPaneHeight = (int)tipPane.getPreferredSize().getHeight(); int tipPaneWidth = (int)tipPane.getPreferredSize().getWidth(); if (tipPaneHeight > 300) { tipScrollPane.setPreferredSize(new Dimension(tipPaneWidth + 50, 300 + (undecorated ? 0 : f3Label.getHeight()))); } else { tipScrollPane.setPreferredSize(new Dimension(tipPaneWidth + 50, tipPaneHeight + 30 + (undecorated ? 0 : f3Label.getHeight()))); } } private void showTip() { if (state == State.IDLE) { // if parent does not have focus we don't want to show tool tip! if (!parent.hasFocus()) { return; } // check if we are really under the mouse. Necessary since mouse exited events // might have been lost when dragging. if (!parent.isDisplayable()) { return; } Rectangle parentBounds = new Rectangle(parent.getLocationOnScreen(), new Dimension(parent.getWidth(), parent.getHeight())); if (!parentBounds.contains(MouseInfo.getPointerInfo().getLocation())) { return; } if (lastMousePosition == null) { return; } currentId = tipProvider.getIdUnder(lastMousePosition); if (currentId == null) { return; } refreshDialogContents(currentId); state = State.SHOWING_TIP; showTipTimer.stop(); //hideTipTimer.start(); lastPoint = new Point((int)lastMousePosition.getX()-50, (int)lastMousePosition.getY()+10);; mousePositionAtPopup = lastMousePosition; makeDialog(true, lastPoint); parent.requestFocus(); } else { state = State.IDLE; } } private void focusTip() { if (state == State.SHOWING_TIP) { state = State.IN_FOCUS; //hideTipTimer.stop(); currentDialog.dispose(); currentDialog = null; makeDialog(false, lastPoint); } } private void hideTip() { if (currentDialog != null) { currentDialog.dispose(); state = State.DISPOSED; } } private void refreshDialogContents(Object objectId) { String tipText = tipProvider.getTip(objectId); if ((tipText == null) || (tipText.length() == 0)) { return; } if (customComponent != null) { mainPanel.remove(customComponent); } mainPanel.remove(tipScrollPane); mainPanel.remove(tipPane); tipPane.setText("<html><body><div style=\"width:300px\">"+tipText+"</div></body></html>"); if (customComponent != null) { mainPanel.remove(customComponent); } mainPanel.remove(tipPane); mainPanel.remove(tipScrollPane); customComponent = tipProvider.getCustomComponent(objectId); if (customComponent != null) { mainPanel.add(tipPane, BorderLayout.NORTH); mainPanel.add(customComponent, BorderLayout.CENTER); } else { tipScrollPane.setViewportView(tipPane); mainPanel.add(tipScrollPane, BorderLayout.CENTER); } } }