import java.lang.Math;

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

/** BarControl extends BarDial,
 * implements MouseListener and MouseMotionListener,
 * and listens for mouse events,
 * so that it can be reshaped in response to mouse inputs.
 *
 * Drags on a border separating segments shifts the border,
 * thus changing the relative sizes of those segments.
 * Note that this directly updates the callers array of values.
 *
 * We first find a reasonable Unit Increment for the range,
 * and detent all adjustable starts to multiples of it.
 * Called only when user adjusts the BarControl,
 * otherwise all values are left alone.
 * Note that starts [0] and starts [last] are end-points,
 * which are not adjustable,
 * and never changed.
 *
 * Other responses could be programmed,
 * for example Click to divide a segment in two,
 * when enabled by some other control.
 * We would need a new color for it.
 * @author Morris Hirsch
 * IPD Inc Newport RI
 * Nov 10 1998 */

public class BarControl extends BarDial
implements Runnable, MouseListener, MouseMotionListener {

    protected int target = -1;

/** Construct a BarControl.
 * @param orient -- direction of the bar,
 * @param colors -- colors to show for each segment,
 * @param starts -- starting value of each segment,
 * plus one to close the last segment. */

    public BarControl (int orient, Color [] colors, float [] starts) {
        super (orient, colors, starts);
        addMouseListener (this);
        addMouseMotionListener (this);
    }

/** Construct a BarControl.
 * @param colors -- colors to show for each segment,
 * @param starts -- starting value of each segment,
 * plus one to close the last segment. */

    public BarControl (Color [] colors, float [] starts) {
        this (HORIZONTAL, colors, starts);
    }

/** Construct a BarControl.
 * @param orient -- direction of the bar, */

    public BarControl (int orient) {
        this (orient, null, null);
    }

/** Default constructor */

    public BarControl () {
        this (null, null);
    }

/** Get the starting values as currently shown,
 * but note that we have access to the callers array,
 * so if it is changed by BarControl,
 * it has already changed for the caller. */

    public float [] getStarts () { return starts; }

/** Find and make note of one of the borders if close enough,
 * and move it to the mouse position,
 * and request a repaint to show it.
 * Also convert from pixels to user values,
 * and clean them up. */

    public synchronized void mousePressed (MouseEvent me) {
        int x = me.getX ();
        int y = me.getY ();

         int ival = xy_to_ivalue (x, y);
	 target = find_border_target (ival);
	 if (move_border (target, ival)) {
	     scaleStarts ();
	     detentStarts ();
	     repaint (100);
	 }
    }

/** Using the border chosen at mousePressed if any,
 * otherwise trying again if we got close enough,
 * either way,
 * if we have chosen one by now,
 * move it to the mouse position,
 * and request a repaint to show it.
 * Also convert from pixels to user values,
 * and clean them up. */

    public synchronized void mouseDragged (MouseEvent me) {
        int x = me.getX ();
        int y = me.getY ();

	 int ival = xy_to_ivalue (x, y);
	 if (-1 == target)
	     target = find_border_target (ival);
	 if (move_border (target, ival)) {
	     scaleStarts ();
	     detentStarts ();
	     repaint (100);
	 }
    }

/* If no (-1) target border just return false.
 * Otherwise find starts before and after target.
 * If the proposed position is inside limits,
 * update the target border,
 * push neighbors along if needed,
 * and return true,
 * otherwise leave it alone and return false. */

    protected boolean move_border (int target, int ival) {
	if (-1 == target)
	    return false;

	if ((0 == target) || (coords.length-1 == target))
	    return false;

	if ((ival < coords [0])
	 || (ival > coords [coords.length-1]))
	    return false;

        coords [target] = ival;
	push_down (target-1, ival);
	push_up (target+1, ival);
        return true;
    }

    protected void push_down (int nn, int ival) {
	for (int ii = nn; ii > 0; ii--)
	    if (coords [ii] >= coords [ii+1])
		coords [ii] = coords [ii+1]-1;
            else
		break;
    }

    protected void push_up (int nn, int ival) {
	for (int ii = nn; ii < coords.length-1; ii++)
	    if (coords [ii] <= coords [ii-1])
		coords [ii] = coords [ii-1]+1;
            else
		break;
    }

/* First try for very close,
 * then accept further misses,
 * but finally give up.
 * Not the end points,
 * only inner borders that could be moved.
 * Ignore any null colors or invalid coordinates,
 * their space is merged with nearest valid color,
 * and their border is not visible,
 * so should not be chosen. */

    protected int find_border_target (int ival) {

	if ((ival < coords [0])
	 || (ival > coords [coords.length-1]))
	     return -1;

        for (int delta = 2;
	     delta < 30;
	     delta *= 2)
	    for (int ii = 1;
	         ii < coords.length-1;
	         ii++)
	        if ((null != colors [ii])
	          && near (ival, coords [ii], delta))
	            return ii;

	return -1;
    }

/** Are two values within some range of each other?
 * @param a -- one value,
 * @param b -- other value,
 * @param range -- range for nearness. */

    public static boolean near (int a, int b, int range) {

	if (a == b)
	    return true;

	float larger, smaller;

	if (a > b) {
	    larger = a;
	    smaller = b;
	}
	else {
	    larger = b;
	    smaller = a;
	}

        return (smaller + range > larger);
    }

/** Is a value between two others? */

    public static boolean inside (int val, int starting, int ending) {
	if (starting < ending)
	    return ((starting < val) && (val < ending));
	else
	    return ((ending < val) && (val < starting));
    }

    protected int xy_to_ivalue (int x, int y) {
        if (HORIZONTAL == orient)
	    return x;
	else
	    return y;
    }

/** No response */

    public synchronized void mouseClicked (MouseEvent me) { }

/** Forget the current target and report to Listeners */

    public synchronized void mouseReleased (MouseEvent me) {
	target = -1;
        processAction ();
    }

/* No response */

    public synchronized void mouseMoved (MouseEvent event) { }

    public synchronized void mouseEntered (MouseEvent event)
    { setActive (true); }

    public synchronized void mouseExited (MouseEvent event)
    { setActive (false); }

    private ActionListener actionListener = null;

/** Caller may add or remove an ActionListener, */

    public synchronized void addActionListener (ActionListener al) {
        actionListener = AWTEventMulticaster.add (actionListener, al);
    }

    public synchronized void removeActionListener (ActionListener al) {
        actionListener = AWTEventMulticaster.remove (actionListener, al);
    }

/** Notify any listeners,
 * We use a separate Thread to notify listeners,
 * so we (and our calling event handler) can immediately return.
 * Otherwise,
 * if the event listener happens to launch a modal dialog,
 * we don't wrap up for a long time,
 * and the event will be gone by the time we return,
 * causing the event distributor to throw a NullPointerException! */

    public void processAction () {
        if (null != actionListener)
            new Thread (this).start ();
    }

    public void run () {

        try { actionListener.actionPerformed
              (new ActionEvent
              (this,
              ActionEvent.ACTION_PERFORMED,
              toString ())); 
        } catch (Exception exc) { 
            System.out.println ("failed processAction:"+exc); 
            exc.printStackTrace (); 
        }
    }

}
/* <IMG SRC="/cgi-bin/counter">*/
