package bibliothek.help.view.text; import java.awt.Cursor; import java.awt.Point; import java.awt.Rectangle; import java.awt.Shape; import java.awt.event.MouseEvent; import javax.swing.JEditorPane; import javax.swing.JTextPane; import javax.swing.SwingUtilities; import javax.swing.event.HyperlinkEvent; import javax.swing.event.HyperlinkListener; import javax.swing.event.MouseInputAdapter; import javax.swing.event.HyperlinkEvent.EventType; import javax.swing.plaf.TextUI; import javax.swing.text.BadLocationException; import javax.swing.text.Document; import javax.swing.text.Element; import javax.swing.text.Position; /** * A {@link Linker} observes a {@link JTextPane} and ensures that the * <code>JTextPane</code> fires an {@link HyperlinkEvent} whenever * the user interacts with an element that {@link #isLink(Element) is a link}.<br> * This <code>Linker</code> also changes the appearance of the mouse cursor to * indicate which parts of the text are links. * @author Benjamin Sigg */ public abstract class Linker { /** the observed textpane */ private JTextPane pane; /** the element that is currently under the mouse */ private Element current; /** * Creates a new linker. * @param pane the textpane that will be observed. */ public Linker( JTextPane pane ){ this.pane = pane; Handler handler = new Handler(); pane.addMouseListener( handler ); pane.addMouseMotionListener( handler ); pane.addHyperlinkListener( handler ); } /** * Searches an element that contains <code>point</code>. * @param point a location on the textpane * @return the element or <code>null</code> if there is nothing * at <code>point</code>. */ protected Element elementAt( Point point ){ if( !pane.getVisibleRect().contains( point ) ) return null; Document document = pane.getDocument(); int offset = pane.viewToModel( point ); Element element = getElement( offset, document ); if( elementContainsLocation( pane, element, point )) return element; else return null; } /** * Gets the leaf which contains <code>offset</code>. * @param offset an number of characters * @param document the document in which to search * @return the leaf containing the <code>offset</code>'th character */ protected Element getElement( int offset, Document document ){ Element element = document.getDefaultRootElement(); while( !element.isLeaf() ){ int index = element.getElementIndex( offset ); element = element.getElement( index ); } return element; } /** * Checks whether the point <code>location</code> lies inside * <code>element</code> when <code>element</code> is displayed by * <code>editor</code>. * @param editor an editor that shows some document * @param element an element of the document shown by <code>editor</code> * @param location a point measured relatively to <code>editor</code> * @return <code>true</code> if <code>location</code> is contained by <code>element</code> */ protected boolean elementContainsLocation( JEditorPane editor, Element element, Point location ){ try { TextUI ui = editor.getUI(); Shape begin = ui.modelToView( editor, element.getStartOffset(), Position.Bias.Forward ); if ( begin == null) { return false; } Rectangle bounds = (begin instanceof Rectangle) ? (Rectangle)begin : begin.getBounds(); Rectangle end = ui.modelToView( editor, element.getEndOffset(), Position.Bias.Backward); if (end != null) { bounds.add( end ); } return bounds.contains( location.x, location.y ); } catch (BadLocationException ble) { return false; } } /** * Tells whether <code>element</code> is a link or not. A link * will have an effect to the mouse: the mouse cursor is changed * to the "hand"-cursor. It's also possible that the user * clicks onto a link, then a {@link HyperlinkEvent} will be fired * by the {@link JTextPane} that was given to this <code>Linker</code> * through the constructor. * @param element a visible element like a text or an image * @return <code>true</code> if clicking onto the element will have an effect */ protected abstract boolean isLink( Element element ); /** * The currently selected element. * @return the element or <code>null</code> */ public Element getCurrent() { return current; } /** * Sets the currently selected element. * @param current the element */ public void setCurrent( Element current ) { if( this.current != current ){ if( this.current != null ){ pane.fireHyperlinkUpdate( new HyperlinkEvent( pane, EventType.EXITED, null, null, this.current ) ); } this.current = current; if( this.current != null ){ pane.fireHyperlinkUpdate( new HyperlinkEvent( pane, EventType.ENTERED, null, null, this.current )); } } } /** * A listener added to a {@link JTextPane}, this listener changes the * mouse cursor and fires {@link HyperlinkEvent}s when entering, clicking * or exiting a {@link Linker#isLink(Element) link}. * @author Benjamin Sigg */ private class Handler extends MouseInputAdapter implements HyperlinkListener{ @Override public void mouseReleased( MouseEvent e ) { if( SwingUtilities.isLeftMouseButton( e )){ Element next = elementAt( e.getPoint() ); if( next != null && next == current ) pane.fireHyperlinkUpdate( new HyperlinkEvent( pane, EventType.ACTIVATED, null, null, next )); else if( next != null && isLink( next )) setCurrent( next ); else setCurrent( null ); } } @Override public void mousePressed( MouseEvent e ) { if( SwingUtilities.isLeftMouseButton( e )){ Element next = elementAt( e.getPoint() ); if( next != null && isLink( next )) setCurrent( next ); else setCurrent( null ); } } @Override public void mouseMoved( MouseEvent e ) { Element next = elementAt( e.getPoint() ); if( next != null && isLink( next )) setCurrent( next ); else setCurrent( null ); } public void hyperlinkUpdate( HyperlinkEvent e ) { if( e.getEventType() == EventType.ENTERED ) pane.setCursor( Cursor.getPredefinedCursor( Cursor.HAND_CURSOR )); else pane.setCursor( null ); } } }