Author: echatellier Date: 2010-03-15 15:08:10 +0100 (Mon, 15 Mar 2010) New Revision: 248 Log: #404 : Add focusable tooltips Added: trunk/src/main/java/org/nuiton/widget/tooltip/ trunk/src/main/java/org/nuiton/widget/tooltip/FocusableTip.java trunk/src/main/java/org/nuiton/widget/tooltip/FocusableTip.properties trunk/src/main/java/org/nuiton/widget/tooltip/FocusableTip_fr.properties trunk/src/main/java/org/nuiton/widget/tooltip/SizeGrip.java trunk/src/main/java/org/nuiton/widget/tooltip/TipUtil.java trunk/src/main/java/org/nuiton/widget/tooltip/TipWindow.java trunk/src/main/java/org/nuiton/widget/tooltip/package-info.java Added: trunk/src/main/java/org/nuiton/widget/tooltip/FocusableTip.java =================================================================== --- trunk/src/main/java/org/nuiton/widget/tooltip/FocusableTip.java (rev 0) +++ trunk/src/main/java/org/nuiton/widget/tooltip/FocusableTip.java 2010-03-15 14:08:10 UTC (rev 248) @@ -0,0 +1,335 @@ +/* + * 07/29/2009 + * + * FocusableTip.java - A focusable tool tip, like those in Eclipse. + * Copyright (C) 2009 Robert Futrell + * robert_futrell at users.sourceforge.net + * http://fifesoft.com/rsyntaxtextarea + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA. + */ +package org.nuiton.widget.tooltip; + +import java.awt.Component; +import java.awt.ComponentOrientation; +import java.awt.Dimension; +import java.awt.Point; +import java.awt.Rectangle; +import java.awt.Window; +import java.awt.event.ComponentEvent; +import java.awt.event.ComponentListener; +import java.awt.event.FocusEvent; +import java.awt.event.FocusListener; +import java.awt.event.KeyEvent; +import java.awt.event.KeyListener; +import java.awt.event.MouseEvent; +import java.net.URL; +import java.util.ResourceBundle; + +import javax.swing.JComponent; +//import javax.swing.JTextArea; +import javax.swing.SwingUtilities; +import javax.swing.ToolTipManager; +import javax.swing.event.CaretEvent; +import javax.swing.event.CaretListener; +import javax.swing.event.HyperlinkListener; +import javax.swing.event.MouseInputAdapter; + + +/** + * A focusable tool tip, similar to those found in Eclipse. The user + * can click in the tip and it becomes a "real," resizable window. + * + * @author Robert Futrell + * @version 1.0 + */ +public class FocusableTip { + + private JComponent attachedComponent; + private TipWindow tipWindow; + private URL imageBase; + private AttachedComponentListener attachedComponentListener; + private HyperlinkListener hyperlinkListener; + private String lastText; + + /** + * The screen bounds in which the mouse has to stay for the currently + * displayed tip to stay visible. + */ + private Rectangle tipVisibleBounds; + + /** + * Margin from mouse cursor at which to draw focusable tip. + */ + private static final int MARGIN = 10; + + private static final String MSG = "org.nuiton.widget.tooltip.FocusableTip"; + private static final ResourceBundle msg = ResourceBundle.getBundle(MSG); + + + public FocusableTip(JComponent attachedComponent, HyperlinkListener listener) { + setAttachedComponent(attachedComponent); + this.hyperlinkListener = listener; + attachedComponentListener = new AttachedComponentListener(); + tipVisibleBounds = new Rectangle(); + } + + + /** + * Compute the bounds in which the user can move the mouse without the + * tip window disappearing. + */ + private void computeTipVisibleBounds() { + // Compute area that the mouse can move in without hiding the + // tip window. Note that Java 1.4 can only detect mouse events + // in Java windows, not globally. + Rectangle r = tipWindow.getBounds(); + Point p = r.getLocation(); + SwingUtilities.convertPointFromScreen(p, attachedComponent); + r.setLocation(p); + tipVisibleBounds.setBounds(r.x,r.y-15, r.width,r.height+15*2); + } + + + private void createAndShowTipWindow(final MouseEvent e, final String text) { + + Window owner = SwingUtilities.getWindowAncestor(attachedComponent); + tipWindow = new TipWindow(owner, this, text); + tipWindow.setHyperlinkListener(hyperlinkListener); + + // TODO: Position tip window better (handle RTL, edges of screen, etc.). + // Wrap in an invokeLater() to work around a JEditorPane issue where it + // doesn't return its proper preferred size until after it is displayed. + // See http://forums.sun.com/thread.jspa?forumID=57&threadID=574810 + // for a discussion. + SwingUtilities.invokeLater(new Runnable() { + public void run() { + + // If a new FocusableTip is requested while another one is + // *focused* and visible, the focused tip (i.e. "tipWindow") + // will be disposed of. If this Runnable is run after the + // dispose(), tipWindow will be null. All of this is done on + // the EDT so no synchronization should be necessary. + if (tipWindow==null) { + return; + } + + tipWindow.fixSize(); + ComponentOrientation o = attachedComponent.getComponentOrientation(); + + Point p = e.getPoint(); + SwingUtilities.convertPointToScreen(p, attachedComponent); + int x = o.isLeftToRight() ? (p.x-10) : + (p.x - tipWindow.getWidth() + MARGIN); + int y = p.y + MARGIN; + + // Ensure tooltip is in the window bounds. + Dimension ss = tipWindow.getToolkit().getScreenSize(); + x = Math.max(x, 0); + if (x+tipWindow.getWidth()>=ss.width) { + x = ss.width - tipWindow.getWidth(); + } + if (y+tipWindow.getHeight()>=ss.height) { // Go above cursor + y = p.y - tipWindow.getHeight() - MARGIN; + } + + tipWindow.setLocation(x, y); + tipWindow.setVisible(true); + tipWindow.toFront(); + computeTipVisibleBounds(); // Do after tip is visible + attachedComponentListener.install(attachedComponent); + lastText = text; + + } + }); + + } + + + /** + * Returns the base URL to use when loading images in this focusable tip. + * + * @return The base URL to use. + * @see #setImageBase(URL) + */ + public URL getImageBase() { + return imageBase; + } + + + /** + * Returns localized text for the given key. + * + * @param key The key into the resource bundle. + * @return The localized text. + */ + static String getString(String key) { + return msg.getString(key); + } + + + /** + * Disposes of the focusable tip currently displayed, if any. + */ + public void possiblyDisposeOfTipWindow() { + if (tipWindow != null) { + tipWindow.dispose(); + tipWindow = null; + attachedComponentListener.uninstall(); + tipVisibleBounds.setBounds(-1, -1, 0, 0); + lastText = null; + attachedComponent.requestFocus(); + } + } + + + void removeListeners() { + //System.out.println("DEBUG: Removing text area listeners"); + attachedComponentListener.uninstall(); + } + + + /** + * Sets the base URL to use when loading images in this focusable tip. + * + * @param url The base URL to use. + * @see #getImageBase() + */ + public void setImageBase(URL url) { + imageBase = url; + } + + + private void setAttachedComponent(JComponent attachedComponent) { + this.attachedComponent = attachedComponent; + // Is okay to do multiple times. + ToolTipManager.sharedInstance().registerComponent(attachedComponent); + } + + + public void toolTipRequested(MouseEvent e, String text) { + + if (text==null || text.length()==0) { + possiblyDisposeOfTipWindow(); + lastText = text; + return; + } + + if (lastText==null || text.length()!=lastText.length() + || !text.equals(lastText)) { + possiblyDisposeOfTipWindow(); + createAndShowTipWindow(e, text); + } + + } + + + private class AttachedComponentListener extends MouseInputAdapter implements + CaretListener, ComponentListener, FocusListener, KeyListener { + + public void caretUpdate(CaretEvent e) { + Object source = e.getSource(); + if (source == attachedComponent) { + possiblyDisposeOfTipWindow(); + } + } + + public void componentHidden(ComponentEvent e) { + handleComponentEvent(e); + } + + public void componentMoved(ComponentEvent e) { + handleComponentEvent(e); + } + + public void componentResized(ComponentEvent e) { + handleComponentEvent(e); + } + + public void componentShown(ComponentEvent e) { + handleComponentEvent(e); + } + + public void focusGained(FocusEvent e) { + } + + public void focusLost(FocusEvent e) { + // Only dispose of tip if it wasn't the TipWindow that was clicked + // "c" can be null, at least on OS X, so we must check that before + // calling SwingUtilities.getWindowAncestor() to guard against an + // NPE. + Component c = e.getOppositeComponent(); + boolean tipClicked = (c instanceof TipWindow) || + (c!=null && + SwingUtilities.getWindowAncestor(c) instanceof TipWindow); + if (!tipClicked) { + possiblyDisposeOfTipWindow(); + } + } + + private void handleComponentEvent(ComponentEvent e) { + possiblyDisposeOfTipWindow(); + } + + public void install(JComponent attachedComponent) { + //attachedComponent.addCaretListener(this); + attachedComponent.addComponentListener(this); + attachedComponent.addFocusListener(this); + attachedComponent.addKeyListener(this); + attachedComponent.addMouseListener(this); + attachedComponent.addMouseMotionListener(this); + } + + public void keyPressed(KeyEvent e) { + if (e.getKeyCode()==KeyEvent.VK_ESCAPE) { + possiblyDisposeOfTipWindow(); + } + else if (e.getKeyCode()==KeyEvent.VK_F2) { + if (tipWindow!=null && !tipWindow.getFocusableWindowState()) { + tipWindow.actionPerformed(null); + e.consume(); // Don't do bookmarking stuff + } + } + } + + public void keyReleased(KeyEvent e) { + } + + public void keyTyped(KeyEvent e) { + } + + public void mouseExited(MouseEvent e) { + // possiblyDisposeOfTipWindow(); + } + + public void mouseMoved(MouseEvent e) { + if (tipVisibleBounds==null || + !tipVisibleBounds.contains(e.getPoint())) { + possiblyDisposeOfTipWindow(); + } + } + + public void uninstall() { + //attachedComponent.removeCaretListener(this); + attachedComponent.removeComponentListener(this); + attachedComponent.removeFocusListener(this); + attachedComponent.removeKeyListener(this); + attachedComponent.removeMouseListener(this); + attachedComponent.removeMouseMotionListener(this); + } + + } + +} \ No newline at end of file Property changes on: trunk/src/main/java/org/nuiton/widget/tooltip/FocusableTip.java ___________________________________________________________________ Added: svn:keywords + Author Date Id Revision HeadURL Added: trunk/src/main/java/org/nuiton/widget/tooltip/FocusableTip.properties =================================================================== --- trunk/src/main/java/org/nuiton/widget/tooltip/FocusableTip.properties (rev 0) +++ trunk/src/main/java/org/nuiton/widget/tooltip/FocusableTip.properties 2010-03-15 14:08:10 UTC (rev 248) @@ -0,0 +1 @@ +FocusHotkey=Press 'F2' for focus \ No newline at end of file Added: trunk/src/main/java/org/nuiton/widget/tooltip/FocusableTip_fr.properties =================================================================== --- trunk/src/main/java/org/nuiton/widget/tooltip/FocusableTip_fr.properties (rev 0) +++ trunk/src/main/java/org/nuiton/widget/tooltip/FocusableTip_fr.properties 2010-03-15 14:08:10 UTC (rev 248) @@ -0,0 +1 @@ +FocusHotkey=Appuyez sur 'F2' pour agrandir \ No newline at end of file Added: trunk/src/main/java/org/nuiton/widget/tooltip/SizeGrip.java =================================================================== --- trunk/src/main/java/org/nuiton/widget/tooltip/SizeGrip.java (rev 0) +++ trunk/src/main/java/org/nuiton/widget/tooltip/SizeGrip.java 2010-03-15 14:08:10 UTC (rev 248) @@ -0,0 +1,266 @@ +/* + * 12/23/2008 + * + * SizeGrip.java - A size grip component that sits at the bottom of the window, + * allowing the user to easily resize that window. + * Copyright (C) 2008 Robert Futrell + * robert_futrell at users.sourceforge.net + * http://fifesoft.com/rsyntaxtextarea + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA. + */ +package org.nuiton.widget.tooltip; + +import java.awt.Color; +import java.awt.ComponentOrientation; +import java.awt.Cursor; +import java.awt.Dimension; +import java.awt.Graphics; +import java.awt.Image; +import java.awt.Point; +import java.awt.Window; +import java.awt.event.MouseEvent; +import java.io.File; +import java.io.IOException; +import java.net.MalformedURLException; +import java.net.URL; +import javax.imageio.ImageIO; +import javax.swing.JPanel; +import javax.swing.SwingUtilities; +import javax.swing.UIManager; +import javax.swing.event.MouseInputAdapter; + + +/** + * A component that allows its parent window to be resizable, similar to the + * size grip seen on status bars. + * + * @author Robert Futrell + * @version 1.0 + */ +class SizeGrip extends JPanel { + + /** + * The size grip to use if we're on OS X. + */ + private Image osxSizeGrip; + + + public SizeGrip() { + MouseHandler adapter = new MouseHandler(); + addMouseListener(adapter); + addMouseMotionListener(adapter); + setPreferredSize(new Dimension(16, 16)); + } + + + /** + * Overridden to ensure that the cursor for this component is appropriate + * for the orientation. + * + * @param o The new orientation. + */ + public void applyComponentOrientation(ComponentOrientation o) { + possiblyFixCursor(o.isLeftToRight()); + super.applyComponentOrientation(o); + } + + + /** + * Creates and returns the OS X size grip image. + * + * @return The OS X size grip. + */ + private Image createOSXSizeGrip() { + ClassLoader cl = getClass().getClassLoader(); + URL url = cl.getResource("org/fife/ui/autocomplete/osx_sizegrip.png"); + if (url==null) { + // We're not running in a jar - we may be debugging in Eclipse, + // for example + File f = new File("../AutoComplete/src/org/fife/ui/autocomplete/osx_sizegrip.png"); + if (f.isFile()) { + try { + url = f.toURI().toURL(); + } catch (MalformedURLException mue) { // Never happens + mue.printStackTrace(); + return null; + } + } + else { + return null; // Can't find resource or image file + } + } + Image image = null; + try { + image = ImageIO.read(url); + } catch (IOException ioe) { // Never happens + ioe.printStackTrace(); + } + return image; + } + + + /** + * Paints this panel. + * + * @param g The graphics context. + */ + protected void paintComponent(Graphics g) { + + super.paintComponent(g); + + Dimension dim = getSize(); + Color c1 = UIManager.getColor("Label.disabledShadow"); + Color c2 = UIManager.getColor("Label.disabledForeground"); + + if (osxSizeGrip!=null) { + g.drawImage(osxSizeGrip, dim.width-16, dim.height-16, null); + return; + } + + ComponentOrientation orientation = getComponentOrientation(); + + if (orientation.isLeftToRight()) { + int width = dim.width -= 3; + int height = dim.height -= 3; + g.setColor(c1); + g.fillRect(width-9,height-1, 3,3); + g.fillRect(width-5,height-1, 3,3); + g.fillRect(width-1,height-1, 3,3); + g.fillRect(width-5,height-5, 3,3); + g.fillRect(width-1,height-5, 3,3); + g.fillRect(width-1,height-9, 3,3); + g.setColor(c2); + g.fillRect(width-9,height-1, 2,2); + g.fillRect(width-5,height-1, 2,2); + g.fillRect(width-1,height-1, 2,2); + g.fillRect(width-5,height-5, 2,2); + g.fillRect(width-1,height-5, 2,2); + g.fillRect(width-1,height-9, 2,2); + } + else { + int height = dim.height -= 3; + g.setColor(c1); + g.fillRect(10,height-1, 3,3); + g.fillRect(6,height-1, 3,3); + g.fillRect(2,height-1, 3,3); + g.fillRect(6,height-5, 3,3); + g.fillRect(2,height-5, 3,3); + g.fillRect(2,height-9, 3,3); + g.setColor(c2); + g.fillRect(10,height-1, 2,2); + g.fillRect(6,height-1, 2,2); + g.fillRect(2,height-1, 2,2); + g.fillRect(6,height-5, 2,2); + g.fillRect(2,height-5, 2,2); + g.fillRect(2,height-9, 2,2); + } + + } + + + /** + * Ensures that the cursor for this component is appropriate for the + * orientation. + * + * @param ltr Whether the current component orientation is LTR. + */ + protected void possiblyFixCursor(boolean ltr) { + int cursor = Cursor.NE_RESIZE_CURSOR; + if (ltr) { + cursor = Cursor.NW_RESIZE_CURSOR; + } + if (cursor!=getCursor().getType()) { + setCursor(Cursor.getPredefinedCursor(cursor)); + } + } + + + public void updateUI() { + super.updateUI(); + // TODO: Key off of Aqua LaF, not just OS X, as this size grip looks + // bad on other LaFs on Mac such as Nimbus. + if (System.getProperty("os.name").indexOf("OS X")>-1) { + if (osxSizeGrip==null) { + osxSizeGrip = createOSXSizeGrip(); + } + } + else { // Clear memory in case of runtime LaF change. + osxSizeGrip = null; + } + + } + + + /** + * Listens for mouse events on this panel and resizes the parent window + * appropriately. + * + * @author Robert Futrell + * @version 1.0 + */ + /* + * NOTE: We use SwingUtilities.convertPointToScreen() instead of just using + * the locations relative to the corner component because the latter proved + * buggy - stretch the window too wide and some kind of arithmetic error + * started happening somewhere - our window would grow way too large. + */ + private class MouseHandler extends MouseInputAdapter { + + private Point origPos; + + public void mouseDragged(MouseEvent e) { + Point newPos = e.getPoint(); + SwingUtilities.convertPointToScreen(newPos, SizeGrip.this); + int xDelta = newPos.x - origPos.x; + int yDelta = newPos.y - origPos.y; + Window wind = SwingUtilities.getWindowAncestor(SizeGrip.this); + if (wind!=null) { // Should always be true + if (getComponentOrientation().isLeftToRight()) { + int w = wind.getWidth(); + if (newPos.x>=wind.getX()) { + w += xDelta; + } + int h = wind.getHeight(); + if (newPos.y>=wind.getY()) { + h += yDelta; + } + wind.setSize(w,h); + } + else { // RTL + int newW = Math.max(1, wind.getWidth()-xDelta); + int newH = Math.max(1, wind.getHeight()+yDelta); + wind.setBounds(newPos.x, wind.getY(), newW, newH); + } + // invalidate()/validate() needed pre-1.6. + wind.invalidate(); + wind.validate(); + } + origPos.setLocation(newPos); + } + + public void mousePressed(MouseEvent e) { + origPos = e.getPoint(); + SwingUtilities.convertPointToScreen(origPos, SizeGrip.this); + } + + public void mouseReleased(MouseEvent e) { + origPos = null; + } + + } + + +} \ No newline at end of file Property changes on: trunk/src/main/java/org/nuiton/widget/tooltip/SizeGrip.java ___________________________________________________________________ Added: svn:keywords + Author Date Id Revision HeadURL Added: trunk/src/main/java/org/nuiton/widget/tooltip/TipUtil.java =================================================================== --- trunk/src/main/java/org/nuiton/widget/tooltip/TipUtil.java (rev 0) +++ trunk/src/main/java/org/nuiton/widget/tooltip/TipUtil.java 2010-03-15 14:08:10 UTC (rev 248) @@ -0,0 +1,117 @@ +/* + * 08/13/2009 + * + * TipUtil.java - Utility methods for homemade tool tips. + * Copyright (C) 2009 Robert Futrell + * robert_futrell at users.sourceforge.net + * http://fifesoft.com/rsyntaxtextarea + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA. + */ +package org.nuiton.widget.tooltip; + +import java.awt.Color; +import java.awt.Font; +import java.awt.SystemColor; +import javax.swing.BorderFactory; +import javax.swing.JEditorPane; +import javax.swing.UIManager; +import javax.swing.plaf.ColorUIResource; +import javax.swing.text.html.HTMLDocument; + + +/** + * Static utility methods for focusable tips. + * + * @author Robert Futrell + * @version 1.0 + */ +class TipUtil { + + + private TipUtil() { + } + + + /** + * Returns the default background color to use for tool tip windows. + * + * @return The default background color. + */ + public static Color getToolTipBackground() { + + Color c = UIManager.getColor("ToolTip.background"); + + // Tooltip.background is wrong color on Nimbus (!) + if (c==null || UIManager.getLookAndFeel().getName().equals("Nimbus")) { + c = UIManager.getColor("info"); // Used by Nimbus (and others) + if (c==null) { + c = SystemColor.info; // System default + } + } + + // Workaround for a bug (?) with Nimbus - calling JLabel.setBackground() + // with a ColorUIResource does nothing, must be a normal Color + if (c instanceof ColorUIResource) { + c = new Color(c.getRGB()); + } + + return c; + + } + + + /** + * Tweaks a <code>JEditorPane</code> so it can be used to render the + * content in a focusable pseudo-tool tip. It is assumed that the editor + * pane is using an <code>HTMLDocument</code>. + * + * @param textArea The editor pane to tweak. + */ + public static void tweakTipEditorPane(JEditorPane textArea) { + + // Jump through a few hoops to get things looking nice in Nimbus + if (UIManager.getLookAndFeel().getName().equals("Nimbus")) { + Color selBG = textArea.getSelectionColor(); + Color selFG = textArea.getSelectedTextColor(); + textArea.setUI(new javax.swing.plaf.basic.BasicEditorPaneUI()); + textArea.setSelectedTextColor(selFG); + textArea.setSelectionColor(selBG); + } + + textArea.setEditable(false); // Required for links to work! + textArea.setBorder(BorderFactory.createEmptyBorder(5,5,5,5)); + + // Make selection visible even though we are not (initially) focusable. + textArea.getCaret().setSelectionVisible(true); + + // Make it use the "tool tip" background color. + textArea.setBackground(TipUtil.getToolTipBackground()); + + // Force JEditorPane to use a certain font even in HTML. + // All standard LookAndFeels, even Nimbus (!), define Label.font. + Font font = UIManager.getFont("Label.font"); + if (font == null) { // Try to make a sensible default + font = new Font("SansSerif", Font.PLAIN, 12); + } + HTMLDocument doc = (HTMLDocument) textArea.getDocument(); + doc.getStyleSheet().addRule( + "body { font-family: " + font.getFamily() + "; font-size: " + + font.getSize() + "pt; }"); + + } + + +} \ No newline at end of file Property changes on: trunk/src/main/java/org/nuiton/widget/tooltip/TipUtil.java ___________________________________________________________________ Added: svn:keywords + Author Date Id Revision HeadURL Added: trunk/src/main/java/org/nuiton/widget/tooltip/TipWindow.java =================================================================== --- trunk/src/main/java/org/nuiton/widget/tooltip/TipWindow.java (rev 0) +++ trunk/src/main/java/org/nuiton/widget/tooltip/TipWindow.java 2010-03-15 14:08:10 UTC (rev 248) @@ -0,0 +1,328 @@ +/* + * 07/29/2009 + * + * TipWindow.java - The actual window component representing the tool tip. + * Copyright (C) 2009 Robert Futrell + * robert_futrell at users.sourceforge.net + * http://fifesoft.com/rsyntaxtextarea + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA. + */ +package org.nuiton.widget.tooltip; + +import java.awt.BorderLayout; +import java.awt.Color; +import java.awt.Component; +import java.awt.Container; +import java.awt.Dimension; +import java.awt.Font; +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.KeyAdapter; +import java.awt.event.KeyEvent; +import java.awt.event.MouseAdapter; +import java.awt.event.MouseEvent; +import java.awt.event.WindowAdapter; +import java.awt.event.WindowEvent; +import javax.swing.BorderFactory; +import javax.swing.JEditorPane; +import javax.swing.JLabel; +import javax.swing.JPanel; +import javax.swing.JScrollPane; +import javax.swing.JSeparator; +import javax.swing.JWindow; +import javax.swing.SwingConstants; +import javax.swing.SwingUtilities; +import javax.swing.UIManager; +import javax.swing.event.HyperlinkEvent; +import javax.swing.event.HyperlinkListener; +import javax.swing.event.MouseInputAdapter; +import javax.swing.text.BadLocationException; +import javax.swing.text.html.HTMLDocument; + + +/** + * The actual tool tip component. + * + * @author Robert Futrell + * @version 1.0 + */ +class TipWindow extends JWindow implements ActionListener { + + private FocusableTip ft; + private JEditorPane textArea; + private String text; + private TipListener tipListener; + private HyperlinkListener userHyperlinkListener; + + private static TipWindow visibleInstance; + + + /** + * Constructor. + * + * @param owner The parent window. + * @param msg The text of the tool tip. This can be HTML. + */ + public TipWindow(Window owner, FocusableTip ft, String msg) { + + super(owner); + this.ft = ft; + this.text = msg; + tipListener = new TipListener(); + + JPanel cp = new JPanel(new BorderLayout()); + cp.setBorder(BorderFactory.createCompoundBorder(BorderFactory + .createLineBorder(Color.BLACK), BorderFactory + .createEmptyBorder())); + cp.setBackground(TipUtil.getToolTipBackground()); + textArea = new JEditorPane("text/html", msg); + TipUtil.tweakTipEditorPane(textArea); + if (ft.getImageBase()!=null) { // Base URL for images + ((HTMLDocument)textArea.getDocument()).setBase(ft.getImageBase()); + } + textArea.addMouseListener(tipListener); + textArea.addHyperlinkListener(new HyperlinkListener() { + public void hyperlinkUpdate(HyperlinkEvent e) { + if (e.getEventType()==HyperlinkEvent.EventType.ACTIVATED) { + TipWindow.this.ft.possiblyDisposeOfTipWindow(); + } + } + }); + cp.add(textArea); + + setFocusableWindowState(false); + setContentPane(cp); + setBottomPanel(); // Must do after setContentPane() + pack(); + + // InputMap/ActionMap combo doesn't work for JWindows (even when + // using the JWindow's JRootPane), so we'll resort to KeyListener + KeyAdapter ka = new KeyAdapter() { + public void keyPressed(KeyEvent e) { + if (e.getKeyCode()==KeyEvent.VK_ESCAPE) { + TipWindow.this.ft.possiblyDisposeOfTipWindow(); + } + } + }; + addKeyListener(ka); + textArea.addKeyListener(ka); + + // Ensure only 1 TipWindow is ever visible. If the caller does what + // they're supposed to and only creates these on the EDT, the + // synchronization isn't necessary, but we'll be extra safe. + synchronized (TipWindow.class) { + if (visibleInstance!=null) { + visibleInstance.dispose(); + } + visibleInstance = this; + } + + } + + + public void actionPerformed(ActionEvent e) { + + if (!getFocusableWindowState()) { + setFocusableWindowState(true); + setBottomPanel(); + textArea.removeMouseListener(tipListener); + pack(); + addWindowFocusListener(new WindowAdapter() { + public void windowLostFocus(WindowEvent e) { + ft.possiblyDisposeOfTipWindow(); + } + }); + ft.removeListeners(); + if (e==null) { // Didn't get here via our mouseover timer + requestFocus(); + } + } + + } + + + /** + * Disposes of this window. + */ + public void dispose() { + //System.out.println("[DEBUG]: Disposing..."); + Container cp = getContentPane(); + for (int i=0; i<cp.getComponentCount(); i++) { + // Okay if listener is already removed + cp.getComponent(i).removeMouseListener(tipListener); + } + ft.removeListeners(); + super.dispose(); + } + + + /** + * Workaround for JEditorPane not returning its proper preferred size + * when rendering HTML until after layout already done. See + * http://forums.sun.com/thread.jspa?forumID=57&threadID=574810 for a + * discussion. + */ + void fixSize() { + + Dimension d = textArea.getPreferredSize(); + Rectangle r = null; + try { + + r = textArea.modelToView(textArea.getDocument().getLength()-1); + d.height = r.y + r.height; + + // Ensure the text area doesn't start out too tall or wide. + d = textArea.getPreferredSize(); + d.width = Math.min(d.width+25, 320); + d.height = Math.min(d.height, 150); + + textArea.setPreferredSize(d); + + } catch (BadLocationException ble) { // Never happens + ble.printStackTrace(); + } + + pack(); // Must re-pack to calculate proper size. + + } + + + public String getText() { + return text; + } + + + private void setBottomPanel() { + + final JPanel panel = new JPanel(new BorderLayout()); + panel.add(new JSeparator(), BorderLayout.NORTH); + + boolean focusable = getFocusableWindowState(); + if (focusable) { + SizeGrip sg = new SizeGrip(); + sg.applyComponentOrientation(sg.getComponentOrientation()); // Workaround + panel.add(sg, BorderLayout.LINE_END); + MouseInputAdapter adapter = new MouseInputAdapter() { + private Point lastPoint; + public void mouseDragged(MouseEvent e) { + Point p = e.getPoint(); + SwingUtilities.convertPointToScreen(p, panel); + if (lastPoint==null) { + lastPoint = p; + } + else { + int dx = p.x - lastPoint.x; + int dy = p.y - lastPoint.y; + setLocation(getX()+dx, getY()+dy); + lastPoint = p; + } + } + public void mousePressed(MouseEvent e) { + lastPoint = e.getPoint(); + SwingUtilities.convertPointToScreen(lastPoint, panel); + } + }; + panel.addMouseListener(adapter); + panel.addMouseMotionListener(adapter); + // Don't add tipListener to the panel or SizeGrip + } + else { + panel.setOpaque(false); + JLabel label = new JLabel(FocusableTip.getString("FocusHotkey")); + Color fg = UIManager.getColor("Label.disabledForeground"); + Font font = textArea.getFont(); + font = font.deriveFont(font.getSize2D() - 1.0f); + label.setFont(font); + if (fg==null) { // Non BasicLookAndFeel-derived Looks + fg = Color.GRAY; + } + label.setOpaque(true); + Color bg = TipUtil.getToolTipBackground(); + label.setBackground(bg); + label.setForeground(fg); + label.setHorizontalAlignment(SwingConstants.TRAILING); + label.setBorder(BorderFactory.createEmptyBorder(0, 5, 0, 5)); + panel.add(label); + panel.addMouseListener(tipListener); + } + + // Replace the previous SOUTH Component with the new one. + Container cp = getContentPane(); + if (cp.getComponentCount()==2) { // Skip first time through + Component comp = cp.getComponent(0); + cp.remove(0); + JScrollPane sp = new JScrollPane(comp); + sp.setViewportBorder(BorderFactory.createEmptyBorder()); + sp.setBackground(textArea.getBackground()); + sp.getViewport().setBackground(textArea.getBackground()); + cp.add(sp); + // What was component 1 is now 0. + cp.getComponent(0).removeMouseListener(tipListener); + cp.remove(0); + } + + cp.add(panel, BorderLayout.SOUTH); + + } + + + /** + * Sets the listener for hyperlink events in this tip window. + * + * @param listener The new listener. The old listener (if any) is + * removed. A value of <code>null</code> means "no listener." + */ + public void setHyperlinkListener(HyperlinkListener listener) { + // We've added a separate listener, so remove only the user's. + if (userHyperlinkListener!=null) { + textArea.removeHyperlinkListener(userHyperlinkListener); + } + userHyperlinkListener = listener; + if (userHyperlinkListener!=null) { + textArea.addHyperlinkListener(userHyperlinkListener); + } + } + + + /** + * Listens for events in this window. + */ + private class TipListener extends MouseAdapter { + + public TipListener() { + } + + public void mousePressed(MouseEvent e) { + actionPerformed(null); // Manually create "real" window + } + + public void mouseExited(MouseEvent e) { + // Since we registered this listener on the child components of + // the JWindow, not the JWindow iteself, we have to be careful. + Component source = (Component)e.getSource(); + Point p = e.getPoint(); + SwingUtilities.convertPointToScreen(p, source); + if (!TipWindow.this.getBounds().contains(p)) { + ft.possiblyDisposeOfTipWindow(); + } + } + + } + +} \ No newline at end of file Property changes on: trunk/src/main/java/org/nuiton/widget/tooltip/TipWindow.java ___________________________________________________________________ Added: svn:keywords + Author Date Id Revision HeadURL Added: trunk/src/main/java/org/nuiton/widget/tooltip/package-info.java =================================================================== --- trunk/src/main/java/org/nuiton/widget/tooltip/package-info.java (rev 0) +++ trunk/src/main/java/org/nuiton/widget/tooltip/package-info.java 2010-03-15 14:08:10 UTC (rev 248) @@ -0,0 +1,9 @@ +/** + * Focusable tooltip. + * + * Original code from RSyntaxTextArea project with following modifications: + * <ul> + * <li>allow setting tooltip on all {@link javax.swing.JComponent}</li> + * </ul> + */ +package org.nuiton.widget.tooltip; Property changes on: trunk/src/main/java/org/nuiton/widget/tooltip/package-info.java ___________________________________________________________________ Added: svn:keywords + Author Date Id Revision HeadURL