/*
 * The contents of this file are subject to the terms of the Common Development
 * and Distribution License (the License). You may not use this file except in
 * compliance with the License.
 * 
 * You can obtain a copy of the License at http://www.netbeans.org/cddl.html
 * or http://www.netbeans.org/cddl.txt.
 * 
 * When distributing Covered Code, include this CDDL Header Notice in each file
 * and include the License file at http://www.netbeans.org/cddl.txt.
 * If applicable, add the following below the CDDL Header, with the fields
 * enclosed by brackets [] replaced by your own identifying information:
 * "Portions Copyrighted [year] [name of copyright owner]"
 * 
 * The Original Software is NetBeans. The Initial Developer of the Original
 * Software is Sun Microsystems, Inc. Portions Copyright 1997-2006 Sun
 * Microsystems, Inc. All Rights Reserved.
 */

package org.netbeans.core.output;

import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.io.PipedReader;
import java.io.PipedWriter;
import java.io.Reader;
import java.io.OutputStreamWriter;
import java.io.IOException;
import java.io.FileWriter;
import java.io.File;

import java.awt.*;
import java.awt.event.*;
import java.awt.datatransfer.*;
import java.beans.PropertyChangeListener;
import java.beans.PropertyChangeEvent;
import java.util.ArrayList;
import java.util.Observable;
import java.util.Observer;
import java.util.Set;
import javax.swing.AbstractAction;

import javax.swing.JComponent;
import javax.swing.JPanel;
import javax.swing.KeyStroke;
import javax.swing.JPopupMenu;
import javax.swing.JMenuItem;
import javax.swing.JSplitPane;
import javax.swing.SwingUtilities;
import javax.swing.text.Keymap;

import org.openide.ErrorManager;
import org.openide.windows.*;
import org.openide.awt.MouseUtils;
import org.openide.actions.CutAction;
import org.openide.actions.DeleteAction;
import org.openide.actions.PasteAction;
import org.openide.actions.CopyAction;
import org.openide.actions.FindAction;
import org.openide.util.io.NullOutputStream;
import org.openide.util.NbBundle;
import org.openide.util.Mutex;
import org.openide.util.datatransfer.*;
import org.openide.util.actions.ActionPerformer;
import org.openide.util.actions.SystemAction;
import org.openide.util.actions.CallbackSystemAction;

import org.netbeans.lib.terminalemulator.*;
import org.openide.util.Lookup;

/**
 * Modified OutputTabTerm to be used as inner component of OutputView
 * for new output window implementation. Prototype. If possible should be
 * merged with OutputTabTerm or common code should be separated. Or common
 * subclass could be created.
 *
 * @author  Marek Slama
 *
 */

public class OutputTabInner extends TopComponent
implements InputOutput, PropertyChangeListener {


    public static final String ICON_RESOURCE =
        "/org/netbeans/core/resources/frames/output.gif"; // NOI18N
    
    /** Should this InputOutput take the focus and bring the window to the front,
    * when anything is written into the stream. */
    private boolean focusTaken = false;

    /** The reader for standard input */
    private Reader inReader;
    /** piped writer */
    PipedWriter inWriter = new PipedWriter();
    
    /** If true, the error output is separated from the normal output */
    private boolean errSeparated = false;
    private Boolean inputVisible = null;
    private boolean errVisible = false;
    
    /** Splitpane used to display split stdout/stderr */
    private JSplitPane splitpane = null;

    /** singleton instance of the standard output tab */
    //private static OutputTabInner standard;
    
    private static final long serialVersionUID =3276412782250080205L;
    
    private OutTermPane output;
    /** Error pane */
    private OutTermPane error = null;
    
    //private static Factory factory;
    
    private boolean hideOnly = false;
    
    // #37711 Hack, to be able to call isClosed API method from any thread while
    // the windows API can be called from AWT thread only.
    /** Flag indicating whether it is opened in window system. */
    private boolean openedInWinSys;
    private final Object LOCK_OPENED_IN_WINSYS = new Object();
    
    OutputTabInner(final String name) {
        synchronized (OutputTabInner.class) {
            
            output = new OutTermPane(this);
            
            synchronized(OutputView.ioCache) {
                OutputView.ioCache.put(name, this);
            }
            
            Mutex.EVENT.readAccess(new Runnable() {
                public void run() {
                    output.setName("StdOut"); //NOI18N
                    // set accessible description
                    getAccessibleContext ().setAccessibleName (
                        NbBundle.getBundle (OutputTabInner.class).getString ("ACSN_OutputWindow"));
                    getAccessibleContext ().setAccessibleDescription (
                        NbBundle.getBundle (OutputTabInner.class).getString ("ACSD_OutputWindow"));
                    setBorder (null);
                    add (output);
                    setName (name);
                }
            });

            TopComponent.getRegistry().addPropertyChangeListener(
                org.openide.util.WeakListener.propertyChange(this, TopComponent.getRegistry()));
            
            getInputMap(WHEN_ANCESTOR_OF_FOCUSED_COMPONENT).put(
                KeyStroke.getKeyStroke(KeyEvent.VK_F4, KeyEvent.CTRL_DOWN_MASK), 
                "discard"); //NOI18N
            
            getActionMap().put("discard", new DiscardAction());
        }        
    }
    
    private class DiscardAction extends AbstractAction {
        public void actionPerformed (ActionEvent ae) {
            OutputView view = (OutputView) SwingUtilities.getAncestorOfClass(
                OutputView.class, OutputTabInner.this);
            if (view != null) {
                view.discardTab(OutputTabInner.this);
            }
        }
    }

    public int getPersistenceType() {
        return TopComponent.PERSISTENCE_ONLY_OPENED;
    }

    public void layout() {
        if (splitpane != null) {
            splitpane.setBounds (0, 0, getWidth(), getHeight());
        } else {
            output.setBounds (0, 0, getWidth(), getHeight());
        }
    }
    
    private synchronized OutTermPane getErrorTerm (boolean create) {
        if (error == null && create) {
            error = new OutTermPane (this);
            error.setName("StdErr"); //NOI18N
            Mutex.EVENT.readAccess(new SplitPaneInstaller());
        }
        return error;
    }
    
    private class SplitPaneInstaller implements Runnable {
        public void run() {
            splitpane = new JSplitPane();
            if (splitpane.getComponentOrientation() == ComponentOrientation.RIGHT_TO_LEFT) {
                splitpane.setLeftComponent(error);
                splitpane.setRightComponent(output);
            } else {
                splitpane.setLeftComponent(output);
                splitpane.setRightComponent(error);
            }
            splitpane.setDividerLocation(getWidth() / 2);
            add (splitpane);
        }
    }
    
    /** Runnable which removes the split pane and restores the output to being
     * child of this component, to run on the event thread. */
    private class SplitPaneRemover implements Runnable {
        public void run() {
            //theoretically, one could munge the order of things enough for
            //the splitpane to be null, so check it
            if (splitpane != null) { 
                remove (splitpane);
            }
            error = null;
            add (output);
            Container parent = getParent ();
            if (parent != null) parent.validate ();
        }
    }    
    
    public synchronized Object writeReplace() throws java.io.ObjectStreamException {
        // bugfix #15703 - don't serialize tabs which you don't want to deseralize
        // later in Replace.readResolve, otherwise InstanceDataObject fails to
        // create instance of the tab
        if (!this.equals(OutputView.getFactory().getStdOutputTab())) {
            return null;
        }

        if (replace == null) {
            replace = new Replace(this.equals(OutputView.getFactory().getStdOutputTab()));
        }
        return replace;
    }
    
    // mainly for testing and debugging purposes
    // returns ActiveTerm for output OutTermPane
    public Term getTerm() {
        return getTerm( true );
    }
    
    // mainly for testing and debugging purposes
    // returns ActiveTerm for output or error OutTermPane
    public Term getTerm(boolean fromOutputPane) {
        if ( fromOutputPane ) {
            return output.getTerm();
        } else {
            return getErrorTerm(true).getTerm();
        }
    }
    
    /* Returns text content of output tab. This is
    * content of terminal buffer, which is limited by
    * history size.
    * <p>
    * This function is no MT-safe call it from the AWT Event Dispatch thread.
    *
    * @return text content of output window buffer
    */
    public String toString () {
        Term term = getTerm( true );
        if ( term == null )
            return "";         // NOI18N
        
        StringBuffer buf = new StringBuffer();
        for (int i=0; i<=term.getCursorCoord().row; i++) {
            if ( i > 0 )
                buf.append('\n');
            buf.append( term.getRowText( i ) );
        }
        return buf.toString();
    }

    private Replace replace;

    /** This class is serializaed instead of OutputTabInner */
    static class Replace implements java.io.Serializable {
        
        boolean defaultInstance;

        private static final long serialVersionUID =-3126744916624172415L;        

        public Replace (boolean defaultInstance) {
            this.defaultInstance = defaultInstance;
        }
        
        /** Resolve as default singleton or null */
        public Object readResolve() throws java.io.ObjectStreamException {
            if ( defaultInstance )
                return OutputView.getFactory().getStdOutputTab();
            else
                return null;
        }
    } 
    
    void ensureOpen() {
        //issue 37810, this code must run in EQ
        //#37996 When called from EQ it must be synchronous to keep
        //code execution order.
        Mutex.EVENT.readAccess(new Runnable() {
            public void run() {
                if (isClosed()) {
                    //debug("opening"); // NOI18N
                    OutputView.findDefault().openInOV(OutputTabInner.this);
                }
            }
        });
    }
    
    static String getOutDisplayName() {
        return NbBundle.getBundle(OutputTabInner.class).getString("CTL_OutputWindow");
    }
    
    //
    // TopComponent methods
    //
    
    /** Activates the component */
    protected void componentActivated () {
        output.activated();
    }

    /** Deactivates the component */
    protected void componentDeactivated () {
        output.deactivated();
    }

    public void requestFocus () {
        super.requestFocus();
        output.requestFocus();
    }
    
    public boolean requestFocusInWindow () {
        super.requestFocusInWindow();
        boolean result = output.requestFocusInWindow();
        return result;
    }
    
    //
    // InputOutput methods
    //
    
    public boolean isFocusTaken() {
        return focusTaken;
    }
    
    /** Set true, if you want to focus out window, when anything is written
    * into the stream */
    public void setFocusTaken(boolean value) {
        focusTaken = value;
    }

    private boolean firstOut=true;
    public org.openide.windows.OutputWriter getOut() {
        if (firstOut) {
            select();
            firstOut = false;
        }
        return output.writer;
    }
    
    public void select() {
        /*System.out.println("== OutputTabInner.select ENTER"
        + " name:" + getName());*/
        
        Mutex.EVENT.readAccess(new Runnable() {
            public void run() {
                ensureOpen();
                OutputView.findDefault().requestVisible(OutputTabInner.this);
            }
        });
        /*System.out.println("== OutputTabInner.select LEAVE 2"
        + " name:" + getName());*/
    }
    
    public org.openide.windows.OutputWriter getErr() {
        //debug("getErr"); // NOI18N
        if (errSeparated) {
            return getErrorTerm(true).writer;
        }
        else return output.writer;
    }
    
    /** Method to acquire a Reader for accessing an input of the tab.
    * @return a Reader for accessing an input line of the tab.
    */
    public java.io.Reader getIn() {
        //debug("getIn()"); // NOI18N
        // init inReader
        flushReader();
        // only if input isn't explicitly forbidden
        if ( inputVisible == null || inputVisible.booleanValue() )
            setInputVisible(true);
        return inReader;
    }

    public void setErrSeparated(boolean value) {
        if (errSeparated != value) {
            errSeparated = value;
            if (errVisible != errSeparated) {
                setErrVisible(errSeparated);
            }
        }
    }
    
    public java.io.Reader flushReader() {
        //debug("flushReader"); // NOI18N
        inWriter = new PipedWriter();
        try {
            inReader = new PipedReader(inWriter);
        } catch (java.io.IOException ex) {
            ErrorManager.getDefault().notify(ex);
            return null;
        }
        return inReader;
    }
    
    public void setOutputVisible(boolean param) {
        //debug("setOutputVisible"); // NOI18N
        if (param) {
            Mutex.EVENT.readAccess(new Runnable() {
                public void run() {
                    ensureOpen();
                }
            });
        }
        else {
            hideOnly = true;
            doClose();
        }
    }
    
    public void setErrVisible(boolean value) {
        if (errSeparated || errVisible != value ) {
            errVisible = value;
            if (errVisible) {
                //create the error term - this will install 
                //the splitpane for us
                getErrorTerm(true);
            } else {
                if (error != null) {
                    Mutex.EVENT.readAccess(new SplitPaneRemover());
                }
            }
        }
    }
    
    public  boolean isClosed() {
        //debug("isClosed()"); // NOI18N
        // XXX
        boolean isInContainerTopComponent = false;
        Component p = getParent();
        if(p instanceof javax.swing.JComponent) {
            javax.swing.JComponent parent = (javax.swing.JComponent)p;
            Object value = parent.getClientProperty("ContainerTopComponent"); // TEMP NOI18N
            if(value instanceof Boolean) {
                isInContainerTopComponent = ((Boolean)value).booleanValue();
            }
        }
        
        return !isOpenedInWinSys() && !isInContainerTopComponent;
    }
    
    protected void componentOpened() {
        super.componentOpened();
        setOpenedInWinSys(true);
    }
    
    protected void componentClosed() {
        super.componentClosed();
        setOpenedInWinSys(false);
    }
    
    private void setOpenedInWinSys(boolean o) {
        synchronized(LOCK_OPENED_IN_WINSYS) {
            openedInWinSys = o;
        }
    }
    
    private boolean isOpenedInWinSys() {
        synchronized(LOCK_OPENED_IN_WINSYS) {
            return openedInWinSys;
        }
    }
    
    public void setInputVisible(boolean param) {
        //debug("setInputVisible:" + param); // NOI18N
        if (param) {
            Mutex.EVENT.readAccess(new Runnable() {
                public void run() {
                    ensureOpen();
                }
            });
        }
        inputVisible = param ? Boolean.TRUE : Boolean.FALSE;
        output.setReadWrite( param );
    }
    
    public boolean isErrSeparated() {
        //debug("isErrSeparated"); // NOI18N
        return errSeparated;
    }
    
    public void closeInputOutput() {
        //debug("closeInputOutput"); // NOI18N
        doClose();
        try {
            inWriter.flush();
            inWriter.close();
        } catch (IOException ioe) {
            // do nothing in this case
        } catch (NullPointerException npe) {
            // only for sure - do nothing in this case
        }
        output.writer.close();
        if (error != null) {
            error.writer.close();
        }
    }
    
    private void doClose() {
        Mutex.EVENT.readAccess(new Runnable() {
            public void run() {
                if(isDisplayable()) {
                    OutputView.findDefault().discardTab(OutputTabInner.this);
                }
            }
        });
    }
    
    public void open () {
        OutputView.findDefault().openInOV(this);
    }
    
    // Replacement for TopComponentListener. Listen on opened components.
    public void propertyChange(PropertyChangeEvent evt) {
        if(TopComponent.Registry.PROP_OPENED.equals(evt.getPropertyName())) {
            Object oldValue = evt.getOldValue();
            Object newValue = evt.getNewValue();
            
            if(!hideOnly 
            && oldValue instanceof Set && ((Set)oldValue).contains(this) // Was opened.
            && newValue instanceof Set && !((Set)newValue).contains(this)) { // Is not opened yet -> was closed.
                output.doClear();
                if (error != null) {
                    error.doClear();
                }
            }
            hideOnly = false;
        }
    }
 
    private static OutputSettings outputSettings () {
        return (OutputSettings)OutputSettings.findObject (OutputSettings.class, true);
    }
    


    // share 'keyStrokeSet' among all Terms. 
    // doubles as a one-time flag.
    private static HashSet keyStrokeSet = null;

    // see setHyperlinkNavigationEnabled() to see what's this all about
    private static HashSet keyStrokeSet2 = null;

    private static void updateKeyStrokeSet() {
	keyStrokeSet.clear();
	keyStrokeSet.addAll( java.util.Arrays.asList( ((Keymap)Lookup.getDefault ().lookup ( Keymap.class ) ).getBoundKeyStrokes()) );
	for (int ks = KeyEvent.VK_A; ks <= KeyEvent.VK_Z; ks++)
	    keyStrokeSet.add( KeyStroke.getKeyStroke( ks, Event.ALT_MASK ) );

	// Note that we have to have the keyChar be ASCII Ctrl-T for this
	// to work.
	KeyStroke ks1 = KeyStroke.getKeyStroke(new Character((char)('T'-64)),
				    Event.CTRL_MASK|Event.SHIFT_MASK);
	KeyStroke ks2 = KeyStroke.getKeyStroke(new Character((char)('T'-64)),
					Event.CTRL_MASK);
	keyStrokeSet2 = (HashSet) keyStrokeSet.clone();
	keyStrokeSet2.add(ks1);
	keyStrokeSet2.add(ks2);
    }


    private static HashSet getCommonKeyStrokeSet() {

	if (keyStrokeSet != null) 
	    return keyStrokeSet;

	// Initialize only once

	keyStrokeSet = new HashSet();

	// Arrange so that we track changes in global keymap
        Keymap map = (Keymap)org.openide.util.Lookup.getDefault().lookup(Keymap.class);
        // This is a hack. Since there is no official API for notifying changes
        // from Keymap, just rely on fact that core impl is an Observable.
        if (map instanceof Observable) {
            Observable o = (Observable)map;
            o.addObserver(new Observer() {
                public void update(Observable o, Object arg) {
		    updateKeyStrokeSet();
                }
            });
        }

	updateKeyStrokeSet();

	return keyStrokeSet;
    }

    private static HashSet getCommonKeyStrokeSet2() {
	getCommonKeyStrokeSet();
	return keyStrokeSet2;
    }



    public static final class OutTermPane extends JPanel implements ActionPerformer, 
                                    ActionListener, PropertyChangeListener, Runnable {
        /** generated Serialized Version UID */
        static final long serialVersionUID = -633812012958420549L;

        private static final String REDIR_EXT = ".out";                 // NOI18N

        /** Information channel for this pane */
        TermOutputWriter writer;

        /** My parent output tab */
        OutputTabInner tab;
        private boolean findNextEnabled = false;


        /** Copy action */
        private static CopyAction copy = (CopyAction)CopyAction.get (CopyAction.class);
        private static FindAction find = (FindAction)FindAction.get (FindAction.class);
        private static CutAction cut = (CutAction) CutAction.findObject(CutAction.class, true);
        private static DeleteAction delete = (DeleteAction) DeleteAction.findObject(DeleteAction.class, true);
        private static PasteAction paste = (PasteAction) PasteAction.findObject(PasteAction.class, true);

        /** Private instance of Next jump action */
        private static NextOutJumpAction nextAction = (NextOutJumpAction)NextOutJumpAction.get (NextOutJumpAction.class);

        /** Private instance of Previous jump action */
        private static PreviousOutJumpAction previousAction = (PreviousOutJumpAction)PreviousOutJumpAction.get (PreviousOutJumpAction.class);

        /** Performer for jump actions */
        private JumpActionPerformer jumpPerformer = new JumpActionPerformer();
        private ActionPerformer copyActionPerformer = null;
        CallbackSystemAction csa;

        private HashMap listeners = new HashMap();

        /** PopupMenu */
        JPopupMenu jPopup;
        private JMenuItem selectAllItem;
        private JMenuItem findNextItem;
        private JMenuItem clearItem;
        private JMenuItem redirItem;
        private JMenuItem discardItem;
        private JMenuItem discardAllItem;

        private TermListener listener;
        private TermInputListener input_listener;
        ActiveTerm term;

        private boolean redirection = false;
        private int tabSize;

	private ActiveRegion currentHyperlink = null;
	private ActiveRegion activeHyperlink = null;	// was 'currentRegion'
        
        /** Creates pane without association to a tab.
        */
        public OutTermPane() {
            this (null);
        }


        /** Creates new OutTermPane in the specific OutputTabInner */
        public OutTermPane(OutputTabInner tab) {

            this.tab = tab;

            listener = new TermListener() {
                public void sizeChanged(Dimension cells, Dimension pixels) {
                    //PENDING
                }
            };

            term = new ActiveTerm ();

            updateNextPrevActions();

            outputSettings ().addPropertyChangeListener (this);

            term.addListener(listener);
            setReadWrite( false );
            term.setHistorySize( outputSettings().getHistorySize() );
            term.pushStream(new LineDiscipline());
            if ( tab != null ) {
                input_listener = new TIListener( tab );
                term.addInputListener(input_listener);
            }
            term.setClickToType(true);
            term.setAutoCopy( false );            
            term.setScrollOnOutput( false );
	    if (tab != null)
		term.getAccessibleContext().setAccessibleName(tab.getName());

            writer = new TermOutputWriter (term);

            term.setWordDelineator(new WordDelineator() {
                public int charClass(char c) {
                    if (Character.isJavaIdentifierPart(c))
                        return 1;
                    else
                        return 0;
                }
            } );

            setLayout(new BorderLayout());
            add( term );  


            term.setActionListener(new ActiveTermListener() {
                    public void action(ActiveRegion r, InputEvent e) {
                        if ( r.end.equals( new Coord() ) ) {
			    // dangling tail region(?)
			    return;
			}

			if ( e instanceof MouseEvent ) {
			    // We either hit the inner or outer region.
			    // Favor picking the outer region since that's what
			    // we're working with.

			    if (r.parent() == term.regionManager().root()) {
				// outer region, keep it
			    } else {
				r = r.parent();
			    }

			    gotoHyperlink(r);
			    activateHyperlink(true);
			}
		    }
		} );

	    term.setKeyStrokeSet(getCommonKeyStrokeSet());
            //XXX fix alt keys here

            term.getCanvas().addMouseListener(new MouseUtils.PopupMouseAdapter() {
                public void showPopup(MouseEvent mevt) {
                    if (jPopup == null) {
                        createPopupMenu();
                    }
                    //Issue 39947, ensure find action in correct state
                    updateCopyCutAction();
                    
                    jPopup.show(term, mevt.getX(), mevt.getY());            
                }
            });

            term.addPropertyChangeListener (new PropertyChangeListener() {
                public void propertyChange(PropertyChangeEvent evt) {
                    if ( "selectionExtent".equals( evt.getPropertyName() )) {
                        updateCopyCutAction();
                    }
                }
            });

            //XXX do this with a normal Swing InputMap/ActionMap:
            term.getCanvas().addKeyListener( new KeyAdapter() {

                public void keyPressed(KeyEvent e) {

                    switch (e.getKeyCode()) {
                        case KeyEvent.VK_ENTER:
                        case KeyEvent.VK_SPACE:
			    // Consume it first because link activation
			    // may shift focus and we don't want the
			    // event to be delivered elsewhere.
			    // Except that it doesn't seem to help
			    e.consume();
			    activateHyperlink(true);
                            break;

                        case KeyEvent.VK_T:
                            if (e.getModifiers() == (Event.CTRL_MASK |
						     Event.SHIFT_MASK) ) {
				// Shift-Ctrl-T previous error
				prevHyperlink();
				e.consume();
			    } else if (e.getModifiers() == Event.CTRL_MASK) {
				// Ctrl-T next error
                                linkOnStart = false;
				nextHyperlink();
				e.consume();
			    }
			    break;

                        case KeyEvent.VK_A:
                            if ( e.getModifiers() == Event.CTRL_MASK ) {
                                selectAll();
                            }
                            break;
                        case KeyEvent.VK_F3:
                            if ( e.getModifiers() == 0 ) {
                                findNextPattern();
                            }
                            break;
                    }
                }

            });

            setSettings ();
            getInputMap(WHEN_ANCESTOR_OF_FOCUSED_COMPONENT).put(
                KeyStroke.getKeyStroke(KeyEvent.VK_F10, KeyEvent.SHIFT_DOWN_MASK), 
                "popup"); //NOI18N
            getActionMap().put("popup", new PopupAction());
        }
        
        public void requestFocus() {
            super.requestFocus();
            term.requestFocus();
            activateLater();
        }
        
        public boolean requestFocusInWindow() {
            super.requestFocusInWindow();
            boolean result = term.requestFocusInWindow();
            if (result) {
                activateLater();
            }
            return result;
        }
        
        /** Calls activated() via SwingUtilities.invokeLater().  Necessary to do
         * this from requestFocus(), or the action performers will be reset by the
         * winsys after we set them.  */
        private void activateLater() {
            SwingUtilities.invokeLater (new Runnable() {
                public void run() {
                    //Make sure we really got focus
                    Component c = KeyboardFocusManager.getCurrentKeyboardFocusManager().getFocusOwner();
                    if (c == OutTermPane.this || OutTermPane.this.isAncestorOf(c)) {
                        activated();
                    }
                }
            });
        }
        
    
        private class PopupAction extends AbstractAction {
            public void actionPerformed (ActionEvent ae) {
                if (jPopup == null) {
                    createPopupMenu();
                }
                //Issue 39947, ensure find action in correct state
                updateCopyCutAction();
                jPopup.show((JComponent) getTerm(), 0, 0);            
            }
        }        

        public Term getTerm() {
            return term;
        }
        
        private boolean isFindNextEnabled() {
            return findNextEnabled;
        }
        
        private void setFindNextEnabled(boolean val) {
            findNextEnabled = val;
            if (findNextItem != null) {
                findNextItem.setEnabled (val);
            }
        }

        /** The writer for this pane.
        */
        public OutputWriter getOut () {
            return writer;
        }
        
        private void createPopupMenu() {
            jPopup = SystemAction.createPopupMenu( new SystemAction[] {copy, find} );

            if ( tab == null )
                // no redirection for notify exception
                redirection = false;
            else
                redirection = outputSettings().isRedirection();

            findNextItem = new JMenuItem(NbBundle.getBundle(OutTermPane.class).getString("CTL_FindNext"));
            findNextItem.addActionListener(this);
            findNextItem.setAccelerator( KeyStroke.getKeyStroke( KeyEvent.VK_F3, 0 ) );
            findNextItem.setEnabled (isFindNextEnabled());
            jPopup.add(findNextItem);

            selectAllItem = new JMenuItem(NbBundle.getBundle(OutTermPane.class).getString("CTL_SelectAll"));
            selectAllItem.addActionListener(this);
            selectAllItem.setAccelerator( KeyStroke.getKeyStroke( KeyEvent.VK_A, Event.CTRL_MASK ) );
            jPopup.add(selectAllItem);
            jPopup.addSeparator();
            // add clear
            clearItem = new JMenuItem(NbBundle.getBundle(OutTermPane.class).getString("CTL_Clear"));
            clearItem.addActionListener(this);
            jPopup.add(clearItem);
            // add redirection
            redirItem = new JMenuItem();
            redirItem.addActionListener(this);
            redirItem.setToolTipText(NbBundle.getBundle(OutputTabInner.class).getString("HINT_Redirect_Tab"));
            if ( tab != null ) {
                checkRedirItem();
                jPopup.addSeparator();
                jPopup.add(redirItem);
            }
            jPopup.addSeparator();
            //Add Discard
            discardItem = new JMenuItem(NbBundle.getBundle(OutTermPane.class).getString("LBL_Discard"));
            discardItem.addActionListener(this);
            jPopup.add(discardItem);
            //Add DiscardAll
            discardAllItem = new JMenuItem(NbBundle.getBundle(OutTermPane.class).getString("LBL_DiscardAll"));
            discardAllItem.addActionListener(this);
            jPopup.add(discardAllItem);
        }

        void updateNextPrevActions() {

	    // Much simpler than before since now we have wrapping. We only
	    // need to enable the actions if we have any hyperlinks.

	    if (firstHyperlink() != null) {
		nextAction.setActionPerformer(jumpPerformer);
		previousAction.setActionPerformer(jumpPerformer);        
	    } else {
                //Fix issue 34908, don't set performer to null if we don't own
                //the performer - tasklist or some other module may own it
                if (nextAction.getActionPerformer() == jumpPerformer) {
                    nextAction.setActionPerformer(null);
                }
                if (previousAction.getActionPerformer() == jumpPerformer) {
                    previousAction.setActionPerformer(null);
                }
	    }
        }

        boolean updateCopyCutAction () {
            cut.setActionPerformer( null );
            delete.setActionPerformer( null );
            find.setActionPerformer( new FindActionPerformer() );
            boolean ret;
            if ( term.getSelectedText() != null && term.getSelectedText().length() > 0 ) {
                copy.setActionPerformer( getCopyActionPerformer() );
                ret = true;
            }
            else {
                copy.setActionPerformer( null );
                ret = false;
            }
            updatePasteAction();
            return ret;
        }
        
        private class FindActionPerformer implements ActionPerformer {
            public void performAction(final SystemAction action) {            
                //XXX Will this code ever be called from off the event thread???
                invokeLater(new Runnable() {
                    public void run() {
                        FindDialogPanel findDialog = FindDialogPanel.showFindDialog();
                        if ( findDialog.isAccepted() ) {
                            String pattern = findDialog.getPattern();
                            if ( pattern != null && pattern.length() > 0 )
                                findPattern(
                                    pattern,
                                    ! findDialog.isBackwardSearch(),
                                    findDialog.isMatchCase(),
                                    findDialog.isWholeWordsOnly()
                                );
                        }
                    }
                });
            }
        }
        
        /** Find and select pattern in output window, which meets find criteria
         */
        private void findPattern(String pattern, boolean forward, boolean matchcase, boolean wholewords) {
            int lastFindRow = 0;
            int lastFindCol = 0;
            boolean doClearSel = false;
            if ( term.getSelectionExtent() != null ) {
                doClearSel = true;
                lastFindRow = term.getSelectionExtent().begin.row;
                lastFindCol = term.getSelectionExtent().begin.col;
            }
            else if ( ! forward ) {
                lastFindRow = term.getCursorCoord().row;
                lastFindCol = term.getCursorCoord().col;
            }
            int row = lastFindRow;
            int bcol;
            int ecol;
            int found = -1;
            int n = term.getCursorRow();
            while ( found == -1 && row <= n && row >= 0) {
                String s = term.getRowText( row );
                if ( row == lastFindRow ) {
                    if ( forward ) {
                        ecol = s.length();
                        if ( lastFindCol + 1 < ecol )
                            bcol = lastFindCol + 1;
                        else
                            bcol = ecol;
                    }
                    else {
                        bcol = 0;
                        if ( lastFindCol + pattern.length() - 1 < s.length() )
                            ecol = lastFindCol + pattern.length() - 1;
                        else
                            ecol = 0;    // NOI18N
                    }
                }
                else {
                    bcol = 0;
                    ecol = s.length();
                }
                if ( s.length() > 0 )
                    found = nextIndexOf( s, bcol, ecol, pattern, forward, matchcase, wholewords );
                if ( found == -1 ) {
                    if ( forward )
                        row ++;
                    else
                        row --;
                }
            }
            if ( found > -1 ) {
                // select found pattern
                Coord beginC = Coord.make( row, found );
                Extent selExt = new Extent( beginC, Coord.make( row, found + pattern.length() - 1 ) );
                term.setSelectionExtent( selExt );
                term.possiblyNormalize( beginC );
                setFindNextEnabled(true);
            }
            else if ( doClearSel )
                    term.clearSelection();
        }
        
        /** Find next pattern (if it was already defined) using previous find criteria
         */
        private void findNextPattern() {
            String pattern = FindDialogPanel.getPattern();
            if ( pattern != null && pattern.length() > 0 )
                findPattern(
                    pattern,
                    ! FindDialogPanel.isBackwardSearch(),
                    FindDialogPanel.isMatchCase(),
                    FindDialogPanel.isWholeWordsOnly()
                );
        }
        
        /** Returns next index of pattern in line (between bcol and ecol columns)
         * meeting find criteria
         */
        private int nextIndexOf(String line, int bcol, int ecol, String pattern, boolean forward, 
                                boolean matchcase, boolean wholewords) {
            int found = -1;
            if ( ! matchcase ) {
                pattern = pattern.toLowerCase();
                line = line.toLowerCase();
            }
            int len = pattern.length();
            while ( found == -1 && bcol < ecol ) {
                if ( forward )
                    found = line.substring( bcol, ecol ).indexOf( pattern );
                else
                    found = line.substring( bcol, ecol ).lastIndexOf( pattern );
                if ( found > -1 ) {
                    found = found + bcol;
                    if ( wholewords ) {
                        if ( ( found > 0 && Character.isUnicodeIdentifierPart( line.charAt( found - 1 ) ) ) ||
                            ( found + len < line.length() 
                                && Character.isUnicodeIdentifierPart( line.charAt( found + len ) ) ) )
                        {
                            if ( forward ) {
                                bcol = found + len;
                            }
                            else
                                ecol = found;
                            found = -1;
                        }
                    }
                }
                else
                    break;
            }
                
            return found;
        }
        
        private void updatePasteAction () {
            if ( term.isReadOnly() ) {
                paste.setPasteTypes (null);        
                return;
            }

            Clipboard clipboard = getClipboard();
            Transferable contents = clipboard.getContents( this );
            if (contents == null) {
                paste.setPasteTypes (null);        
                return;
            }

            if (!contents.isDataFlavorSupported(DataFlavor.stringFlavor)) {
                paste.setPasteTypes (null);        
                return;
            }

            paste.setPasteTypes( new PasteType[] {
                new PasteType() {
                    public Transferable paste() throws IOException {
                        term.paste();
                        return null;
                    }
                }
            });        
        }

        private ActionPerformer getCopyActionPerformer() {
            if ( copyActionPerformer == null )
                copyActionPerformer = new CopyActionPerformer( term );
            return copyActionPerformer;
        }
        
        private static Clipboard getClipboard() {
            Clipboard c = (java.awt.datatransfer.Clipboard)
                org.openide.util.Lookup.getDefault().lookup(java.awt.datatransfer.Clipboard.class);
            if (c == null) {
                c = java.awt.Toolkit.getDefaultToolkit().getSystemClipboard();
            }
            return c;
        }





	// We use terminalemulator.ActiveTerm's ActiveRegions to simulate
	// hyperlinks, but not in the obvious way.
	//
	// For accessibility purposes we separate the currently navigated to
	// hyperlink from the activated hyperlink. Navigation between links
	// happens using Ctrl-T and Shift-Ctrl-T, Activation happens using
	// Space,Enter,Return. Navigation wraps!
	// When we click on a hyperlink or use next prev actions the
	// navigation and activation are combined.
	//
	// Traditionally OW hilited the "whole" error message. This is done
	// when the hyperlink is activated. (There is the issue with OWs
	// detecting the whole error being rather ad-hoc but that's for elsewhere).
	// To implement this the chain of ActiveRegions is two-level so getting
	// the hyperlink is a bit messy. Here's what we get:
	//
	//	Some random text
	//  +-=	SyntaxError.java [14:1] invalid method declaration; return ...
	//  |	      public SyntaxErrorr() {
	//  +--		     ^
	//	More random text
	// 
	// We see the beginning of an error and create an "outer" region to wrap
	// the whole message. This outer region doesn't appear as a hyperlink
	// and only gets hilited. We also create an "inner" region that gets
	// rendered as a hyperlink.  This line is marked with '=' above.
	//
	// Now that we've defined "outer" and "inner", 'currentHyperlink' and 
	// 'activeHyperlink' point to the "outer" ActiveRegion!

	private void activateHyperlink(boolean doAction) {
	    if (currentHyperlink != null) {
		invokeJumpListener(currentHyperlink.begin, doAction );

		// erase old stuff
		if ( activeHyperlink != null ) {
		    int brow = activeHyperlink.begin.row;
		    int erow = activeHyperlink.end.row;
		    for (int r = brow; r <= erow; r++)
			term.setRowGlyph(r, 0, 0);
		}

		activeHyperlink = currentHyperlink;

		// draw new stuff
		if ( activeHyperlink != null ) {
		    int brow = activeHyperlink.begin.row;
		    int erow = activeHyperlink.end.row;
		    for (int r = brow; r <= erow; r++)
			term.setRowGlyph(r, 0, 58+1);	// first custom color
		    term.flush();
		}
	    }
	}

	private ActiveRegion firstHyperlink() {
	    ActiveRegion ar = term.regionManager().root();
	    if (ar != null)
		ar = ar.firstChild();
	    return ar;
	}

	private ActiveRegion lastHyperlink() {
	    ActiveRegion ar = term.regionManager().root();
	    if (ar != null)
		ar = ar.lastChild();
	    return ar;
	}

	private boolean next_wrap_warned = false;
	private boolean prev_wrap_warned = false;
        private boolean linkOnStart = false;

        private boolean nextHyperlink() {
            return nextHyperlink( false );
        }
        
	private boolean nextHyperlink(boolean forAction) {
	    ActiveRegion ar = currentHyperlink;
	    if (ar == null) {
		ar = firstHyperlink();
            } else if ( forAction && linkOnStart ) {
                ar = firstHyperlink();
	    } else {
		prev_wrap_warned = false;
		ar = ar.getNextSibling();
		if (ar == null) {
		    if (next_wrap_warned) {
			next_wrap_warned = false;
			ar = firstHyperlink();
		    } else {
			String msg = NbBundle.getBundle(OutputTabInner.class).
			    getString("MSG_AtLastError");
			org.openide.awt.StatusDisplayer.getDefault().setStatusText(msg);
			next_wrap_warned = true;
			return false;
		    }
		}
	    }
	    gotoHyperlink(ar);
	    return true;
	}

	private boolean prevHyperlink() {
	    ActiveRegion ar = currentHyperlink;
	    if (ar == null) {
		ar = lastHyperlink();
	    } else {
		next_wrap_warned = false;
		ar = ar.getPreviousSibling();
		if (ar == null) {
		    if (prev_wrap_warned) {
			prev_wrap_warned = false;
			ar = lastHyperlink();
		    } else {
			String msg = NbBundle.getBundle(OutputTabInner.class).
			    getString("MSG_AtFirstError");
			org.openide.awt.StatusDisplayer.getDefault().setStatusText(msg);
			prev_wrap_warned = true;
			return false;
		    }
		}
	    }
	    gotoHyperlink(ar);
	    return true;
	}

	private void gotoHyperlink(ActiveRegion ar) {
	    if (ar == null)
		return;

	    // Find the "inner" portion
	    ActiveRegion link = ar.firstChild();
	    if (link == null) {
		// We have a one-level region
		link = ar;
	    }

	    currentHyperlink = ar;
	    term.setSelectionExtent(link.getExtent());
	    term.possiblyNormalize(ar);
	}


        private void invokeJumpListener( Coord index, boolean doAction ) {
            //debug ("invokeJumpListener:doAction:" + doAction + ":index:" + index); // NOI18N

	    /* DEBUG
	    System.out.println("invokeJumpListener() " + index + " " + doAction); // NOI18N
	    */

	    // This can be improved by registering the listener with
	    // the region instead of an independent map.

            OutputListener olistener = (OutputListener)listeners.get(index);
            String str = term.textWithin(currentHyperlink.begin,
					 currentHyperlink.end);
            linkOnStart = false;
            if (olistener != null) {
                if ( doAction )
                    olistener.outputLineAction( new OutputEventImpl(InputOutput.NULL, str ));
                else
                    olistener.outputLineSelected( new OutputEventImpl(InputOutput.NULL, str ));
            }
        }


        private static final class OutputEventImpl extends OutputEvent {
            private String txt;

            static final long serialVersionUID =-437312125483471519L;

            public OutputEventImpl(InputOutput src, String txt) {
                super(src);
                this.txt = txt;
            }

            /** Returns text on the line.
            * @return the text on the line
            */
            public String getLine () {
                //return model.getElementAt(index).toString ();
                return txt;  // NOI18N
            }
        }    


        /** Writer, which can insert text into the output document.
        * @see java.io.Writer
        */
        class TermOutputWriter extends OutputWriter {
            ActiveTerm aterm = null;
            boolean timerSet = false;
            Boolean timerMode = null;
            boolean redirOpened = false;
            FileWriter redirWriter = null;
            private ActiveRegion region = null;

            TermOutputWriter(ActiveTerm term) {
                super(new OutputStreamWriter(new NullOutputStream()));
                aterm = term;
                setPageMode (false);
		aterm.setRefreshEnabled(false);
                if ( tab != null )
                    redirection = outputSettings ().isRedirection();
                checkRedirItem();
                if ( redirection )
                    redirOpen();
            }

            /** ---------------- OutputWriter methods ------------------------------*/
            public void reset() throws java.io.IOException {
                //debug ("reset");
		invokeNow(new Runnable() {
		    public void run() {
			doClear();
			aterm.setRefreshEnabled(false);
			resetRegion ();            
			prev_wrap_warned = false;
			next_wrap_warned = false;
		    }
		});
            }

            public void println(String str, final OutputListener outputListener) throws java.io.IOException {
		final String strCopy = str;

                //debug ("println:str: " + str + ":" + outputListener);
                ensureOpen ();
		invokeNow(new Runnable() {
		    public void run() {
			historySizeKeeper ();

			if ( outputListener != null ) {

			    // This code is fraught with various assumptions about
			    // the ordering and semantics of of output from the compiler.
			    // It expects that the compiler sends stuff in two println's
			    // where the first one may be a "hyperlink" and the second
			    // one may not be. If we get any other ordering we'll get
			    // poorly formed regions.
			    // On this basis beginRegion() and endRegion() should really
			    // be called something like firstHalf() and secondHalf().

                            /* Dead, but may be of historical interest:
			    if (isFromCompiler (outputListener)) {
				if (isHyperLink (outputListener)) {
				    firstHalf(strCopy);
				    registerListener(region ,outputListener);
				} else {
				    secondHalf(strCopy);                
				}
			    } else {
                             */
                            createRegion(strCopy, true);
                            registerListener(region ,outputListener);
			} else 
			    appendText(strCopy,false);

			printEOL (); 
		    }
		});
            }

            public void println() {
                //debug ("println");
                ensureOpen ();
		invokeNow(new Runnable() {
		    public void run() {
			printEOL (); 
		    }
		});
	    }

            public void write(char cbuf[], final int off, final int len) {            
                //debug ("WRITE:cbuf: " + new String(cbuf,off,len));
                ensureOpen ();
		final char cbufCopy[] = safe_way? new char[cbuf.length]: cbuf;
		if (safe_way)
		    System.arraycopy(cbuf, 0, cbufCopy, 0, cbuf.length);
		invokeNow(new Runnable() {
		    public void run() {
			appendText(new String(cbufCopy,off,len), !isTimerMode ());
		    }
		});
            }

            public void write(final int ch) {
                //debug ("write:ch:" + (char)ch + ":int: " + ch);
		invokeNow(new Runnable() {
		    public void run() {
			write ( new char[]{(char)ch}, 0, 1);
		    }
		});
            }

            public void write(String str, int off, final int len) {
                //debug ("write:str: " + str);
		final char[] chars = new char [len];
		str.getChars(off,off+len,chars,0);
		invokeNow(new Runnable() {
		    public void run() {
			write(chars,0,len);
		    }
		});
            }

            public void flush() {
                //debug ("flush");
		invokeNow(new Runnable() {
		    public void run() {
			aterm.flush();
			if ( redirWriter != null )
			    try {
				redirWriter.flush();
			    } catch (IOException e) {
				// XXX really ignore?
			    }
		    }
		});
            }

            public void close() {
                //debug ("close");
		invokeNow(new Runnable() {
		    public void run() {
			if ( redirOpened )
			    redirClose();
		    }
		});
            }                

            /** ----------------- Private helper methods -----------------*/

            private void redirOpen() {
                File redirFile = null;
                try {
                    File redirDirFile = outputSettings ().getDirectory();

                    String name = "";
                    if (tab != null) {
                        name = tab.getName();
                        int ispace = name.indexOf(' ');
                        if (ispace > 0)
                            name = name.substring( 0, ispace );
                    }
                    name = name.replace(File.separatorChar, '_');
                    name = name + REDIR_EXT;
                    redirFile = new File (redirDirFile, name);

                    redirFile.createNewFile();
                    redirWriter = new FileWriter(redirFile.getAbsolutePath(),true);

                    redirOpened = true;
                } catch (Exception e) {
                    ErrorManager.getDefault().annotate(e, ErrorManager.UNKNOWN, "Redir file: " + redirFile, null, null, null); // NOI18N
                    ErrorManager.getDefault().notify(ErrorManager.INFORMATIONAL, e);
                }
            }

            private void redirClose() {
                if ( redirWriter != null )
                    try {
                        redirWriter.close();
                        redirWriter = null;            
                    } catch (IOException e) {
                        ErrorManager.getDefault().notify(ErrorManager.INFORMATIONAL, e);
                    }
            }

            private void redirPrint(char[] chars,int offs, int len) {
                if ( redirWriter == null )
                    redirOpen();
                if (redirWriter == null) return;
                try {
                    redirWriter.write(chars, offs, len);
                } catch (IOException e) {
                    ErrorManager.getDefault().notify(ErrorManager.INFORMATIONAL, e);
                }            
            }

            private void redirPrint(String str) {
                if ( redirWriter == null )
                    redirOpen();
                if (redirWriter == null) return;
                try {
                    redirWriter.write(str);
                } catch (IOException e) {
                    ErrorManager.getDefault().notify(ErrorManager.INFORMATIONAL, e);
                }            
            }

            /** If region is created then switched to PageMode. */
            private ActiveRegion createRegion(String str, boolean hyperlink) {
                beginRegion (str, hyperlink);
                return endRegion (null);
            }

            private ActiveRegion beginRegion(String str, boolean hyperlink) {
                resetRegion ();
                setPageMode (true);
                Coord beginCoord = Coord.make(aterm.getCursorCoord().row, 0 );

                try {
                    region = aterm.regionManager().beginRegion(beginCoord);
                } catch (RegionException x) {
                    return null;
                }
                // TRY THIS INSTEAD: region = term.beginRegion(false);

                region.setFeedbackEnabled(false);
                //region.setFeedbackEnabled(true);
                region.setSelectable(false);

                if (hyperlink) {
                    aterm.setAttribute( getLinkAttr() );		// fg -> blue
                    aterm.setAttribute(4);		// underline
                }                

                if (str != null)
                    appendText(str, false);

                return region;
            }

            private ActiveRegion endRegion(String str) {            
                aterm.setAttribute(0);		// reset            
                if (str != null)
                    appendText(str, false);

                aterm.endRegion();

                updateNextPrevActions();
                return region;
            }

            private void firstHalf(String str) {
                setPageMode (true);
                Coord beginCoord = Coord.make(aterm.getCursorCoord().row, 0 );

		// Create beginning of region of whole error message
                try {
                    region = aterm.regionManager().beginRegion(beginCoord);
                } catch (RegionException x) {
                    return;
                }

                region.setFeedbackEnabled(false);
                region.setSelectable(false);


		// Create region for hyperlink
		ActiveRegion link_region;
                try {
                    link_region = aterm.regionManager().beginRegion(beginCoord);
                } catch (RegionException x) {
                    return;
                }

		aterm.setAttribute( getLinkAttr() );	// fg -> blue
		aterm.setAttribute(4);			// underline

		appendText(str, false);

                aterm.setAttribute(0);		// reset            

                aterm.endRegion();

		// done with hyperlink region, the main region will be close
		// in secondHalf().
            }

            private void secondHalf(String str) {
		appendText(str, false);

                aterm.endRegion();

                updateNextPrevActions();
            }

            /**
	     * Switches whether timer should be used as a way of optimalization. 
             * Default ON (QA evaluated performance without timer as
	     * insufficient). If this optimization needs to be excluded 
             * then this method should return false. If problems occur or 
             * you need to debug then switch it off.  
             */
            private boolean isTimerMode () {
                if (timerMode == null) {
                    String tempStr = System.getProperty("org.netbeans.core.output.OutputTabInner.timerMode");                
                    timerMode = (tempStr != null && tempStr.equalsIgnoreCase("false")) ? Boolean.FALSE : Boolean.TRUE; // NOI18N
                }
                /** Long enough to discourage everybody*/
                return timerMode.booleanValue();
            }

            /** Optimization way: all chars putted or appended to terminaemulator
             * are requested not to cause repaint. Repaint is made using this
             * timer. If this way of optimization will be used is switchable and
             * depends if timer mode is switched see  isTimerMode (). If not
             * smoothly enough then please decrease timer period or
             * switch timeMode off.
             */        
            private void repaintTimer() {
                if ( ! timerSet ) {
                    timerSet = true;
                    org.openide.util.RequestProcessor.getDefault().post( new Runnable() {
                        public void run() {                        
                            flush();                        
                            timerSet  = false;
                        }
                    },
                    120);
                }            
            }

            /** Page mode == mode of usage in terminalemulator.
             * Perpetually grows (danger of OutOfMemory), but may be reset.
             * If not Page mode then Interactive mode. Interactive mode keeps
             * historySize, so old Lines are removed from Buffer, but not reliable
             * coordinate operation, then not reliable work with regions.
             * In OW default Interactive mode. If any region request the
             * switched to PageMode. If in PageMode exceeded historySize
             * then reset. Not clever but unavoidable at the moment. Cannot be
             * reverted from PageMode to IneractiveMode.
             */
            private void setPageMode (boolean pageMode) {
                if (isPageMode ()) return;
                aterm.setAnchored (pageMode);
            }

            private boolean isPageMode () {
                return aterm.isAnchored ();
            }

            /**
             * Has meaning only in PageMode. If historySize is exceeded reset
             * is called
             */
            private void historySizeKeeper () {
                if (!isPageMode ()) return;
                /** Compiler output is about 100 lines. If anybody sets historySize
                 *  smaller then 100 line. Then bad luck.*/
                if (aterm.getHistoryBuffSize () - aterm.getHistorySize () > 0) {
                    try {
                        reset ();
                    } catch (Exception exc) {
                        // XXX really ignore?
                    }
                }

            }

            private void resetRegion () {
                if (region != null) {
                    aterm.endRegion();
                    region = null;
                }            
            }

            private void registerListener (ActiveRegion regRegion, OutputListener outpList) {
                if (regRegion != null && outpList != null)
                    listeners.put( regRegion.begin, outpList );
            }

            private  void printEOL () {
                appendText("\n",!isTimerMode ());//NOI18N
            }

            /**
             * Mysterious method, probably a relic of obsolete stack trace
             * processing. Don't be afraid to kill it.
             */
            private void appendText (String str, boolean repaint) {
                if (isTimerMode ()) {
                    /** I don`t want to call term.appendText basically. But it
                     * is only one method that doesn`t call repaint. But has
                     * side effect: expands '\n' to "\n\r".
                     */
                    if ( redirection )
                        redirPrint(str);            

                    char[] cs = str.toCharArray();
                    aterm.putChars(cs, 0, cs.length);
                    repaintTimer();                                        
                    return;
                }

                char[] tmp = new char[str.length()];
                str.getChars(0, str.length(), tmp, 0);            

                appendChars(tmp,0, str.length(), repaint);
            }

            private void appendChars(char[] chars,int offs, int len, boolean repaint) {                        
                if (isTimerMode ()) {
                    appendText (new String (chars, offs, len), repaint);
                    return;
                }

                if ( redirection )
                    redirPrint(chars, offs, len);            

                if (!repaint && aterm.isRefreshEnabled())  {
                    /** Introduced in Ivan`s DirectTermOutputWriter: but I
                     * hasitate if has any sense because
                     * term.setRefreshEnabled(true) calls repaint then can be
                     * directly called term.putChars(chars, offs, len);
                     */
                    aterm.setRefreshEnabled(false);                                    
                    aterm.putChars(chars, offs, len);
                    aterm.setRefreshEnabled(true);
                } else aterm.putChars(chars, offs, len);            
            }
        }

        final class JumpActionPerformer implements ActionPerformer {

            /** Performer for actions */
            public void performAction(final SystemAction action) {            
		invokeLater(new Runnable() {
		    public void run() {

			if (action instanceof NextOutJumpAction) {
			    // Traditionally bound to F12
			    if (nextHyperlink(true))
				activateHyperlink(true);
			}

			if (action instanceof PreviousOutJumpAction) {
			    // Traditionally bound to Shift-F12
			    if (prevHyperlink())
				activateHyperlink(true);
			}

			updateNextPrevActions();
		    }
		});
            }
        }

        static class CopyActionPerformer extends Object implements
            org.openide.util.actions.ActionPerformer {

            ActiveTerm at;

            public CopyActionPerformer (ActiveTerm at) {
                this.at = at;
            }

            /** Perform copy or cut action. */
            public void performAction(org.openide.util.actions.SystemAction action) {
		invokeLater(new Runnable() {
		    public void run() {
			at.copy();
		    }
		});
            }
        }

        private static class TIListener implements TermInputListener {

            private OutputTabInner tab;

            public TIListener(OutputTabInner tab) {
                this.tab = tab;
            }

            public void sendChar(char c) {
                try {                
                    tab.inWriter.write(c);
                    tab.inWriter.flush();
                } catch (Exception x) {                
                    // XXX really ignore?
                } 
            }
            public void sendChars(char[] c, int offset, int n) {
                try {                
                    tab.inWriter.write(c, offset, n);
                    tab.inWriter.flush();
                } catch (Exception x) {                
                    // XXX really ignore?
                } 
            }
        }

        /** Sets action performers. Called by OutputTabInner.componentActivated(). */
        public void activated() {
            updateCopyCutAction ();
            if (csa == null) {
                try {
                    // XXX what is wrong with SystemAction.get(PopupAction.class)?
                    Class popup = Class.forName("org.openide.actions.PopupAction"); // NOI18N
                    csa = (CallbackSystemAction) CallbackSystemAction.get(popup);
                } catch (ClassNotFoundException e) {
                    Error err = new NoClassDefFoundError();
                    ErrorManager.getDefault().annotate(err, e);
                    throw err;
                }
            }
            csa.setActionPerformer(this);
            setFindNextEnabled( FindDialogPanel.getPattern() != null );
        }

        /** Unsets action performers. Called by OutputTabInner.componentDeactivated(). */
        public void deactivated() {
            if (csa != null && this.equals( csa.getActionPerformer() ) ) {
                csa.setActionPerformer(null);
            }
        }

        public void focusGained(FocusEvent ev) {
            //Code moved to activated() called by OutputTabInner.componentActivated()
        }

        public void focusLost(FocusEvent ev) {
            //Code moved to deactivated() called by OutputTabInner.componentDeactivated()
        }

        /**
	 * Performer for action which shows popup menu (Shift-F10)
	 */
        public void performAction(SystemAction action) {
            if (! (action instanceof org.openide.actions.PopupAction) ) 
		return;
	    Mutex.EVENT.readAccess( new Runnable() {
		public void run() {
		    Coord xy = null;

		    if ( ! term.isReadOnly() && term.isCoordVisible(term.getCursorCoord()) ) {
			xy = term.getCursorCoord();
		    }
		    else if ( term.getSelectionExtent() != null ) {
			if ( term.isCoordVisible(term.getSelectionExtent().begin )) {
			    xy = term.getSelectionExtent().begin;
			}
			else if ( term.isCoordVisible(term.getSelectionExtent().end )) {
			    xy = term.getSelectionExtent().end;
			}
		    }

		    if ( xy == null && currentHyperlink != null ) {
			if ( term.isCoordVisible(currentHyperlink.begin )) {
			    xy = currentHyperlink.begin;
			}
			else if ( term.isCoordVisible(currentHyperlink.end )) {
			    xy = currentHyperlink.end;
			}
		    }

		    Point p = null;
		    if ( xy == null )
			p = new Point( 0, 0 );
		    else
			p = term.toPixel( xy );

		    if (p == null) {
			return;
		    }
                    if (jPopup == null) {
                        createPopupMenu();
                    }
		    jPopup.show(term, p.x, p.y);
	       }
	   });
        }

        public void actionPerformed(java.awt.event.ActionEvent actionEvent) {
            if (actionEvent.getSource() == selectAllItem) {
                selectAll();
            }
            else if (actionEvent.getSource() == findNextItem) {
                findNextPattern();
            } else if (actionEvent.getSource() == clearItem) {
                doClear();
            } else if (actionEvent.getSource() == redirItem) {                        
                redirection = !redirection;
                checkRedirItem();
            } else if (actionEvent.getSource() == discardItem) {                        
                OutputView.findDefault().discardTab();
            } else if (actionEvent.getSource() == discardAllItem) {                        
                OutputView.findDefault().discardAllTabs();
            }
        }

        void doClear() {
            term.clearHistory();
            term.clear();        
	    setHyperlinkNavigationEnabled(false);
            Iterator it = listeners.values().iterator();
            while (it.hasNext()) {
                OutputListener oli = (OutputListener)it.next();
                oli.outputLineCleared( new OutputEventImpl(InputOutput.NULL, null ));
            }
            listeners.clear();
            activeHyperlink = null;
	    currentHyperlink = null;
            updateNextPrevActions();
        }

        /** output settings */
        private static OutputSettings outputSettings () {
            return (OutputSettings)OutputSettings.findObject (OutputSettings.class, true);
        }


	private boolean hyperlinkNavigationEnabled = false;

	private void setHyperlinkNavigationEnabled(boolean hlne) {

	    if (hyperlinkNavigationEnabled == hlne)
		return;
	    hyperlinkNavigationEnabled = hlne;

	    // Switch between the keysets where Ctrl-T and Ctrl-Shift-T are 
	    // allowed and one where they aren't.

	    if (hlne) {
		term.setScrollOnInput(false);
		term.setKeyStrokeSet(getCommonKeyStrokeSet2());
	    } else {
		term.setScrollOnInput(true);
		term.setKeyStrokeSet(getCommonKeyStrokeSet());
	    }

	}

        private void checkRedirItem() {
            if ( tab == null )
                return;
            if ( !redirection && writer != null )
                ((TermOutputWriter)writer).redirClose();
            if ( redirItem == null )
                return;
            if ( redirection ) {
                if (outputSettings().checkRedirectionDirExists(outputSettings().getDirectory())) {
                    redirItem.setText(NbBundle.getBundle (OutputTabInner.class).getString ("CTL_Redirect_Off"));
                } else {
                    redirection = false;
                }
            } else {
                redirItem.setText(NbBundle.getBundle (OutputTabInner.class).getString ("CTL_Redirect_On"));
            }
        }

        private void checkFont() {
            Font font = term.getFont();
            if (font != null &&
                font.isPlain() &&
                (font.getSize() == outputSettings ().getFontSize())) {
                return;
            } else {
                Font nf = new Font("Monospaced", java.awt.Font.PLAIN, outputSettings ().getFontSize()); // NOI18N
                term.setFont(nf);
            }
        }
        
        private void setSettings () {
            checkFont();
            term.setForeground( outputSettings ().getBaseForeground());
            term.setBackground( outputSettings ().getBaseBackground());
	    term.setCustomColor(1, outputSettings().getJumpCursorBackground());
            term.setHistorySize( outputSettings().getHistorySize() );
            tabSize = outputSettings ().getTabSize();
            term.setTabSize( tabSize );
            term.setCustomColor( 0, outputSettings ().getJumpLinkForeground());
            term.setHighlightColor( outputSettings ().getSelectionBackground());
        }

        public void propertyChange (PropertyChangeEvent evt) {
            if ( OutputSettings.PROP_REDIRECTION.equals( evt.getPropertyName() )) {
                if ( tab != null ) {
                    redirection = ((Boolean)evt.getNewValue()).booleanValue();
                    checkRedirItem();
                }
	    }
            else if ( OutputSettings.PROP_DIRECTORY.equals( evt.getPropertyName() )) {
                if ( tab != null ) {
                    if ( redirection && writer != null ) {
                        ((TermOutputWriter)writer).redirClose();
                        if (outputSettings().getDirectory().exists()) {
                            ((TermOutputWriter)writer).redirOpen();
                        }
                    }
                }
	    }
            else
                setSettings ();
        }

        private void selectAll() {
            Extent allExt = new Extent( Coord.make(0,0), term.getCursorCoord() );
            term.setSelectionExtent( allExt );
        }

        private void ensureOpen() {
            if (isShowing()) {
                return;
            }
            //Note, intentionally not using Mutex.EVENT.readAccess here.  Since
            //this can be called for individual characters that are output,
            //avoiding any unnecessary overhead, however trivial
            if (!SwingUtilities.isEventDispatchThread()) {
                SwingUtilities.invokeLater(new Runnable() {
                    public void run() {
                        _ensureOpen();
                    }
                });
            } else {
                _ensureOpen();
            }
        }
        
        private void _ensureOpen() {
            if (tab != null) {
                tab.ensureOpen();
            }
        }
        
        private ArrayList runnables = new ArrayList();
        private boolean runningRunnables;
        private Object syncObject = new Object();
        
        /**
         * Utility to schedule stuff on the Event Dispatch thread.
         * <p>
         * The usual pattern is for every function foo() there be a fooImpl()
         * and call fooImpl() or wrap it in a Runnable. With this 'invokeNow'
         * we always wrap in a Runnable but I believe it's a small cost
         * since inmost cases (heavy character output) we're going through the
         * Runnable anyway.
         * <p>
         * For an explanation of why invokeAndWait() as opposed to invokeLater()
         * see .../terminalemulator/ReleaseNotes.ivan.txt under tag ivan_17.
         */
        private void invokeNow(final Runnable runnable) {
            if (SwingUtilities.isEventDispatchThread()) {
                runnable.run();
            } else {
                if (safe_way) {
                    synchronized ( syncObject ) {
                        if (runningRunnables) {
                            try {
                                syncObject.wait (500);
                            } catch (InterruptedException ex) {
                            }
                        }
                        
                        runnables.add( runnable );
                        if (runnables.size () > 50) {
                            try {
                                syncObject.wait (500);
                            } catch (InterruptedException ex) {
                            }
                        }
                        if ( runnables.size() == 1 )
                            SwingUtilities.invokeLater( this );
                    }
                }
                else {
                    try {
                        SwingUtilities.invokeAndWait(runnable);
                    } catch (Exception x) {
                        ;
                    } 
                }
            }
        }
        
        public void run () {
            ArrayList torun;
            synchronized ( syncObject ) {
                runningRunnables = true;
                torun = (ArrayList)runnables.clone();
                runnables = new ArrayList();
            }
            Iterator it = torun.iterator();
            while ( it.hasNext() )
                ((Runnable)(it.next())).run();
            synchronized ( syncObject ) {
                runningRunnables = false;
                syncObject.notifyAll();
            }
        }

        private int getLinkAttr() {
            // custom color 0 in term
            return 50;
        }

        void setReadWrite( boolean rw ) {
            term.setCursorVisible ( rw );        
            term.setReadOnly ( !rw );
        }
    }

    final static private boolean safe_way = true;

    
    

    private static void invokeLater(Runnable runnable) {
	if (SwingUtilities.isEventDispatchThread()) {
	    runnable.run();
	} else {
            SwingUtilities.invokeLater(runnable);
	}
    }

    /* DEBUG
    private static void ckEventDispatchThread() {
	if (!SwingUtilities.isEventDispatchThread()) {
	    System.out.println("OW: NOT IN EventDispatchThread");
	    Thread.dumpStack();
	}
    }
    */
    
}
