| TextPanel |

    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.ResultsTable;
    import ij.util.Tools;
    import ij.plugin.frame.Recorder;

    /**
    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;

        /** 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();
            if (title.equals("Results")) {
                pm.addSeparator();
                addPopupItem("Clear Results");
                addPopupItem("Summarize");
                addPopupItem("Distribution...");
                addPopupItem("Set Measurements...");
                addPopupItem("Duplicate...");
            }
        }

        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 {
                sColHead = Tools.split(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 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 data) {
            if (vData==null)
                setColumnHeadings("");
            char[] chars = data.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 data) {
            if (data==null) data="null";
            if (vData==null)
                setColumnHeadings("");
            while (true) {
                int p=data.indexOf('\n');
                if (p<0) {
                    appendWithoutUpdate(data);
                    break;
                }
                appendWithoutUpdate(data.substring(0,p));
                data = data.substring(p+1);
                if (data.equals(""))
                    break;
            }
            if (isShowing()) {
                updateDisplay();
                unsavedLines = true;
            }
        }

        /** Adds a single line to the end of this TextPanel without updating the display. */
        void appendWithoutUpdate(String data) {
            char[] chars = data.toCharArray();
            vData.addElement(chars);
            iRowCount++;
        }

        void updateDisplay() {
            iY=iRowHeight*(iRowCount+1);
            adjustVScroll();
            if (iColCount>1 && 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<iColCount;i++)
                w+=iColWidth[i];
            iGridWidth=w;
            sbHoriz.setValues(iX,d.width,0,iGridWidth);
            iX=sbHoriz.getValue();
        }

        public void adjustmentValueChanged (AdjustmentEvent e) {
            iX=sbHoriz.getValue();
            iY=iRowHeight*sbVert.getValue();
            tc.repaint();
        }

        public void mousePressed (MouseEvent e) {
            int x=e.getX(), y=e.getY();
            if (e.isPopupTrigger() || e.isMetaDown())
                pm.show(e.getComponent(),x,y);
            else if (e.isShiftDown())
                extendSelection(x, y);
            else {
                select(x, y);
                handleDoubleClick();
            }
        }

        void handleDoubleClick() {
            if (selStart<0 || selStart!=selEnd || iColCount!=1) return;
            boolean doubleClick = System.currentTimeMillis()-mouseDownTime<=DOUBLE_CLICK_THRESHOLD;
            mouseDownTime = System.currentTimeMillis();
            if (doubleClick) {
                char[] chars = (char[])(vData.elementAt(selStart));
                String s = new String(chars);
                int index = s.indexOf(": ");
                if (index>-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();
                }
            }
        }

        /** 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<tc.getSize().width) {
                int w=x-iXDrag;
                if(w<0) w=0;
                iColWidth[iColDrag]=w;
                columnsManuallyAdjusted = true;
                adjustHScroll();
                tc.repaint();
            } else {
                extendSelection(x, y);
            }
        }

        public void mouseReleased (MouseEvent e) {}
        public void mouseClicked (MouseEvent e) {}
        public void mouseEntered (MouseEvent e) {}

        public void mouseWheelMoved(MouseWheelEvent event) {
            synchronized(this) {
                int rot = event.getWheelRotation();
                sbVert.setValue(sbVert.getValue()+rot);
                iY=iRowHeight*sbVert.getValue();
                tc.repaint();
            }
        }

        /** Unused keyPressed and keyTyped events will be passed to 'listener'.*/
        public void addKeyListener(KeyListener listener) {
            keyListener = listener;
        }

        public void keyPressed(KeyEvent e) {
            //boolean cutCopyOK = (e.isControlDown()||e.isMetaDown())
            //  && selStart!=-1 && selEnd!=-1;
            //if (cutCopyOK && e.getKeyCode()==KeyEvent.VK_C)
            //  copySelection();
            //else if (cutCopyOK && e.getKeyCode()==KeyEvent.VK_X)
            //  {if (copySelection()>0) clearSelection();}
            //else if (cutCopyOK && e.getKeyCode()==KeyEvent.VK_A)
            //  selectAll();
            //else if (keyListener!=null)
            //  keyListener.keyPressed(e);
            int key = e.getKeyCode();
            if (keyListener!=null&&key!=KeyEvent.VK_S&& key!=KeyEvent.VK_C && key!=KeyEvent.VK_X&& key!=KeyEvent.VK_A)
                keyListener.keyPressed(e);

        }

        public void keyReleased (KeyEvent e) {}

        public void keyTyped (KeyEvent e) {
            if (keyListener!=null)
                keyListener.keyTyped(e);
        }

        public void actionPerformed (ActionEvent e) {
            String cmd=e.getActionCommand();
            doCommand(cmd);
        }

        void doCommand(String cmd) {
            if (cmd==null)
                return;
            if (cmd.equals("Save As..."))
                saveAs("");
            else if (cmd.equals("Cut"))
                cutSelection();
            else if (cmd.equals("Copy"))
                copySelection();
            else if (cmd.equals("Clear"))
                clearSelection();
            else if (cmd.equals("Select All"))
                selectAll();
            else if (cmd.equals("Duplicate..."))
                duplicate();
            else if (cmd.equals("Summarize"))
                IJ.doCommand("Summarize");
            else if (cmd.equals("Distribution..."))
                IJ.doCommand("Distribution...");
            else if (cmd.equals("Clear Results"))
                IJ.doCommand("Clear Results");
            else if (cmd.equals("Set Measurements..."))
                IJ.doCommand("Set Measurements...");
            else if (cmd.equals("Set File Extension..."))
                IJ.doCommand("Input/Output...");
        }

        public void lostOwnership (Clipboard clip, Transferable cont) {}

        void duplicate() {
            if (rt==null) return;
            ResultsTable rt2 = (ResultsTable)rt.clone();
            String title2 = IJ.getString("Title:", "Results2");
            if (!title2.equals("")) {
                if (title2.equals("Results")) title2 = "Results2";
                rt2.show(title2);
            }
        }

        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<iRowCount) {
                char[] chars = (char[])vData.elementAt(r);
                lineWidth = Math.max(tc.fMetrics.charsWidth(chars,0,chars.length), iGridWidth);
            }
            if(r>=0 && r<iRowCount && x<lineWidth) {
                selOrigin = r;
                selStart = r;
                selEnd = r;
            } else {
                resetSelection();
                selOrigin = r;
                if (r>=iRowCount)
                    selOrigin = iRowCount-1;
                //System.out.println("select: "+selOrigin);
            }
            tc.repaint();
            selLine=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;
            //System.out.println(r+"  "+selOrigin);
            if(r>=0 && r<iRowCount) {
                if (r<selOrigin) {
                    selStart = r;
                    selEnd = selOrigin;

                } else {
                    selStart = selOrigin;
                    selEnd = r;
                }
            }
            tc.repaint();
            selLine=r;
        }

        /**
        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();
            for (int i=selStart; i<=selEnd; i++) {
                char[] chars = (char[])(vData.elementAt(i));
                sb.append(chars);
                if (i<selEnd || selEnd>selStart) 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;
            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 (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; i<count; i++) {
                    vData.removeElementAt(selStart);
                    iRowCount--;
                }
                if (rt!=null && rowCount==rt.getCounter()) {
                    for (int i=0; i<count; i++)
                        rt.deleteRow(selStart);
                    rt.show(title);
                    if (!atEnd) {
                        iY = 0;
                        tc.repaint();
                    }
                }
            }
            selStart=-1; selEnd=-1; selOrigin=-1; selLine=-1;
            adjustVScroll();
            tc.repaint();
        }

        /** Deletes all the lines. */
        public void clear() {
            if (vData==null) return;
            vData.removeAllElements();
            iRowCount = 0;
            selStart=-1; selEnd=-1; selOrigin=-1; selLine=-1;
            adjustVScroll();
            tc.repaint();
        }

        /** Selects all the lines in this TextPanel. */
        public void selectAll() {
            if (selStart==0 && selEnd==iRowCount-1) {
                resetSelection();
                return;
            }
            selStart = 0;
            selEnd = iRowCount-1;
            selOrigin = 0;
            tc.repaint();
            selLine=-1;
        }

        /** Clears the selection, if any. */
        public void resetSelection() {
            selStart=-1;
            selEnd=-1;
            selOrigin=-1;
            selLine=-1;
            if (iRowCount>0)
                tc.repaint();
        }

        /** Writes all the text in this TextPanel to a file. */
        public void save(PrintWriter pw) {
            resetSelection();
            if (labels!=null && !labels.equals(""))
                pw.println(labels);
            for (int i=0; i<iRowCount; i++) {
                char[] chars = (char[])(vData.elementAt(i));
                pw.println(new String(chars));
            }
            unsavedLines = false;
        }

        /** Saves all the text in this TextPanel to a file. Set
            'path' to "" to display a save as dialog. Returns
            'false' if the user cancels the save as dialog.*/
        public boolean saveAs(String path) {
            boolean isResults = IJ.isResultsWindow() && IJ.getTextPanel()==this;
            if (path.equals("")) {
                IJ.wait(10);
                String ext = isResults?Prefs.get("options.ext", ".xls"):".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.write("" + e);
                return true;
            }
            save(pw);
            pw.close();
            if (isResults) {
                Analyzer.setUnsavedMeasurements(false);
                if (Recorder.record)
                    Recorder.record("saveAs", "Measurements", path);
            } else {
                if (Recorder.record)
                    Recorder.record("saveAs", "Text", path);
            }
            IJ.showStatus("");
            return true;
        }

        /** Returns all the text as a string. */
        public String getText() {
            StringBuffer sb = new StringBuffer();
            if (labels!=null && !labels.equals("")) {
                sb.append(labels);
                sb.append('\n');
            }
            for (int i=0; i<iRowCount; i++) {
                char[] chars = (char[])(vData.elementAt(i));
                sb.append(chars);
                sb.append('\n');
            }
            return new String(sb);
        }

        public void setTitle(String title) {
            this.title = title;
        }

        /** Returns the number of lines of text in this TextPanel. */
        public int getLineCount() {
            return iRowCount;
        }

        /** Returns the specified line as a string. The argument
            must be greater than or equal to zero and less than
            the value returned by getLineCount(). */
        public String getLine(int index) {
            if (index<0 || index>=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) {
            this.rt = rt;
        }

        void flush() {
            if (vData!=null)
                vData.removeAllElements();
            vData = null;
        }

    }

| TextPanel |