import java.awt.*;
import java.awt.event.*;

/** BubbleHelper to associate popup Help with any Components.
 * @author Morris Hirsch
 * IPD Inc Newport RI 1998
 */

public class BubbleHelper {

    private Bubble bubble;

    private boolean do_help = true;

    private long MSDelay = 250;

/** BubbleHelper to associate popup Help with any Components. */

    BubbleHelper () {

/* Because class Bubble extends Window,
 * we need aFrame for the constructor,
 * but nobody said the Frame has to be showing,
 * so we just construct one for this purpose.
 * Surprise -- it works. */

	Frame aFrame = new Frame ("BubbleHelper");
	bubble = new Bubble (aFrame);
    }

/** Globally (this controller) enable or disable popup Help. */

    public void setDoHelp (boolean do_help) {
	this.do_help = do_help;
        bubble.setDoHelp (do_help);
    }

/** Set delay on mouse visit before posting help.
 * @param MSDelay -- milliseconds delay.
 * Anything under 100 becomes 100. */

    public void setMSDelay (long MSDelay) {
	if (MSDelay < 100)
	    MSDelay = 100;

        this.MSDelay = MSDelay;
    }

    public long getMSDelay () {
	return MSDelay;
    }

/** Set popup Help on Component,
 * by adding a MouseListener that will popup the Help Window.
 * @return -- the MouseListener,
 * caller may keep a reference,
 * if they later wish to remove help from the Component.
 * @param comp -- the component to have help.
 * @param help -- the help string. */

    public MouseListener setHelp (Component comp, String help) {
        MouseListener ML = new HelpListener (comp, bubble, help, this);
        try { comp.addMouseListener (ML); }
	catch (Exception exc) { exc.printStackTrace (); return null; }
	return ML;
    }

/** Set popup Help on a Mapping component,
 * with method String XYToString (int x, int y)
 * by adding a MouseListener that will popup the Help Window.
 * (Wanted the Mapping interface to extend Component but cannot.)
 * @return -- the MouseListener,
 * caller may keep a reference,
 * if they later wish to remove help from the Component.
 * @param mapping -- the mapping component to have help.
 * @param help -- the help string. */

    public HelpListener setMappingHelp (Component comp) {
        HelpListener HL = new HelpListener (comp, bubble, this);
        try {
	    comp.addMouseListener (HL);
	    comp.addMouseMotionListener (HL);
	}
	catch (Exception exc) { exc.printStackTrace (); return null; }
	return HL;
    }
}

/* Private class */

class HelpListener
implements MouseListener, MouseMotionListener
{

    BubbleHelper bh;
    Component comp;
    private Bubble  bubble;
    String  help;

    int relx = 0, rely = 0;

    BubbleThread  BThread = null;

/** Constructor.
 * @param comp -- the component getting bubble help.
 * @param bubble -- the bubble that will show help.
 * @param help -- the help string.
 * @param bh -- the help manager.
 */

    HelpListener (Component comp, Bubble bubble, String help, BubbleHelper bh) {
        this.comp = comp;
	this.bubble = bubble;
        this.help = help;
        this.bh = bh;
    }

/** Constructor.
 * @param comp -- the mapping component getting bubble help.
 * @param bubble -- the bubble that will show help.
 * @param bh -- the help manager.
 */

    HelpListener (Component comp, Bubble bubble, BubbleHelper bh) {
        this.comp = comp;
	this.bubble = bubble;
        this.bh = bh;
    }

    public synchronized void mouseEntered (MouseEvent event) {
	targetEnter ();
    }

/* When mouse Enters this target:
 * 1 -- kill any other timer in progress.
 * 2 -- start a new timer for this listener,
 * using the current delay value. */

    protected void targetEnter () {
        if (BThread != null && BThread.isAlive  ())
            BThread.stop ();

	BThread = null;

	if (!bubble.getDoHelp ())
	    return;

        BThread = new BubbleThread (this, bh.getMSDelay ());
        BThread.start ();
    }

    public synchronized void mouseExited (MouseEvent event) {
	targetExit ();
    }

/* Handle press just like exit,
 * because the user no longer needs the help. */

    public synchronized void mousePressed (MouseEvent event) {
	targetExit ();
    }

/* When mouse Exits target:
 * 1 -- dispose any help bubble that is showing.
 * 2 -- kill the timer for this listener.  */

    protected void targetExit () {
        if ((null != bubble) && bubble.isShowing  ())
            bubble.dispose ();

        if (BThread != null && BThread.isAlive  ())
            BThread.stop ();

	BThread = null;
    }

    public synchronized void mouseReleased (MouseEvent event) { }

    public synchronized void mouseClicked (MouseEvent event) { }

/* Only of interest for Mapping.
 * Treat each Map region as if a separate visited Component.
 * Consider it a new region when the String changes.
 * When mouse moves from one region to another:
 * 1 -- dispose any help bubble that is showing.
 * 2 -- kill the timer for this listener.
 * 3 -- start a new timer for this listener,
 * using the current delay value.
 * Save the mouse coordinates,
 * so we can position the bubble according to the XY within the Map,
 * rather than fixed offset from entire component.  */

    public synchronized void mouseMoved (MouseEvent event) {

        if (comp instanceof Mapping) {
	    String regionHelp = ((Mapping)comp).XYToString
		(relx = event.getX (), rely = event.getY ());

            if ((null == regionHelp)
	     || (!regionHelp.equals (this.help))) {
		 targetExit ();
		 this.help = regionHelp;
		 if (null != regionHelp)
		     targetEnter ();
	     }
	}
    }

    public synchronized void mouseDragged (MouseEvent event) { }

/* To show help,
 * position bubble for this component,
 * have it show the help. */

    synchronized void showBubbleHelp () {
	if (!bubble.getDoHelp ())
	    return;

        Point scrnLoc = comp.getLocationOnScreen ();
        Dimension size = comp.getSize ();

/* Don't let bubble cover the cursor! */

        if (comp instanceof Mapping)
            bubble.setLocation
		(scrnLoc.x + 20 + relx, scrnLoc.y + 20 + rely);

        else
            bubble.setLocation
		(scrnLoc.x, scrnLoc.y + size.height + 2);

        bubble.showBubbleHelp (help);
    }

}

/* Private class times mouse visits to delay help popup */

class BubbleThread extends Thread {
    HelpListener HL;
    long MSDelay;

    public BubbleThread (HelpListener HL, long MSDelay) {
        this.HL = HL;
        this.MSDelay = MSDelay;
    }

    public void run () {
        long  start = System.currentTimeMillis ();
        boolean done = false;

        while (!done) {
            long delta = System.currentTimeMillis () - start;
            if (delta > MSDelay) {
                try {HL.showBubbleHelp (); }
		catch (Exception exc) { ; }
                done = true;
            }
	    yield ();
        }
    }
}

/* Private class shows the help */

class Bubble extends Window {

    private String text = null;
    private boolean do_help = true;

    Bubble (Frame aFrame) {
	super (aFrame);
    }

    public void setDoHelp (boolean do_help) {
	this.do_help = do_help;
    }

    public boolean getDoHelp () {
	return do_help;
    }

    public void showBubbleHelp (String text) {
        this.text = text;

	if (!do_help)
	    return;

        pack ();
        setVisible (true);
    }

    public void update (Graphics g) {
	paint (g);
    }

    public void paint (Graphics g) {
	if (null == text)
	    return;

        if (!do_help)
            return;

        Dimension   size = getSize ();
        FontMetrics fm   = g.getFontMetrics ();

	g.setColor (Color.yellow);
	g.fillRect (0,0,size.width,size.height);

	g.setColor (Color.black);
        g.drawString (text,2,fm.getAscent ()+2);
    }

/**
 * @deprecated for JDK1.1
 */

    public Dimension preferredSize () {
	return getPreferredSize ();
    }

    public Dimension getPreferredSize () {
	if (null == text)
	    return new Dimension (0, 0);

        Graphics  g  = getGraphics ();
        FontMetrics fm = g.getFontMetrics ();
        return new Dimension (fm.stringWidth (text)+4, 
          fm.getHeight ()+4);
    }
}
/* <IMG SRC="/cgi-bin/counter">*/
