package ij.text;

import java.awt.*;
import java.io.*;
import java.awt.event.*;
import java.util.*;
import java.awt.datatransfer.*;
import ij.*;
import ij.plugin.filter.Analyzer;
import ij.io.SaveDialog;
import ij.measure.*;
import ij.util.Tools;
import ij.plugin.frame.Recorder;
import ij.gui.*;
import ij.macro.Interpreter;


/**
This is an unlimited size text panel with tab-delimited,
labeled and resizable columns. It is based on the hGrid
class at
    http://www.lynx.ch/contacts/~/thomasm/Grid/index.html.
*/
public class TextPanel extends Panel implements AdjustmentListener,
	MouseListener, MouseMotionListener, KeyListener,  ClipboardOwner,
	ActionListener, MouseWheelListener, Runnable {

	static final int DOUBLE_CLICK_THRESHOLD = 650;
	// height / width
	int iGridWidth,iGridHeight;
	int iX,iY;
	// data
	String[] sColHead;
	Vector vData;
	int[] iColWidth;
	int iColCount,iRowCount;
	int iRowHeight,iFirstRow;
	// scrolling
	Scrollbar sbHoriz,sbVert;
	int iSbWidth,iSbHeight;
	boolean bDrag;
	int iXDrag,iColDrag;
  
	boolean headings = true;
	String title = "";
	String labels;
	KeyListener keyListener;
	Cursor resizeCursor = new Cursor(Cursor.E_RESIZE_CURSOR);
  	Cursor defaultCursor = new Cursor(Cursor.DEFAULT_CURSOR);
	int selStart=-1, selEnd=-1,selOrigin=-1, selLine=-1;
	TextCanvas tc;
	PopupMenu pm;
	boolean columnsManuallyAdjusted;
	long mouseDownTime;
    String filePath;
    ResultsTable rt;
    boolean unsavedLines;
    String searchString;
    Menu fileMenu, editMenu;
    boolean menusExtended;
    boolean saveAsCSV;

  
	/** Constructs a new TextPanel. */
	public TextPanel() {
		tc = new TextCanvas(this);
		setLayout(new BorderLayout());
		add("Center",tc);
		sbHoriz=new Scrollbar(Scrollbar.HORIZONTAL);
		sbHoriz.addAdjustmentListener(this);
		sbHoriz.setFocusable(false); // prevents scroll bar from blinking on Windows
		add("South", sbHoriz);
		sbVert=new Scrollbar(Scrollbar.VERTICAL);
		sbVert.addAdjustmentListener(this);
		sbVert.setFocusable(false);
		ImageJ ij = IJ.getInstance();
		if (ij!=null) {
			sbHoriz.addKeyListener(ij);
			sbVert.addKeyListener(ij);
		}
		add("East", sbVert);
		addPopupMenu();
	}
  
	/** Constructs a new TextPanel. */
	public TextPanel(String title) {
		this();
		this.title = title;
		if (title.equals("Results")) {
			pm.addSeparator();
			addPopupItem("Clear Results");
			addPopupItem("Summarize");
			addPopupItem("Distribution...");
			addPopupItem("Set Measurements...");
		}
	}

	void addPopupMenu() {
		pm=new PopupMenu();
		addPopupItem("Save As...");
		pm.addSeparator();
		addPopupItem("Cut");
		addPopupItem("Copy");
		addPopupItem("Clear");
		addPopupItem("Select All");
		add(pm);
	}
	
	void addPopupItem(String s) {
		MenuItem mi=new MenuItem(s);
		mi.addActionListener(this);
		pm.add(mi);
	}

	/**
	Clears this TextPanel and sets the column headings to
	those in the tab-delimited 'headings' String. Set 'headings'
	to "" to use a single column with no headings.
	*/
	public synchronized void setColumnHeadings(String labels) {
		boolean sameLabels = labels.equals(this.labels);
		this.labels = labels;
		if (labels.equals("")) {
			iColCount = 1;
			sColHead=new String[1];
			sColHead[0] = "";
		} else {
			if (labels.endsWith("\t"))
				this.labels = labels.substring(0, labels.length()-1);
			sColHead = Tools.split(this.labels, "\t");
        	iColCount = sColHead.length;
		}
		flush();
		vData=new Vector();
		if (!(iColWidth!=null && iColWidth.length==iColCount && sameLabels && iColCount!=1)) {
			iColWidth=new int[iColCount];
			columnsManuallyAdjusted = false;
		}
		iRowCount=0;
		resetSelection();
		adjustHScroll();
		tc.repaint();
	}
  
	/** Returns the column headings as a tab-delimited string. */
	public String getColumnHeadings() {
		return labels==null?"":labels;
	}
	
	public synchronized void updateColumnHeadings(String labels) {
		this.labels = labels;
		if (labels.equals("")) {
			iColCount = 1;
			sColHead=new String[1];
			sColHead[0] = "";
		} else {
			if (labels.endsWith("\t"))
				this.labels = labels.substring(0, labels.length()-1);
			sColHead = Tools.split(this.labels, "\t");
        	iColCount = sColHead.length;
			iColWidth=new int[iColCount];
			columnsManuallyAdjusted = false;
		}
	}

	public void setFont(Font font, boolean antialiased) {
		tc.fFont = font;
		tc.iImage = null;
		tc.fMetrics = null;
		tc.antialiased = antialiased;
		iColWidth[0] = 0;
		if (isShowing()) updateDisplay();
	}
  
	/** Adds a single line to the end of this TextPanel. */
	public void appendLine(String text) {
		if (vData==null)
			setColumnHeadings("");
		char[] chars = text.toCharArray();
		vData.addElement(chars);
		iRowCount++;
		if (isShowing()) {
			if (iColCount==1 && tc.fMetrics!=null) {
				iColWidth[0] = Math.max(iColWidth[0], tc.fMetrics.charsWidth(chars,0,chars.length));
				adjustHScroll();
			}
			updateDisplay();
			unsavedLines = true;
		}
	}
	
	/** Adds one or more lines to the end of this TextPanel. */
	public void append(String text) {
		if (text==null) text="null";
		if (vData==null)
			setColumnHeadings("");
		if (text.length()==1 && text.equals("\n"))
			text = "";
		String[] lines = text.split("\n");
		for (int i=0; i1 && iRowCount<=10 && !columnsManuallyAdjusted)
			iColWidth[0] = 0; // forces column width calculation
		tc.repaint();
	}

	String getCell(int column, int row) {
		if (column<0||column>=iColCount||row<0||row>=iRowCount)
			return null;
		return new String(tc.getChars(column, row));
	}

	synchronized void adjustVScroll() {
		if(iRowHeight==0) return;
		Dimension d = tc.getSize();
		int value = iY/iRowHeight;
		int visible = d.height/iRowHeight;
		int maximum = iRowCount+1;
		if (visible<0) visible=0;
		if (visible>maximum) visible=maximum;
		if (value>(maximum-visible)) value=maximum-visible;
		sbVert.setValues(value,visible,0,maximum);
		iY=iRowHeight*value;
	}

	synchronized void adjustHScroll() {
		if (iRowHeight==0) return;
		Dimension d = tc.getSize();
		int w=0;
		for (int i=0; i-1 && !s.endsWith(": "))
				s = s.substring(index+2); // remove sequence number added by ListFilesRecursively
			if (s.indexOf(File.separator)!=-1 ||  s.indexOf(".")!=-1) {
				filePath = s;
				Thread thread = new Thread(this, "Open");
				thread.setPriority(thread.getPriority()-1);
				thread.start();
			}
		}
	}
	
	private void handleDoubleClickInOverlayList(String s) {
		ImagePlus imp = WindowManager.getCurrentImage();
		if (imp==null)
			return;
		Overlay overlay = imp.getOverlay();
		if (overlay==null)
			return;
		String[] columns = s.split("\t");
		int index = (int)Tools.parseDouble(columns[0]);
		Roi roi = overlay.get(index);
		imp.setRoi(roi);
	}
	
    /** For better performance, open double-clicked files on 
    	separate thread instead of on event dispatch thread. */
    public void run() {
        if (filePath!=null) IJ.open(filePath);
    }

	public void mouseExited (MouseEvent e) {
		if(bDrag) {
			setCursor(defaultCursor);
			bDrag=false;
		}
	}
	
	public void mouseMoved (MouseEvent e) {
		int x=e.getX(), y=e.getY();
		if(y<=iRowHeight) {
			int xb=x;
			x=x+iX-iGridWidth;
			int i=iColCount-1;
			for(;i>=0;i--) {
				if(x>-7 && x<7) break;
				x+=iColWidth[i];        
			}
			if(i>=0) {
				if(!bDrag) {
					setCursor(resizeCursor);
					bDrag=true;
					iXDrag=xb-iColWidth[i];
					iColDrag=i;
				}
				return;
			}
		}
		if(bDrag) {
			setCursor(defaultCursor);
			bDrag=false;
		}
	}
	
	public void mouseDragged (MouseEvent e) {
		if (e.isPopupTrigger() || e.isMetaDown())
			return;
		int x=e.getX(), y=e.getY();
		if(bDrag && x=0 && selEnd0 && tw.mb.getMenu(mbSize-1).getLabel().equals("Results"))
				tw.mb.remove(mbSize-1);
			title = title2;
			rt.show(title);
		}
		Menus.updateWindowMenuItem(title1, title2);
		if (Recorder.record) {
			if (Recorder.scriptMode())
				Recorder.recordString("IJ.renameResults(\""+title1+"\", \""+title2+"\");\n");
			else
				Recorder.record("Table.rename", title1, title2);
		}
	}

	void duplicate() {
		if (rt==null) return;
		ResultsTable rt2 = (ResultsTable)rt.clone();
		String title2 = IJ.getString("Title:", getNewTitle(title));
		if (!title2.equals("")) {
			if (title2.equals("Results")) title2 = "Results2";
			rt2.show(title2);
		}
	}
	
	private String getNewTitle(String oldTitle) {
		if (oldTitle==null)
			return "Table2";
		String title2 = oldTitle;
		if (title2.endsWith("-1") || title2.endsWith("-2"))
			title2 = title2.substring(0,title.length()-2);
		String title3 = title2+"-1";
		if (title3.equals(oldTitle))
			title3 = title2+"-2";
        return title3;
	}
	
	void select(int x,int y) {
		Dimension d = tc.getSize();
		if(iRowHeight==0 || x>d.width || y>d.height)
			return;
     	int r=(y/iRowHeight)-1+iFirstRow;
     	int lineWidth = iGridWidth;
		if (iColCount==1 && tc.fMetrics!=null && r>=0 && r=0 && r=iRowCount)
				selOrigin = iRowCount-1;
		}
		tc.repaint();
		selLine=r;
		Interpreter interp = Interpreter.getInstance();
		if (interp!=null && title.equals("Debug"))
			interp.showArrayInspector(r);
	}

	void extendSelection(int x,int y) {
		Dimension d = tc.getSize();
		if(iRowHeight==0 || x>d.width || y>d.height)
			return;
     	int r=(y/iRowHeight)-1+iFirstRow;
     	if(r>=0 && r tc.getSize().height)
        	return -1;
        else
        	return (y/iRowHeight)-1+iFirstRow;
    }

	/**
	Copies the current selection to the system clipboard. 
	Returns the number of characters copied.
	*/
	public int copySelection() {
		if (Recorder.record && title.equals("Results"))
			Recorder.record("String.copyResults");
		if (selStart==-1 || selEnd==-1)
			return copyAll();
		StringBuffer sb = new StringBuffer();
		if (Prefs.copyColumnHeaders && labels!=null && !labels.equals("") && selStart==0 && selEnd==iRowCount-1) {
			if (Prefs.noRowNumbers) {
				String s = labels;
				int index = s.indexOf("\t");
				if (index!=-1)
					s = s.substring(index+1, s.length());
				sb.append(s);
			} else
				sb.append(labels);
			sb.append('\n');
		}
		for (int i=selStart; i<=selEnd; i++) {
			char[] chars = (char[])(vData.elementAt(i));
			String s = new String(chars);
			if (s.endsWith("\t"))
				s = s.substring(0, s.length()-1);
			if (Prefs.noRowNumbers && labels!=null && !labels.equals("")) {
				int index = s.indexOf("\t");
				if (index!=-1)
					s = s.substring(index+1, s.length());
				sb.append(s);
			} else
				sb.append(s);
			if (iselStart) sb.append('\n');
		}
		String s = new String(sb);
		Clipboard clip = getToolkit().getSystemClipboard();
		if (clip==null) return 0;
		StringSelection cont = new StringSelection(s);
		clip.setContents(cont,this);
		if (s.length()>0) {
			IJ.showStatus((selEnd-selStart+1)+" lines copied to clipboard");
			if (this.getParent() instanceof ImageJ)
				Analyzer.setUnsavedMeasurements(false);
		}
		return s.length();
	}
	
	int copyAll() {
		selectAll();
		int count = selEnd - selStart + 1;
		if (count>0)
			copySelection();
		resetSelection();
		unsavedLines = false;
		return count;
	}
	
	void cutSelection() {
		if (selStart==-1 || selEnd==-1)
			selectAll();
		copySelection();
		clearSelection();
	}	

	/** Deletes the selected lines. */
	public void clearSelection() {
		if (selStart==-1 || selEnd==-1) {
			if (getLineCount()>0)
				IJ.error("Selection required");
			return;
		}
		if (Recorder.record) {
			if (Recorder.scriptMode())
				Recorder.recordString("IJ.deleteRows("+selStart+", "+selEnd+");\n");
			else {
				if ("Results".equals(title))
					Recorder.record("Table.deleteRows", selStart, selEnd);
				else
					Recorder.record("Table.deleteRows", selStart, selEnd, title);
			}
		}
		int first=selStart, last=selEnd, rows=iRowCount;
		if (selStart==0 && selEnd==(iRowCount-1)) {
			vData.removeAllElements();
			iRowCount = 0;
			if (rt!=null) {
				if (IJ.isResultsWindow() && IJ.getTextPanel()==this) {
					Analyzer.setUnsavedMeasurements(false);
					Analyzer.resetCounter();
				} else
					rt.reset();
			}
		} else {
			int rowCount = iRowCount;
			boolean atEnd = rowCount-selEnd<8;
			int count = selEnd-selStart+1;
			for (int i=0; i0)
			tc.repaint();
	}
	
	/** Creates a selection and insures that it is visible. */
	public void setSelection (int startLine, int endLine) {
		if (startLine>endLine) endLine = startLine;
		if (startLine<0) startLine = 0;
		if (endLine<0) endLine = 0;
		if (startLine>=iRowCount) startLine = iRowCount-1;
		if (endLine>=iRowCount) endLine = iRowCount-1;
		selOrigin = startLine;
		selStart = startLine;
		selEnd = endLine;
		int vstart = sbVert.getValue();
		int visible = sbVert.getVisibleAmount()-1;
		if (startLine=vstart+visible) {
			vstart = endLine - visible + 1;
			if (vstart<0) vstart = 0;
			sbVert.setValue(vstart);
			iY=iRowHeight*vstart;
		}
		tc.repaint();
	}
	
	

	/** Writes all the text in this TextPanel to a file. */
	public void save(PrintWriter pw) {
		resetSelection();
		if (labels!=null && !labels.equals("")) {
			String labels2 = labels;
			if (saveAsCSV)
				labels2 = labels2.replaceAll("\t",",");
			pw.println(labels2);
		}
		for (int i=0; i=2?getLine(iRowCount-2):null;
			summarized = lastLine!=null && lastLine.startsWith("Max");
		}
		String fileName = null;
		if (rt!=null && rt.size()!=0 && !summarized) {
			if (path==null || path.equals("")) {
				IJ.wait(10);
				String name = isResults?"Results":title;
				SaveDialog sd = new SaveDialog("Save Results", name, Prefs.defaultResultsExtension());
				fileName = sd.getFileName();
				if (fileName==null) return false;
				path = sd.getDirectory() + fileName;
			}
			rt.save(path);
			TextWindow tw = getTextWindow();
			if (fileName!=null && tw!=null && !"Results".equals(title)) {
				tw.setTitle(fileName);
				title = fileName;
			}
		} else {
			if (path.equals("")) {
				IJ.wait(10);
				boolean hasHeadings = !getColumnHeadings().equals("");
				String ext = isResults||hasHeadings?Prefs.defaultResultsExtension():".txt";
				SaveDialog sd = new SaveDialog("Save as Text", title, ext);
				String file = sd.getFileName();
				if (file==null)
					return false;
				path = sd.getDirectory() + file;
			}
			PrintWriter pw = null;
			try {
				FileOutputStream fos = new FileOutputStream(path);
				BufferedOutputStream bos = new BufferedOutputStream(fos);
				pw = new PrintWriter(bos);
			}
			catch (IOException e) {
				IJ.error("Save As>Text", e.getMessage());
				return true;
			}
			saveAsCSV = path.endsWith(".csv");
			save(pw);
			saveAsCSV = false;
			pw.close();
		}
		if (isResults) {
			Analyzer.setUnsavedMeasurements(false);
			if (Recorder.record && !IJ.isMacro())
				Recorder.record("saveAs", "Results", path);
		} else if (rt!=null) {
			if (Recorder.record && !IJ.isMacro())
				Recorder.record("saveAs", "Results", path);
		} else {
			if (Recorder.record && !IJ.isMacro())
				Recorder.record("saveAs", "Text", path);
		}
		IJ.showStatus("");
		return true;
	}

	/** Returns all the text as a string. */
	public synchronized String getText() {
		if (vData==null)
			return "";
		StringBuffer sb = new StringBuffer();
		if (labels!=null && !labels.equals("")) {
			sb.append(labels);
			sb.append('\n');
		}
		for (int i=0; i=iRowCount)
			throw new IllegalArgumentException("index out of range: "+index);
		return new String((char[])(vData.elementAt(index)));
	}
	
	/** Replaces the contents of the specified line, where 'index'
		must be greater than or equal to zero and less than 
		the value returned by getLineCount(). */
	public void setLine(int index, String s) {
		if (index<0 || index>=iRowCount)
			throw new IllegalArgumentException("index out of range: "+index);
		if (vData!=null) {
			vData.setElementAt(s.toCharArray(), index);	
			tc.repaint();
		}
	}

	/** Returns the index of the first selected line, or -1 
		if there is no slection. */
	public int getSelectionStart() {
		return selStart;
	}

	/** Returns the index of the last selected line, or -1 
		if there is no slection. */
	public int getSelectionEnd() {
		return selEnd;
	}
	
	/** Sets the ResultsTable associated with this TextPanel. */
	public void setResultsTable(ResultsTable rt) {
		if (IJ.debugMode) IJ.log("setResultsTable: "+rt);
		this.rt = rt;
		if (!menusExtended)
			extendMenus();
	}
	
	/** Returns the ResultsTable associated with this TextPanel, or null. */
	public ResultsTable getResultsTable() {
		if (IJ.debugMode) IJ.log("getResultsTable: "+rt);
		return rt;
	}

	/** Returns the ResultsTable associated with this TextPanel, or
		attempts to create one and returns the created table. */
	public ResultsTable getOrCreateResultsTable() {
		if ((rt==null||rt.size()==0) && iRowCount>0 && labels!=null && !labels.equals("")) {
			String tmpDir = IJ.getDir("temp");
			if (tmpDir==null) {
				if (IJ.debugMode) IJ.log("getOrCreateResultsTable: tmpDir null");
				return null;
			}
			String path = tmpDir+"temp-table.csv";
			saveAs(path);
			try {
				rt = ResultsTable.open(path);
				new File(path).delete();
			} catch (Exception e) {
				rt = null;
				if (IJ.debugMode) IJ.log("getOrCreateResultsTable: "+e);
			}
		}
		if (IJ.debugMode) IJ.log("getOrCreateResultsTable: "+rt);
		return rt;
	}
	
	private void extendMenus() {
		pm.addSeparator();
		addPopupItem("Rename...");
		addPopupItem("Duplicate...");
		addPopupItem("Apply Macro...");
		if (fileMenu!=null) {
			fileMenu.add("Rename...");
			fileMenu.add("Duplicate...");
		}
		if (editMenu!=null) {
			editMenu.addSeparator();
			editMenu.add("Apply Macro...");
		}
		menusExtended = true;
	}

	public void scrollToTop() {
		sbVert.setValue(0);
		iY = 0;
		for (int i=0;i 

Ads help maintain this website.