import ij.*; 
import ij.gui.*; 
import ij.plugin.frame.*; 

import java.awt.*;
import java.awt.event.*;
import java.util.*; 
import java.lang.reflect.*; 


/** This class "synchronizes" mouse input in multiple windows. Once
    several windows are synchronized, mouse events in any one of the
    synchronized windows are propagated to the others.

    Note, the notion of synchronization use by the SyncWindows class
    here (i.e. multiple windows that all get the same mouse input) is
    somewhat different than the use of the synchronize keyword in the
    Java language. (In Java, synchronize has to do w/ critical section
    access by multiple threads.)


@author Patrick Kelly <phkelly@ucsd.edu>

*/

public class SyncWindows_ extends PlugInFrame implements 
ActionListener, MouseMotionListener, MouseListener {

    /** Indecies of synchronized image windows are maintained in this
        Vector. */
    private Vector vwins = null;

    // Manage mouse information.
    private int oldX, oldY; 
    private int x=0;
    private int y=0; 

    /** List of currently displayed windows retrieved from ImageJ
       window manager. */
    private java.awt.List wList; 

    // Buttons for user control.
    private Button bDone, bSync, bSyncAll, bUnsync, bUnsyncAll; 

    /** Hashtable to map title strings to image window ids. */
    private Hashtable htWindows; 

    // Control size of cursor box and clipping region. These could be
    // changed to tune performance.
    static final int RSZ = 20; 
    static final int SZ = RSZ/2;
    static final int SCALE = 3;


    //--------------------------------------------------
    /** Create window sync frame. Frame is shown via call to show() or
	by invoking run method.  */
    public SyncWindows_() {
	super("Synchronize Windows"); 

	buildWindowsHash(); 
	Panel p = controlPanel(); 
	add(p); 
	pack(); 
	setResizable(false); 
	//show(); 
    }

    // --------------------------------------------------
    /** Show the frame, making it accessible to users. */
    public void run(String args) {
	show(); 
    }

    // --------------------------------------------------
    // 
    // MouseMotionListener interface methods.
    //

    // --------------------------------------------------
    /** Draws the "synchronize" cursor in each of the synchronized
     windows.  */
    public void mouseMoved(MouseEvent e) {
	oldX = x; oldY = y; 
	x = e.getX();
	y = e.getY(); 

	Rectangle rect = boundingRect(x,y,oldX,oldY); 

	// Draw new cursor box in each synchronized window.
	for(int n=0; n<vwins.size();++n) {
	    Integer iw = (Integer)vwins.elementAt(n); 
	    
	    ImageWindow iwin = 
		WindowManager.getImage(iw.intValue()).getWindow(); 
	    drawSyncCursor(iwin,rect,x,y); 
	}
    }


    // --------------------------------------------------
    //
    // MouseListener interface
    //
    
    // --------------------------------------------------
    /** Propagate mouse clicked events to all synchronized windows. */
    public void mouseClicked(MouseEvent e) {
	// Current window already received mouse event.
	ImageWindow iwc = WindowManager.getCurrentWindow(); 

	for(int n=0; n<vwins.size();++n) {
	    Integer I = (Integer)vwins.elementAt(n); 
	    ImageWindow iw = WindowManager.getImage(I.intValue()).getWindow(); 

	    if(iw != iwc)
		iw.getCanvas().mouseClicked(e); 
	}
    }

    // --------------------------------------------------
    /** Propagate mouse entered events to all synchronized windows. */
    public void mouseEntered(MouseEvent e) {
	// Current window already received mouse event.
	ImageWindow iwc = WindowManager.getCurrentWindow(); 

	for(int n=0; n<vwins.size();++n) {
	    Integer I = (Integer)vwins.elementAt(n); 
	    ImageWindow iw = WindowManager.getImage(I.intValue()).getWindow(); 

	    if(iw != iwc)
		iw.getCanvas().mouseEntered(e); 
	}
    }


    // --------------------------------------------------
    /** Propagate mouse dragged events to all synchronized windows.  */
    public void mouseDragged(MouseEvent e) {
	// Current window already received mouse event.
	ImageWindow iwc = WindowManager.getCurrentWindow(); 

	for(int n=0; n<vwins.size();++n) {
	    Integer I = (Integer)vwins.elementAt(n); 
	    ImageWindow iw = WindowManager.getImage(I.intValue()).getWindow(); 

	    if(iw != iwc)
		iw.getCanvas().mouseDragged(e); 
	}
    }

    // --------------------------------------------------
    /** Propagate mouse exited events to all synchronized windows. */
    public void mouseExited(MouseEvent e) {
	// Current window already received mouse event.
	ImageWindow iwc = WindowManager.getCurrentWindow(); 

	for(int n=0; n<vwins.size();++n) {
	    Integer I = (Integer)vwins.elementAt(n); 
	    ImageWindow iw = WindowManager.getImage(I.intValue()).getWindow(); 

	    // Repaint to get rid of cursor.
	    iw.getCanvas().paint(iw.getCanvas().getGraphics()); 

	    if(iw != iwc)
		iw.getCanvas().mouseExited(e); 
	}
    }


    // --------------------------------------------------
    /** Propagate mouse pressed events to all synchronized windows. */
    public void mousePressed(MouseEvent e) {
	// Current window already received mouse event.
	ImageWindow iwc = WindowManager.getCurrentWindow(); 

	for(int n=0; n<vwins.size();++n) {
	    Integer I = (Integer)vwins.elementAt(n); 
	    ImageWindow iw = WindowManager.getImage(I.intValue()).getWindow(); 

	    // Repaint to get rid of sync indicator.
	    iw.getCanvas().paint(iw.getCanvas().getGraphics()); 

	    if(iw != iwc)
		iw.getCanvas().mousePressed(e); 
	}
    }

    // --------------------------------------------------
    /** Propagate mouse released events to all synchronized
        windows. */
    public void mouseReleased(MouseEvent e) {
	// Current window already received mouse event.
	ImageWindow iwc = WindowManager.getCurrentWindow(); 

	Rectangle rect = boundingRect(e.getX(),e.getY(),
				      e.getX(),e.getY()); 

	for(int n=0; n<vwins.size();++n) {
	    Integer I = (Integer)vwins.elementAt(n); 
	    ImageWindow iw = WindowManager.getImage(I.intValue()).getWindow(); 

	    // Redraw to make sure sync cursor is drawn.
	    drawSyncCursor(iw,rect,e.getX(),e.getY()); 

	    if(iw != iwc)
		iw.getCanvas().mouseReleased(e); 
	}

    }


    // --------------------------------------------------
    /** Implementation of ActionListener interface. */
    public void actionPerformed(ActionEvent e) {
	// Determine which button was pressed.
	Button bpressed = (Button)e.getSource(); 
	if(bpressed==bDone) {
	    // All do so finish up.
	    setVisible(false); 
	    dispose(); 
	    removeAllWindows(); 
	}
	else if(bpressed==bSync) {
	    addSelections(); 
	}
	else if(bpressed==bSyncAll) {
	    // Selected all items on list.
	    for(int i=0; i<wList.getItemCount();++i)
		wList.select(i); 
	    addSelections(); 
	}
	else if(bpressed==bUnsync) {
	    removeSelected(); 
	}
	else if(bpressed==bUnsyncAll) {
	    removeAllWindows(); 
	}
    }

    // --------------------------------------------------
    /** Override parent class handler to clean up synchronized
	resources on exit. */
    public void processWindowEvent(WindowEvent e) {
	super.processWindowEvent(e); 
	if(e.getID()==WindowEvent.WINDOW_CLOSING) {
	    // Unsynchronize all windows.
	    IJ.log("\nRemoving all windows.\n"); 
	    removeAllWindows(); 
	}
    }


    // --------------------------------------------------
    /** Build window list display and button controls. */
    protected Panel controlPanel() {
	Panel p = new Panel();
	p.setLayout(new BorderLayout()); 

	int[] imageIDs = WindowManager.getIDList(); 
	if(imageIDs != null) {
	    // Initialize window list.
	    wList = new java.awt.List(imageIDs.length); 
	    wList.setMultipleMode(true);  // allow multiple selections

	    for(int n=0; n<imageIDs.length;++n)
		wList.add(WindowManager.getImage(imageIDs[n]).getTitle()); 

	    p.add("Center",wList); 
	}
	else {
	    Label label = new Label("No windows to select."); 
	    p.add("Center",label); 
	    wList = null; 
	}

	Panel pp = buildControlPanel();
	p.add("South",pp); 

	return p; 
    }

    // --------------------------------------------------
    /** Builds panel containing control buttons. */
    protected Panel buildControlPanel() {
	Panel p = new Panel(new GridLayout(5,1)); 

	// Synchronize selections.
	bSync = new Button("Synchronize Selections"); 
	bSync.addActionListener(this); 
	p.add(bSync); 

	// Synchronize all windows.
	bSyncAll = new Button("Synchronize All"); 
	bSyncAll.addActionListener(this); 
	p.add(bSyncAll); 

	// Unsyncrhonize selections.
	bUnsync = new Button("Unsynchronize Selection(s)"); 
	bUnsync.addActionListener(this); 
	p.add(bUnsync); 

	// Unsynchronize all windows.
	bUnsyncAll = new Button("Unsynchronize All"); 
	bUnsyncAll.addActionListener(this); 
	p.add(bUnsyncAll); 

	// Finished processing.
	bDone = new Button(" Done "); 
	bDone.addActionListener(this); 
	p.add(bDone); 

	return p; 
    }

    // --------------------------------------------------
    /** Compute bounding rectangle given current and old cursor
    locations. This is used to determine what part of image to
    redraw. */
    protected Rectangle boundingRect(int x, int y, 
				   int xOld, int yOld) {
	int dx = Math.abs(oldX - x)/2; 
	int dy = Math.abs(oldY - y)/2; 
	
	int xOffset = dx + SCALE * SZ; 
	int yOffset = dy + SCALE * SZ; 

	int xCenter = (x + oldX)/2;
	int yCenter = (y + oldY)/2; 

	int xOrg = Math.max(xCenter - xOffset,0); 
	int yOrg = Math.max(yCenter - yOffset,0); 

	int w = 2 * xOffset; 
	int h = 2 * yOffset; 

	return new Rectangle(xOrg, yOrg, w, h); 
    }


    // --------------------------------------------------
    /** Adds "this" object as mouse listener and mouse motion listener
	to each of the windows in input array.  */
    private void addWindows(Vector v) {
	//  Handle initial case of no windows.
	if(vwins==null & v.size() > 0)
	    vwins = new Vector(); 

	// Add all windows in vector to synchronized window list.
	for(int n=0; n<v.size();++n) {
	    Integer iw = (Integer)v.elementAt(n); 

	    // Make sure input window is not already on list.
	    if(!vwins.contains(iw)) {
		ImageWindow iwin = WindowManager.getImage(iw.intValue()).getWindow(); 
		iwin.getCanvas().addMouseMotionListener(this); 
		iwin.getCanvas().addMouseListener(this); 
		
		vwins.addElement(iw); 
	    }
	}
    }

    // --------------------------------------------------
    /** Remove "this" object as mouse listener and mouse motion
	listener from each of the windows in window list.  Also,
	clears window list. */
    private void removeWindows() {
	if(vwins==null)
	    return; 

	for(int n = 0; n<vwins.size();++n) {
	    Integer im = (Integer)vwins.elementAt(n); 
	    removeWindow(im); 
	}

	// Remove all windows from window list.
	vwins.removeAllElements(); 
    }


    // --------------------------------------------------
    private void removeWindow(Integer im) {
	ImageWindow iwin = WindowManager.getImage(im.intValue()).getWindow(); 
	iwin.getCanvas().removeMouseListener(this); 
	iwin.getCanvas().removeMouseMotionListener(this); 

	// Repaint to get rid of sync indicator.
	iwin.getCanvas().paint(iwin.getCanvas().getGraphics()); 
    }


    // --------------------------------------------------
    private void removeAllWindows() {
	removeWindows();  // outer class

	// Deselect all elements on list.
	for(int n=0;n<wList.getItemCount();++n) 
	    wList.deselect(n); 
    }


    // --------------------------------------------------
    private void addSelections() {
	if(wList==null) // nothing to select
	    return; 

	String[] images = wList.getSelectedItems(); 

	Vector v = new Vector(); 
	for(int n=0; n<images.length;++n) {
	    Integer im = (Integer)htWindows.get(images[n]); 
	    v.addElement(im); 
	}

	addWindows(v);  
    }


    // --------------------------------------------------
    private void removeSelected() {
	if(vwins==null) 
	    return; 

	String[] images = wList.getSelectedItems(); 
	int[] selected = wList.getSelectedIndexes(); 

	for(int n=0; n<images.length;++n) {
	    Integer im = (Integer)htWindows.get(images[n]); 
	    removeWindow(im);

	    // Remove from window list.
	    vwins.removeElement(im); 

	    // Deselect
	    wList.deselect(selected[n]); 
	}
    }


    // --------------------------------------------------
    /** Create a hashtable so that we can retrieve window objects (via
	window id) using image titles.  */
    private void buildWindowsHash() {
	htWindows = new Hashtable(); 

	int[] images = WindowManager.getIDList(); 

	// Make sure there are images to synchronize.
	if(images==null) return; 

	for(int c=0; c<images.length;++c) {
	    ImagePlus img = WindowManager.getImage(images[c]); 
	    htWindows.put(img.getTitle(),new Integer(images[c])); 
	}
    }



    // --------------------------------------------------
    /** Draw cursor that indicates windows are synchronized. */
    private void drawSyncCursor(ImageWindow iw, Rectangle rect,
				int x, int y) {
	Graphics g = iw.getCanvas().getGraphics(); 

	try {
	    g.setClip(rect.x,rect.y,rect.width,rect.height); 
	    iw.getCanvas().paint(g); 
	    g.setColor(Color.red); 		   
	    g.drawRect(x-SZ,y-SZ,RSZ,RSZ); 
	}
	finally {
	    // free up graphics resources
	    g.dispose();   
	}
    }
}   // SyncWindows_


