Browser.java

Änderung 1: Einbinden der CIS-Klassen
Änderung 2: Einfügen eines neuen Menüpunkts „CIS”
Änderung 3: Aktion, die beim Aufruf des Menüpunkts ausgeführt wird


/*
 * LAPIS lightweight structured text processing system
 * Copyright (C) 1998,1999 Carnegie Mellon University
 *
 * This library is free software; you can redistribute it
 * and/or modify it under the terms of the GNU Library
 * General Public License as published by the Free Software
 * Foundation, version 2.
 *
 * LAPIS homepage: http://www.cs.cmu.edu/~rcm/lapis/
 */
package lapis.swing;

import lapis.*;
import lapis.tc.*;
import java.awt.*;
import java.awt.event.*;
import java.io.*;
import java.util.*;
import java.net.*;
import java.beans.*;
import java.awt.datatransfer.*;
import javax.swing.*;
import javax.swing.text.*;
import javax.swing.event.*;
import javax.swing.tree.*;
import javax.swing.undo.*;
import rcm.awt.Constrain;
import rcm.awt.PopupDialog;
import rcm.swing.*;
import rcm.net.URLUtil;
import rcm.progress.*;
import rcm.util.Str;
import rcm.util.Thr;
import rcm.util.Debug;
import lapis.Parser;


// MED: Klasse hinzugefügt:
import lapis.cis.*;

public class Browser extends JFrame implements Tool {

    ////////////////////
    //////////////////// Constants
    ////////////////////

    /**
     * Number of Documents stored in browser history.  Note that the
     * browser history stores not only the URL but the content and named
     * region sets too, so this may occupy significant memory.
     */
    public static final int MAX_DOCUMENT_HISTORY = 100;

    /**
     * Number of patterns stored in pattern history.  Patterns are stored
     * simply as strings.
     */
    public static final int MAX_PATTERN_HISTORY = 100;

    /**
     * Priority of background thread (for evaluating commands, downloading
     * URLs, and parsing). Ranges from 1 to 10.
     */
    public static final int BACKGROUND_PRIORITY = 4;

//     /**
//      * Proportion of browser pane devoted to displaying standard output
//      * when a program's stdout and stderr must be shown. Ranges from 0 to 1.
//      */
//     public static final double STDOUT_PROPORTION = 0.5;

    /**
     * Background color of active browser pane.
     */
    public static final Color ACTIVE_PANE_COLOR = Color.orange;

    ////////////////////
    //////////////////// Browser UI
    ////////////////////

    JMenuBar menubar;
    JToolBar toolbar;
    JMenu toolMenu;

    JComboBox location;         // Document location (URL or filename) + document history
    JComboBox view;             // Content type (text, HTML, Java, etc.)
    
    TCEditor pattern;           // TC pattern to highlight
    UndoManager undoPattern;    // undo manager for pattern 
    LabelTree tree;             // Labeling tree
    JLabel patternMatchCount;   // displays # of matches

    BrowserPane outputPane;    // Text/HTML display
    BrowserPane errorPane;      // Standard error display
    
    JLabel outputLabel;
    JLabel errorLabel;

    JScrollPane browserScrollPane, errorScrollPane;
    JScrollPane patternScrollPane, treeScrollPane;

    // Panels containing outputPane, errorPane, pattern, and tree.
    JPanel outputPanel;
    JPanel errorPanel;
    JPanel patternPanel;
    JPanel treePanel;

    // Splitters tiling the browser window with outputPanel,
    // errorPanel, and treePanel.
    JSplitPane errorSplitter; // splitter containing outputPane and errorPane
    JSplitPane treeSplitter;    // splitter containing pattern and tree
    JSplitPane midSplitter;    // splitter containing errorSplitter and treeSplitter

    SwingProgressBar progressBar;   // Progress bar
    JLabel statusBar;           // Status bar
    JButton feedbackButton;     // Feedback button

    TCFileEditor tcEditor;          // Pattern editor (null until requested by user)
    TCLFileEditor tclEditor;          // Script editor (null until requested by user)

    lapis.trees.RegionSetDisplay regionSetDisplay;
                                // Debugging window (==null until requested by user)

    ////////////////////
    //////////////////// Browser state
    ////////////////////

    BrowserPane activePane;   // either outputPane or errorPane, whichever
                                // has been given focus by user

    SwingDocument.Factory factory;  // DocumentFactory (includes Tcl interpreter state)


    ////////////////////
    //////////////////// Highlighted search result
    ////////////////////

    RegionSet highlightedRegions; // Currently highlighted region set, or null if none
    RegionSet highlightedFlatRegions;    // Flat version of highlightedRegions, or null if none
    String highlightedPattern;    // TC pattern of highlighted region set, or null if none
    String highlightedName;       // Label of highlighted region set, or null if not a simple named region set

    boolean isSearching = false;    // true if Next Match has been invoked
                                    // on the current highlighted region set

    int shiftDragMark = -1; // the anchor for the current mouse selection
    Region clickedRegion = null; // the region right-clicked on
    
    ////////////////////
    //////////////////// Constructors
    ////////////////////

    public Browser () {
        super ("Lapis");


        // Set look-and-feel
        // Windows: "com.sun.java.swing.plaf.windows.WindowsLookAndFeel"
        // Motif: "com.sun.java.swing.plaf.motif.MotifLookAndFeel"
        // Default: null
        String lookAndFeel = Main.config.getProperty ("look-and-feel");
        if (lookAndFeel != null) {
            try {
                javax.swing.UIManager.setLookAndFeel(lookAndFeel);
            } catch (Exception e) { 
                Debug.report (e);
            }
        }

        //System.err.println ("Creating Browser");

        // Handle window-close operation
        setDefaultCloseOperation (DISPOSE_ON_CLOSE);
        addWindowListener (new WindowAdapter () {
            public void windowClosed (WindowEvent evt) {
                saveConfiguration ();
                lapis.Main.browserDisposed ();
            }
        });

        Layout mainLayout = Layout.vertical ((JComponent)getContentPane ());

        // Create the menubar
        menubar = new JMenuBar ();
        JMenuItem item;

        JMenu fileMenu = new AcceleratedMenu ("&File");
        fileMenu.add (newBrowserAction);
        fileMenu.add (openAction);
        fileMenu.add (saveAction);
        fileMenu.add (closeAction);
        fileMenu.add (exitAction);
        menubar.add (fileMenu);

        JMenu editMenu = new AcceleratedMenu ("&Edit");
        editMenu.add (undoAction);
        editMenu.add (cutAction);
        editMenu.add (copyAction);
        editMenu.add (pasteAction);
        editMenu.add (selectAllAction);
        editMenu.addSeparator ();
        editMenu.add (nextMatchAction);
        //editMenu.add (previousMatchAction);
        menubar.add (editMenu);

        JMenu goMenu = new AcceleratedMenu ("&Go");
        goMenu.add (backAction);
        goMenu.add (forwardAction);
        goMenu.add (reloadAction);
        goMenu.add (stopAction);
        menubar.add (goMenu);

        JMenu parserMenu = new AcceleratedMenu ("&Patterns");
        parserMenu.add (editorAction);
        parserMenu.add (parserAction);
        parserMenu.addSeparator ();
        parserMenu.add (nameAction);
        parserMenu.add (nameAsAction);
        parserMenu.add (unnameAction);
        parserMenu.addSeparator ();
        parserMenu.add (deleteLabelAction);
        menubar.add (parserMenu);

        JMenu scriptMenu = new AcceleratedMenu ("&Scripts");
        scriptMenu.add (scriptEditorAction);
        scriptMenu.add (demonstrateAction);
        scriptMenu.add (historyScriptAction);
        menubar.add (scriptMenu);

        JMenu toolMenu = new AcceleratedMenu ("&Tools");
        toolMenu.add (keepAction);
        toolMenu.add (deleteAction);
        toolMenu.add (sortAction);
        toolMenu.add (replaceAction);
        toolMenu.add (extractAction);
        toolMenu.add (sumAction);
        toolMenu.add (averageAction);
        menubar.add (toolMenu);

// MED: Hier Menüleiste eingefügt

	JMenu cisMenu = new AcceleratedMenu("&CIS");
	cisMenu.add(focCrawlAction);
	menubar.add(cisMenu);

        JMenu helpMenu = new AcceleratedMenu ("&Help");
        helpMenu.add (feedbackAction);
        menubar.add (helpMenu);

        JMenu debugMenu = new AcceleratedMenu ("&Debug");
        debugMenu.add (documentPropertiesAction);
        debugMenu.add (regionSetDisplayAction);
        debugMenu.add (highlightOptionAction);
        helpMenu.add (debugMenu);

        setJMenuBar (menubar);

        // Create the toolbar
        toolbar = new JToolBar (JToolBar.HORIZONTAL);
        toolbar.setFloatable (false);
        toolbar.add (backAction);
        toolbar.add (forwardAction);
        toolbar.add (reloadAction);
        toolbar.add (stopAction);
//         toolbar.addSeparator ();
//         toolbar.add (keepAction);
//         toolbar.add (deleteAction);
//         toolbar.add (sortAction);
//         toolbar.add (replaceAction);
//         toolbar.add (extractAction);
//         toolbar.add (sumAction);
//         toolbar.add (averageAction);
//         toolbar.add (dictionaryAction);
//         toolbar.add (googleAction);
//         toolbar.add (amazonAction);
//         toolbar.addSeparator ();
//         toolbar.add (editorAction);
//         toolbar.add (demonstrateAction);

        mainLayout.add (toolbar, Layout.FIXED_HEIGHT);

        // Create the Location/View As controls
        location = new JComboBox (new History (MAX_DOCUMENT_HISTORY));
        location.setEditable (true);
        location.setEditor (new BrowserComboBoxEditor ());
        location.getEditor ().addActionListener (locationAction);
        location.addActionListener (historyAction);

        view = new JComboBox ();
        view.addItem (new ViewType ("Text", "text/plain"));
        view.addItem (new ViewType ("HTML", "text/html"));
        view.addActionListener (changeViewAction);

        mainLayout.add (Layout.gridbag ()
                        .setContainerInsets (1, 1, 1, 1)
                        .add (new JLabel ("Command: "), Layout.FIXED_WIDTH)
                        .add (location)
                        .add (new JLabel (" View As: "), Layout.FIXED_WIDTH)
                        .add (view, Layout.FIXED_WIDTH)
                        .done (),
                        Layout.FIXED_HEIGHT);

        // Create the browser pane and its decorations
        outputPane = new BrowserPane (this);
        outputPane.addCaretListener (caretListener);
        outputPane.addHyperlinkListener (hyperlinkListener);
        outputPane.addMouseListener (rightClickListener);
        outputPane.addMouseListener (shiftClickListener);
        outputPane.addMouseMotionListener (shiftDragListener);
        outputPane.addFocusListener (paneFocusListener);

        browserScrollPane = new JScrollPane (outputPane);
        browserScrollPane.setVerticalScrollBarPolicy (JScrollPane.VERTICAL_SCROLLBAR_ALWAYS);
        browserScrollPane.setMinimumSize (new Dimension (0, 0));

        outputPanel = (JPanel) Layout.border ()
            .add (outputLabel = new JLabel ("Output: "), Layout.NORTH)
            .add (browserScrollPane, Layout.CENTER)
            .done ();
        outputPanel.setPreferredSize 
            (getSavedDimension ("outputPane", new Dimension (600, 400)));

        outputPane
            .getKeymap ()
            .addActionForKeyStroke 
            (KeyStroke.getKeyStroke ('C', KeyEvent.CTRL_MASK),
             copyKeystrokeAction);

        // Create the error pane and its decorations
        errorPane = new BrowserPane (this);
        errorPane.addCaretListener (caretListener);
        errorPane.addHyperlinkListener (hyperlinkListener);
        errorPane.addMouseListener (rightClickListener);
        errorPane.addMouseListener (shiftClickListener);
        errorPane.addMouseMotionListener (shiftDragListener);
        errorPane.addFocusListener (paneFocusListener);

        errorScrollPane = new JScrollPane (errorPane);
        errorScrollPane.setVerticalScrollBarPolicy (JScrollPane.VERTICAL_SCROLLBAR_ALWAYS);
        errorScrollPane.setMinimumSize (new Dimension (0, 0));

        errorPanel = (JPanel) Layout.border ()
            .add (errorLabel = new JLabel ("Errors: "), Layout.NORTH)
            .add (errorScrollPane, Layout.CENTER)
            .done ();
        errorPanel.setPreferredSize 
            (getSavedDimension ("errorPane", new Dimension (600, 80)));

        // Create the pattern box
        pattern = new TCEditor ();
        undoPattern = new UndoManager ();
        pattern.getDocument ().addUndoableEditListener 
            (new UndoableEditListener () {
                public void undoableEditHappened (UndoableEditEvent e) {
                    undoPattern.addEdit (e.getEdit ());
                }
            });

        patternScrollPane = new JScrollPane (pattern);
        patternScrollPane.setMinimumSize (new Dimension (0, 0));

        JLabel patternLabel = new JLabel ("Pattern: ");
        int buttonHeight = patternLabel.getPreferredSize ().height;

        patternPanel =  (JPanel)
            Layout.vertical ()
            .setAlignment (Layout.WEST)
            .setContainerInsets (5, 5, 5, 5)
            .add (Layout.horizontal ()
                  .setAlignment (Layout.SOUTH)
                  .add (patternLabel)
                  .addGlue ()
                  .add (makeTinyButton ("Go",
                                        findAction,
                                        buttonHeight))
                  .add (makeTinyButton ("Clear",
                                        removeAllHighlightsAction,
                                        buttonHeight))
                  .done (),
                  Layout.FIXED_HEIGHT)
            .add (patternScrollPane)
            .add (patternMatchCount = new JLabel ("0 matches highlighted"))
            .done ();
        patternPanel.setMinimumSize (new Dimension (0, 0));
        patternPanel.setPreferredSize 
            (getSavedDimension ("patternPane", new Dimension (200, 150)));
        patternPanel.registerKeyboardAction 
            (findAction, 
             KeyStroke.getKeyStroke (KeyEvent.VK_ENTER, KeyEvent.CTRL_MASK),
             JComponent.WHEN_IN_FOCUSED_WINDOW);
        patternPanel.registerKeyboardAction 
            (removeAllHighlightsAction, 
             KeyStroke.getKeyStroke (KeyEvent.VK_ESCAPE, 0),
             JComponent.WHEN_IN_FOCUSED_WINDOW);


        // Create the label tree and its decorations
        tree = new LabelTree ();
        tree.addMouseListener (treeMouseListener);
        tree.addTreeExpansionListener (new TreeExpansionListener () {
            public void treeExpanded(TreeExpansionEvent event) {
                if (logExpansions)
                    Log.println ("expanded " + event.getPath ());
            }
            public void treeCollapsed(TreeExpansionEvent event) {
                if (logExpansions)
                    Log.println ("collapsed " + event.getPath ());
            }
        });

        treeScrollPane = new JScrollPane (tree);
        treeScrollPane.setMinimumSize (new Dimension (0, 0));
        
        treePanel = (JPanel) 
            Layout.vertical ()
            .setAlignment (Layout.WEST)
            .setContainerInsets (5, 5, 5, 5)
            .add (Layout.horizontal ()
                  .setAlignment (Layout.SOUTH)
                  .add (new JLabel ("Names:"))
                  .addGlue ()
                  .add (makeTinyButton ("Name...",
                                        labelAction,
                                        buttonHeight))
                  .done (),
                  Layout.FIXED_HEIGHT)
            .add (treeScrollPane)
            .done ();
        treePanel.setMinimumSize (new Dimension (0, 0));
        treePanel.setPreferredSize 
            (getSavedDimension ("treePane", new Dimension (200, 400)));
        treePanel.registerKeyboardAction 
            (labelAction, 
             KeyStroke.getKeyStroke ('M', KeyEvent.CTRL_MASK),
             JComponent.WHEN_IN_FOCUSED_WINDOW);

        // Create the splitters separating output, error, pattern, 
        // and tree 
        errorSplitter = new JSplitPane (JSplitPane.VERTICAL_SPLIT,
                                        outputPanel, errorPanel);
        errorSplitter.setOneTouchExpandable (true);

        treeSplitter = new JSplitPane (JSplitPane.VERTICAL_SPLIT,
                                        patternPanel, treePanel);
        treeSplitter.setOneTouchExpandable (true);

        midSplitter = new JSplitPane (JSplitPane.HORIZONTAL_SPLIT,
                                      errorSplitter, treeSplitter);
        midSplitter.setOneTouchExpandable (true);

        mainLayout.add (midSplitter);

        // Create the status panel
        progressBar = new BrowserProgressBar ();

        statusBar = new JLabel ("");

        feedbackButton = new JButton ("Feedback");
        feedbackButton.addActionListener (feedbackAction);

        mainLayout.add (Layout.horizontal ()
                        .setContainerInsets (1, 1, 1, 1)
                        .add (progressBar, Layout.FIXED_SIZE)
                        .add (statusBar, new Insets (0, 3, 0, 0))
                        .addGlue ()
                        .add (feedbackButton, Layout.FIXED_WIDTH)
                        .done (),
                        Layout.FIXED_HEIGHT);

        //System.err.println ("Packing window");
        pack ();

        // Activate output pane
        activatePane (outputPane, false);

        // Create document factory and Tcl interpreter (needs to be
        // after pack() call for some reason)
        factory = new SwingDocument.Factory ();
        factory.getProgressGenerator ().addProgressListener (progressBar);
//         factory.getProgressGenerator ().addProgressListener (new StreamProgressBar (System.err));

        // Register browse tool
        registerBrowserTools ();

        // Register self with default Toolbox, so that when user creates
        // new tools we can add them to our menus 
        //System.err.println ("Loading toolbox");
        Toolbox.getDefaultToolbox ().addBrowser (this);

        lapis.Main.browserCreated ();
        //System.err.println ("Done creating Browser");
    }

    public void show () {
        super.show ();

        Point p = getSavedPoint ("location", null);
        if (p != null)
            setLocation (p);
        else
            PopupDialog.centerWindow (this, null);
    }

    void saveDimension (String name, Dimension d) {
        Main.config.put (name, d.width + "x" + d.height);
    }

    Dimension getSavedDimension (String name, Dimension defaultDim) {
        String s = Main.config.getProperty (name);
        try {
            return new Dimension (Integer.parseInt (Str.before (s, "x")),
                                  Integer.parseInt (Str.after (s, "x")));
        } catch (Exception e) {
            return defaultDim;
        }
    }
        
    void savePoint (String name, Point pt) {
        Main.config.put (name, pt.x + "," + pt.y);
    }

    Point getSavedPoint (String name, Point defaultPt) {
        String s = Main.config.getProperty (name);
        try {
            return new Point (Integer.parseInt (Str.before (s, ",")),
                              Integer.parseInt (Str.after (s, ",")));
        } catch (Exception e) {
            return defaultPt;
        }
    }
        
    void saveConfiguration () {
        savePoint ("location", getLocation ());
        saveDimension ("outputPane", outputPanel.getSize ());
        saveDimension ("errorPane", errorPanel.getSize ());
        saveDimension ("patternPane", patternPanel.getSize ());
        saveDimension ("namesPane", treePanel.getSize ());
        Main.config.put ("errorSplitter", String.valueOf (getErrorSplitterLocations () [0]));
        Main.config.save ();
        
        Log.close ();
    }

    JButton makeTinyButton (String label, Action action, int height) {
        JButton button = new JButton (label);
        Font oldFont = button.getFont ();
        Font font = new Font (oldFont.getName (), 
                              oldFont.getStyle (),
                              oldFont.getSize () - 2);
        button.setFont (font);

        Dimension d = new Dimension (button.getPreferredSize ().width,
                                     height);
        button.setPreferredSize (d);
        button.setMaximumSize (d);

        button.addActionListener (action);

        String tooltip = (String) action.getValue (Action.SHORT_DESCRIPTION);
        if (tooltip != null)
            button.setToolTipText (tooltip);

        return button;
    }

    class BrowserProgressBar extends SwingProgressBar { 
        public BrowserProgressBar () {
            setStringPainted (false);
            Dimension size = BrowserProgressBar.this.getMinimumSize ();
            if (size.width < 100)
                setMinimumSize (new Dimension (100, size.height));
        }

        public void setString (String label) {
            if (label != null)
                setStatus (label + "...");
        }

        public void doneProgress (ProgressEvent event) {
            clearStatus (); 
        }

    }


    /*
     * Text field of Location and Find combo boxes.  Overrides JComboBox's standard
     * text field so that losing focus doesn't automatically send an Action event;
     * only pressing Enter does.
     */
    static class BrowserComboBoxEditor extends javax.swing.plaf.basic.BasicComboBoxEditor {
        public  BrowserComboBoxEditor () {
            super ();
            // make fonts big for demo purposes
//             Font f = editor.getFont ();
//             if (f != null)
//                 editor.setFont (new Font (f.getName (), Font.BOLD, 14));
        }
        public void focusGained(FocusEvent e) {
            selectAll ();
        }

        public void focusLost(FocusEvent e) {
            JTextField f = (JTextField)getEditorComponent ();
            Caret caret = f.getCaret ();
            caret.setDot (caret.getDot ());
        }
    }


    void registerBrowserTools () {
        Interpreter interp = factory.getInterpreter ();

        interp.registerTool ("view", new Tool () {
            public lapis.Document invoke (Arguments args) throws Exception {
                args.setUsage (
                               "Usage: View [options] <file/URL> ...\n"
                               + "    <file/URL>        Document(s) to print\n"
                               + "\n"
                               + "Options include:\n"
                               + "    -help           Display this message\n"
                               );
                while (args.hasMoreElements ()) {
                    String name = args.nextName ();
                    args.consume (name);
                }
                
                Enumeration inputs = args.getInputs ();
                lapis.Document doc = null;
                
                while (inputs.hasMoreElements ()) {
                    doc = (lapis.Document)inputs.nextElement ();
                    setDocument (doc);
                }
                return doc;
            }
        });

        interp.registerTool ("print", new Tool () {
            public lapis.Document invoke (Arguments args) throws Exception {
                args.setUsage (
                               "Usage: Print [options] <file/URL> ...\n"
                               + "    <file/URL>        Document(s) to print\n"
                               + "\n"
                               + "Options include:\n"
                               + "    -help           Display this message\n"
                               );
                while (args.hasMoreElements ()) {
                    String name = args.nextName ();
                    args.consume (name);
                }
                
                Enumeration inputs = args.getInputs ();
                lapis.Document doc = null;
                
                while (inputs.hasMoreElements ()) {
                    doc = (lapis.Document)inputs.nextElement ();
                    setDocument (doc);

                    String title = (String)doc.getProperty ("title");
                    if (title == null)
                        title = doc.toString ();
                    
                    BasicProgressGenerator gen = 
                        args.getFactory ().getProgressGenerator ();
                    gen.fireStartProgress ("Printing " + title + "...",
                                           10);
                    for (int i = 0; i < 10; ++i) {
                        gen.fireProgress (i);
                        Thread.sleep (100);
                    }
                    gen.fireDoneProgress ();
                }
                return doc;
            }
        });

    }

    ////////////////////
    //////////////////// Closing the browser
    ////////////////////

    public Action closeAction = new AcceleratedAction ("&Close", 'W', KeyEvent.CTRL_MASK) {
        public void actionPerformed (ActionEvent evt) {
            dispose ();
        }
    };

    public Action exitAction = new AcceleratedAction ("E&xit", 'Q', KeyEvent.CTRL_MASK) {
        public void actionPerformed (ActionEvent evt) {
            if (lapis.Main.getRunningBrowsers () == 1
                || JOptionPane.showConfirmDialog(Browser.this, 
                           "Close all windows and exit Lapis?", "Exit Lapis?", 
                           JOptionPane.YES_NO_OPTION)
                == JOptionPane.YES_OPTION) {
                saveConfiguration ();
                System.exit (0);
            }
        }
    };

    ////////////////////
    //////////////////// Debugging
    ////////////////////

    Action documentPropertiesAction = new AcceleratedAction ("&Document Info") {
        public void actionPerformed (ActionEvent evt) {
            SwingDocument doc = getDocument ();
            if (doc == null) {
                getToolkit ().beep ();
                return;
            }

            JDialog dlg = new JDialog (Browser.this, 
                                       "Document Info - " + doc.toString (), 
                                       false);
            Container contentPane = dlg.getContentPane ();
            Layout.border (contentPane)
                .add (new DocumentProperties (doc), Layout.CENTER);
            dlg.pack ();
            dlg.show ();
            PopupDialog.centerWindow (dlg, null);
        }
    };

    Action regionSetDisplayAction = new AcceleratedAction ("&Region Set") {
        public void actionPerformed (ActionEvent evt) {
            if (regionSetDisplay == null) {
                regionSetDisplay = lapis.trees.RegionSetDisplay.makeInFrame ();
                regionSetDisplay.setLocation (getSize ().width, 0);
                regionSetDisplay.setDocument (getDocument ());
                regionSetDisplay.setRegions (getRegions ());
            }
            regionSetDisplay.setVisible (true);
        }
    };

    Action highlightOptionAction = new AcceleratedAction ("&Highlight Options") {
        public void actionPerformed (ActionEvent evt) {
            ((RegionHighlighter) activePane.getHighlighter ())
                .showOptionDialog (Browser.this);
        }
    };

    ////////////////////
    //////////////////// Help & feedback
    ////////////////////

    Action feedbackAction = new AcceleratedAction ("&Feedback and Bug Reports") {
        public void actionPerformed (ActionEvent evt) {
            FeedbackForm.showFeedbackForm (Browser.this);
        }
    };

    ////////////////////
    //////////////////// Status bar and stop button
    ////////////////////

    public void setStatus (String message) {
        if (statusBar != null)
            statusBar.setText (message);
    }

    public void clearStatus () {
        setStatus (" ");
        progressBar.setValue (0);
    }

    int hourglassCount; // reference count on setHourglass(true)

    public void setHourglass (boolean enabled) {
        if (enabled)
            ++hourglassCount;
        else if (--hourglassCount > 0)
            return; // somebody still wants the hourglass showing
        else
            hourglassCount = 0;


        // Change the stop button
        stopAction.putValue (Action.SMALL_ICON, 
                             enabled ? stopIcon : graystopIcon);
    }

    public void clearHourglass () {
        hourglassCount = 0;
        setHourglass (false);
    }

    ////////////////////
    //////////////////// Evaluating Tcl commands
    ////////////////////

    /**
      * Evaluate a Tcl script (in the background).
      * @param script Tcl code to evaluate (may be multiple lines)
      */
    public void eval (final String script) {
        this.eval (script, true);
    }
        
    public void eval (final String script, final boolean annotateResult) {
        stop ();

        Log.println ("eval " + script);

        setStatus ("Evaluating command...");

        runStoppableActivity (new RunnableWithExceptions () {
            public void run () throws Exception {
                Interpreter interp = factory.getInterpreter ();
                
                // Evaluate the script and get its output 
                lapis.Document output = 
                    (lapis.Document) interp.eval (script, getDocument ());
                Thr.check ();
                
                if (annotateResult)
                    // Attach script to its output 
                    output.putProperty (SwingDocument.CommandProperty, 
                                        toOneLine (script));
                
                // Display output
                setDocument (output);
            }
        });
    }

    // Convert a possibly multi-line Tcl script to a single line.
    private static String toOneLine (String script) {
        return Str.replace (script.trim (), "\n", " ; ");
    }

    ////////////////////
    //////////////////// Getting browser state
    ////////////////////

    /**
     * Get the current document.
     */
    public SwingDocument getDocument () {
        return (SwingDocument)activePane.getDocument ();
    }

    /**
     * Get the browser's document factory.
     */
    public DocumentFactory getFactory () {
        return factory;
    }

    /**
     * Get command that generated browser contents.
     * @returns command (actually a URL, filename, or command), 
     * or null if browser is showing nothing.
     */
    public String getCommand () {
        SwingDocument doc = getDocument ();
        return (doc != null) 
            ? (String)doc.getProperty (SwingDocument.CommandProperty) 
            : null;
    }

    /**
     * Get browser history.
     */
    public History getHistory () {
        return (History)location.getModel ();
    }

    ////////////////////
    //////////////////// Visiting new pages in browser
    ////////////////////


    /**
     * Change the current document to a Document object.
     */
    public void setDocument (lapis.Document newdoc) {
        // Convert to a SwingDocument if necessary
        final SwingDocument doc = convertToSwingDocument (newdoc);

        Log.println ("show " + doc);

        // Prepare to change current document
        loadingDocument (doc);

        
        // Load output document
        setStatus ("Loading " + doc + "...");
//         runStoppableActivity (new RunnableWithExceptions () {
//             public void run () {
                outputPane.setDocument (doc);

                // Done changing document
                loadedDocument (doc);

                clearStatus ();
                setStatus ("Document loaded.");
//             }
//         });

        // Load error document (if any)
        final lapis.Document error =
            (lapis.Document) doc.getProperty (SwingDocument.ErrorProperty);
        if (error != null) {
//             runStoppableActivity (new RunnableWithExceptions () {
//                 public void run () {
                    errorPane.setDocument (convertToSwingDocument (error));
//                 }
//             });
        }
    }

    private SwingDocument convertToSwingDocument (lapis.Document doc) {
        if (doc instanceof SwingDocument)
            return (SwingDocument)doc;

        SwingDocument sdoc = (SwingDocument) factory.make (doc);
        factory.getInterpreter ().setVar (Interpreter.DOCUMENT_VAR, 
                                          sdoc);
        return sdoc;
    }

    /**
     * Called when a document is about to be loaded.  The document has not
     * necessarily
     * been fully downloaded yet, so its content is unavailable, but its
     * URL and content type are available.  Default behavior adds
     * the document to the browsing history and puts its URL in the Location
     * text box and its content type in the View drop-down list.
     */
    protected void loadingDocument (SwingDocument doc) {
        Thr.check ();

        // Put new doc in the history, if necessary
        try {
            blockHistoryAction = true;
            History history = getHistory ();
            if (!history.contains (doc))
                history.put (doc);
        } finally {
            blockHistoryAction = false;
        }

        // Update location box
        location.getEditor ().setItem (doc);
        location.getEditor ().selectAll ();

        // Update content-type
        updateContentType (doc);

        // Update title bar (no TitleProperty available yet, but URLProperty
        // is)
        updateTitle (doc);

        // Clear the highlighted region set
        setRegions (null, null);

        // Update debugging display
        if (regionSetDisplay != null)
            regionSetDisplay.setDocument (doc);
        
        // Activate browser pane
        activatePane (outputPane,
                      doc.getProperty (SwingDocument.ErrorProperty) != null);
    }

    /**
     * Called after the current document has been fully loaded.
     */
    protected void loadedDocument (SwingDocument doc) {
        Thr.check ();

        // Update title bar -- TitleProperty (if any) is available now
        // since document is loaded.
        updateTitle (doc);

        // Update label tree
        updateLabels ();
    }

    private void updateTitle (SwingDocument doc) {
        String title = 
            (String) doc.getProperty (SwingDocument.TitleProperty);
        if (title == null)
           title = doc.toString ();
        setTitle ((title != null) ? "Lapis - " + title : "Lapis");
    }

    ////////////////////
    //////////////////// Switching between browser and error pane
    ////////////////////

    private FocusListener paneFocusListener = new FocusAdapter () {
        public void focusGained (FocusEvent event) {
            selectPane ((BrowserPane)event.getSource ());
        }
    };

    private void selectPane (BrowserPane pane) {
        if (pane == activePane)
            return;

        Log.println ("select " + ((pane == outputPane) ? "output" : "error"));

        // Clear the old pane's highlighting
        setRegions (null, null);
        
        // Activate the new pane
        activatePane (pane, errorShowing);
        
        // Update the UI to match new pane
        updateContentType (getDocument ());
        updateLabels ();
    }

    boolean errorShowing = true;
    Color inactivePaneColor;

    private void activatePane (BrowserPane pane, boolean showErrorPane) {
        if (inactivePaneColor == null) {
            // first time a pane has been activated.
            // save the default background color to
            // use for inactive panes
            inactivePaneColor = outputPanel.getBackground ();
        }

        // show or hide error panel, adjusting error splitter accordingly
        int[] locations = getErrorSplitterLocations ();

        outputLabel.setVisible (showErrorPane);
        errorPanel.setVisible (showErrorPane);

        errorSplitter.setDividerLocation (locations[showErrorPane ? 0 : 1]);
        errorSplitter.setLastDividerLocation (locations[0]);

        // Set the panel's background color (affecting only the 
        // panel's label) to indicate which panel is activated
        outputPanel.setBackground ((pane == outputPane)
                                   ? ACTIVE_PANE_COLOR
                                   : inactivePaneColor);
        errorPanel.setBackground ((pane == errorPane)
                                      ? ACTIVE_PANE_COLOR
                                      : inactivePaneColor);

        activePane = pane;
        errorShowing = showErrorPane;
    }

    int[] getErrorSplitterLocations () {
        int bottom = errorSplitter.getHeight ()
            - errorSplitter.getInsets ().bottom
            - errorSplitter.getDividerSize ();

        int middle = errorSplitter.getDividerLocation ();

        if (Math.abs (middle - bottom) < 15)
            middle = errorSplitter.getLastDividerLocation();

        if (Math.abs (middle - bottom) < 15) {
            String loc = Main.config.getProperty ("errorSplitter");
            if (loc != null)
                middle = Integer.parseInt (loc);
        }

        errorSplitter.setLastDividerLocation (middle);

        //System.err.println ("middle = " + middle + ", bottom = " + bottom);

        return new int[] { middle, bottom };
    }

    ////////////////////
    //////////////////// Invoking URLs (by typing, from history, or by hyperlink)
    ////////////////////

    /**
     * Action invoked when user edits the URL in the Location box and leaves
     * the URL box (by pressing Enter or clicking somewhere else).
     */
    public Action locationAction = new AbstractAction () {
        public void actionPerformed (ActionEvent evt) {
            Object item = location.getEditor ().getItem ();
            //System.err.println ("Goto " + item);
            if (item instanceof String)
                eval (item.toString ());
            
            location.getEditor ().selectAll ();
        }
    };

    /**
     * Action invoked when user selects a document from the history drop-down list.
     */
    public Action historyAction = new AbstractAction () {
        public void actionPerformed (ActionEvent evt) {
            if (blockHistoryAction)
                return;

            SwingDocument doc = (SwingDocument)location.getSelectedItem ();
            if (doc == null)
                return;

            setDocument (doc);
        }
    };

    boolean blockHistoryAction = false;


    /**
     * Listener invoked when user clicks on a hyperlink in an HTML document.
     */
    public HyperlinkListener hyperlinkListener = new HyperlinkListener () {
        public void hyperlinkUpdate(HyperlinkEvent e) {
            HyperlinkEvent.EventType type = e.getEventType();

            URL url = e.getURL ();
            if (url == null)
                return;

            if (url.toString ().startsWith ("file://localhost/"))
                url = URLUtil.FileToURL (new File (url.toString ().substring (16)));

            if (type == HyperlinkEvent.EventType.ACTIVATED) {
                eval (url.toString ());
            }
            else if (type == HyperlinkEvent.EventType.ENTERED) {
                // FIX: ENTERED and EXITED events aren't sent by current Swing
                setStatus (url.toString ());
                ((Component)e.getSource ())
                    .setCursor (Cursor.getPredefinedCursor (Cursor.HAND_CURSOR));
            }
            else if (type == HyperlinkEvent.EventType.EXITED) {
                setStatus (" ");
                ((Component)e.getSource ())
                    .setCursor (Cursor.getDefaultCursor ());
            }
        }
    };

    /**
     * Action invoked by Back command.  Move back to previous document in history.
     */
    public Action backAction = new AcceleratedAction 
        ("&Back", 
         new ImageIcon (System.getProperty ("lapis.dir", ".") 
                        + "/images/back.gif"),
         KeyEvent.VK_LEFT, KeyEvent.ALT_MASK,
         "Go back to previous page in history") {
        public void actionPerformed (ActionEvent evt) {
            History history = getHistory ();
            SwingDocument doc = (SwingDocument)history.back ();
            if (doc == null)
                getToolkit().beep ();
            else {
                stop ();
                setDocument (doc);
            }
        }
    };

    /**
     * Action invoked by Forward command.  Moves forward to next document in history.
     */
    public Action forwardAction = new AcceleratedAction 
        ("&Forward", 
         new ImageIcon (System.getProperty ("lapis.dir", ".") 
                        + "/images/fwd.gif"),
         KeyEvent.VK_RIGHT, KeyEvent.ALT_MASK,
         "Go forward to next page in history") {
        public void actionPerformed (ActionEvent evt) {
            History history = getHistory ();
            SwingDocument doc = (SwingDocument)history.forward ();
            if (doc == null)
                getToolkit().beep ();
            else {
                stop ();
                setDocument (doc);
            }
        }
    };

    /**
     * Action invoked by Reload command.  Reloads and reparses the current
     * page.
     */
    public Action reloadAction = new AcceleratedAction 
        ("&Reload", 
         new ImageIcon (System.getProperty ("lapis.dir", ".") 
                        + "/images/reload.gif"),
         'R', KeyEvent.CTRL_MASK,
         "Reload the current page") {
        public void actionPerformed (ActionEvent evt) {
            String command = getCommand ();
            if (command != null)
                eval (command.toString ());
        }
    };

    ////////////////////
    //////////////////// Running and stopping background activities
    ////////////////////

    Vector backgroundThreads = new Vector ();

    public synchronized void runStoppableActivity (RunnableWithExceptions r) {
        new BackgroundThread (r)
            .start ();
    }

    private interface RunnableWithExceptions {
        public void run () throws Exception;
    }

    private class BackgroundThread extends Thread {
        RunnableWithExceptions r;

        public BackgroundThread (RunnableWithExceptions r) {
            this.r = r;
            setPriority (BACKGROUND_PRIORITY);
            backgroundThreads.addElement (this);
        }

        public void run () {
            try {
                setHourglass (true);
                r.run ();
                setHourglass (false);
            } catch (final Exception e) {
                SwingUtilities.invokeLater (new Runnable () {
                    public void run () {
                        FeedbackForm.showExceptionDialog (Browser.this, e);
                        clearHourglass ();
                        clearStatus ();
                    }
                });
            }
        }
    }


    /**
     * Action invoked by Stop command.  Stops downloading or parsing.
     */
    ImageIcon stopIcon = new ImageIcon (System.getProperty ("lapis.dir", ".")
                                        + "/images/stop.gif");
    ImageIcon graystopIcon = new ImageIcon (System.getProperty ("lapis.dir", ".")
                                            + "/images/gray-stop.gif");
    public Action stopAction = new AcceleratedAction 
        ("&Stop", 
         graystopIcon,
         "Stop the current page load") {
        public void actionPerformed (ActionEvent evt) {
            stop ();
            SwingUtilities.invokeLater (new Runnable () {
                public void run () {
                    setStatus ("Stopped.");
                }
            });
        }
    };

    public synchronized void stop () {
        // Interrupt all background threads, forcing I/O operations to throw
        // errors and Thr.check() to throw ThreadDeath
        for (Enumeration e = backgroundThreads.elements ();
             e.hasMoreElements (); )
            ((Thread)e.nextElement ()).interrupt ();

        backgroundThreads.removeAllElements ();

        // Reset progress UI 
        clearHourglass ();
        clearStatus ();
    }


    ////////////////////
    //////////////////// File operations (open and save)
    ////////////////////

    /**
     * Action invoked when user selects File/New Browser.
     * Creates a new Browser window.
     */
    public Action newBrowserAction = new AcceleratedAction ("&New Browser", 'N', KeyEvent.CTRL_MASK) {
        public void actionPerformed (ActionEvent evt) {
            Browser b = new Browser ();

            // Copy history to new browser window
            History h = new History (getHistory ());
            b.location.setModel (h);
            Object d = h.get ();
            if (d != null)
                b.setDocument ((SwingDocument)d);
            
            // Show new browser window offset 50,50 from this window
            b.show ();
            Point p = getLocation ();
            p.translate (50, 50);
            b.setLocation (p);
        }
    };

    /**
     * Action invoked when user selects File/Open.
     * Pops up a file-open dialog to open a file into the browser.
     */
    public Action openAction = new AcceleratedAction ("&Open...", 'O', KeyEvent.CTRL_MASK) {
        public void actionPerformed (ActionEvent evt) {
            FileChooser chooser = new FileChooser ();
            if (chooser.showOpenDialog (Browser.this) == 
                JFileChooser.APPROVE_OPTION)
                eval (chooser.getSelectedFile ().toString ());
        }
    };

    /**
     * Action invoked when user selects File/Save.
     * Pops up a save-as dialog box, then saves the browser contents.
     */
    public Action saveAction = new AcceleratedAction ("&Save As...", 'S', KeyEvent.CTRL_MASK) {
        public void actionPerformed (ActionEvent evt) {
            FileChooser chooser = new FileChooser ();
            File filename = getFile ();
            if (filename != null)
                chooser.setSelectedFile (filename);

            if (chooser.showSaveDialog (Browser.this) != JFileChooser.APPROVE_OPTION)
                return;

            File f = chooser.getSelectedFile ();

            if (f.exists ()) 
                switch (JOptionPane.showConfirmDialog (Browser.this, 
                                                  f + " already exists. Overwrite?\n",
                                                  "Overwrite?", 
                                                       JOptionPane.YES_NO_CANCEL_OPTION)) {
                case JOptionPane.YES_OPTION:
                    break;
                case JOptionPane.NO_OPTION:
                    // Pop up dialog box again
                    actionPerformed (evt);
                    return;
                case JOptionPane.CANCEL_OPTION:
                    return;
                }

            save (f);
        }
    };

    /**
     * Save browser contents to a local file.
     * @param f Local file to save
     */
    public void save (File f) {
        try {
            PrintWriter out = new PrintWriter (new FileWriter (f));
            out.print (getDocument ().getContentText ());
            out.close ();
        } catch (Exception e) {
            FeedbackForm.showExceptionDialog (Browser.this, e);
        }
    }

    /**
     * Get local filename corresponding to browser contents.
     * @returns local filename, or null if browser is showing nothing or
     * a remote page.
     */
    public File getFile () {
        SwingDocument doc = getDocument ();
        if (doc == null)
            return null;

        URL url = (URL)doc.getProperty (SwingDocument.URLProperty);
        if (url == null)
            return null;

        try {
            return URLUtil.URLToFile (url);
        } catch (MalformedURLException e) {
            return null;
        }
    }



    ////////////////////
    //////////////////// edit operations (cut, copy, paste, etc)
    ////////////////////

    public Action cutAction = new KeystrokeAction ("Cu&t", 'X', KeyEvent.CTRL_MASK);
    public Action copyAction = new KeystrokeAction ("&Copy", 'C', KeyEvent.CTRL_MASK);
    public Action pasteAction = new KeystrokeAction ("&Paste", 'V', KeyEvent.CTRL_MASK);
    public Action selectAllAction = new KeystrokeAction ("Select &All", 'A', KeyEvent.CTRL_MASK);

    class KeystrokeAction extends AcceleratedAction {
        public KeystrokeAction (String name, int keycode, int modifiers) {
            super (name, keycode, modifiers);
        }

        public void actionPerformed (ActionEvent event) {
            Keymap keymap = outputPane.getKeymap ();
            if (keymap == null) {
                Toolkit.getDefaultToolkit ().beep ();
                return;
            }

            KeyStroke keystroke = (KeyStroke) getValue (ACCELERATOR);
            Action action = keymap.getAction (keystroke);
            if (action == null) {
                Toolkit.getDefaultToolkit ().beep ();
                return;
            }

            action.actionPerformed (event);            
        }
    }

    /**
     * Action invoked by Undo command.  Undoes one editing action.
     */
    public Action undoAction = 
        new AcceleratedAction 
        ("&Undo", 'Z', KeyEvent.CTRL_MASK,
         new javax.swing.text.TextAction ("Undo") {
            public void actionPerformed (ActionEvent evt) {
                if (getTextComponent (evt) == pattern) {
                    try {
                        undoPattern.undo ();
                    } catch (CannotUndoException e) {
                        getToolkit ().beep ();
                    }
                }
            }
        });


    /**
      * Override copy action (for ALL JTextComponents) to mark 
      * which JTextComponent generated the clipboard contents.
      */
    public Action copyKeystrokeAction = new DefaultEditorKit.CopyAction () {
        public void actionPerformed(ActionEvent event) {
            JTextComponent target = getTextComponent(event);
            if (! (target instanceof BrowserPane)) {
                super.actionPerformed (event);
                return;
            }

            Clipboard clipboard = 
                Toolkit.getDefaultToolkit().getSystemClipboard();
            BrowserPane br = (BrowserPane) target;
            javax.swing.text.Document doc = 
                (javax.swing.text.Document)target.getDocument();
            RegionHighlighter h = (RegionHighlighter) br.getHighlighter ();
            RegionSet s = h.getRegions ();
            if (s != null) {
                if (doc instanceof CookedDocument)
                    s = s.convert (((CookedDocument)doc).getCoordinateMap ());
                s = s.meld ();
                RegionEnumeration re = s.regions ();
                StringBuffer result = new StringBuffer ();
                for (Interval r = (Interval) re.first (); 
                     r != null; 
                     r = (Interval) re.next ()) {
                    try {
                        result.append (doc.getText(r.start, r.end - r.start));
                    } catch (BadLocationException e) {
                    }
                }

                StringSelection contents = 
                    new StringSelection (result.toString ());
                clipboard.setContents(contents, contents);
            }
        }
    };

    ////////////////////
    //////////////////// Changing the view (text, html, etc.)
    ////////////////////

    static class ViewType {
        public String uiName;       // name for UI purposes (e.g. "Text")
        public String contentType;  // content type (e.g. "text/plain")

        public ViewType (String uiName, String contentType) {
            this.uiName = uiName;
            this.contentType = contentType;
        }

        public String toString () {
            return uiName;
        }
    }

    private void updateContentType (SwingDocument doc) {
        try {
            blockContentTypeAction = true;
            setViewContentType (view, 
                                (String) doc.getProperty 
                                   (SwingDocument.MIMEProperty));
        } finally {
            blockContentTypeAction = false;
        }
    }

    /**
     * Action invoked when user selects a different view from the drop-down list.
     */
    boolean blockContentTypeAction = false;

    public Action changeViewAction = new AbstractAction () {
        public void actionPerformed (ActionEvent evt) {
            if (blockContentTypeAction)
                return;

            ViewType vt = (ViewType) view.getSelectedItem ();
            if (vt == null)
                return;

            changeContentType (vt.contentType);
        }
    };

    public void changeContentType (String contentType) {
        SwingDocument doc = getDocument ();
        if (doc == null)
            return;

        // Check whether content type is actually changing
        String oldContentType = 
            (String) doc.getProperty (SwingDocument.MIMEProperty);
        if (contentType.equals (oldContentType))
            return;

        // Re-create document with desired content type
        SwingDocument olddoc = doc;
        doc.putProperty (SwingDocument.MIMEProperty, contentType);
        doc = (SwingDocument) factory.make (olddoc);
        olddoc.putProperty (SwingDocument.MIMEProperty, contentType);

        // Replace document in history.
        // Use block flag to block its action listener
        // from firing, thereby avoiding multiple calls to setDocument().
        try {
            blockHistoryAction = true;
            (getHistory ()).replace (doc);
        } finally {
            blockHistoryAction = false;
        }

        // Hang on the highlighting
        RegionSet highlight = getRegions ();
        String highlightName = getRegionSetName ();

        // Set the converted document
        setDocument (doc);

        // Set the background appropriate to this content type
        new lapis.parsers.BackgroundParser ()
            .parse (doc);

        // Restore the original highlighting
        setRegions (highlightName, highlight);
    }

    /*
     * Set the view drop-down list to match a loaded document's content type.
     */
    static void setViewContentType (JComboBox view, String contentType) {
        int selection = 0;  // use Text as default if we can't find contentType

        try {
            ComboBoxModel model = view.getModel ();
            int n = model.getSize ();
            for (int i = 0; i < n; ++i) {
                ViewType vt = (ViewType)model.getElementAt (i);
                if (vt.contentType.equals (contentType)) {
                    selection = i;
                    break;
                }
            }
        } catch (NullPointerException e) {
        }

        view.setSelectedIndex (selection);
    }


    ////////////////////
    //////////////////// Parsing the current document
    ////////////////////

    /**
     * Parse the current document (in the background) using 
     * a specified parser.
     */
    public void parse (final Parser parser) {
        runStoppableActivity (new RunnableWithExceptions () {
            public void run () {
                SwingDocument doc = getDocument ();
                if (doc == null)
                    return;
                parser.parse (doc);
                updateLabels ();
            }
        });
    }

    ////////////////////
    //////////////////// Searching for patterns
    ////////////////////


    /**
     * Action invoked when user types something in the Find text box and presses Enter,
     * or selects a pattern from the pattern history drop-down.
     * Searches for the text constraint pattern and highlights the matching regions.
     */
    public Action findAction = new AcceleratedAction 
        ("Go", KeyEvent.VK_ENTER, KeyEvent.CTRL_MASK,
         "Highlight all matches to the pattern [Ctrl-Enter]") {
        // keep this action from firing recursively
        public boolean blockFindAction = false;

        public void actionPerformed (ActionEvent evt) {
            try {   // finally
                if (blockFindAction)
                    return;
                blockFindAction = true;

//                 // get pattern expression
//                 ComboBoxEditor ed = pattern.getEditor ();
//                 String expr = ed.getItem ().toString ();
//                 if (expr.length () == 0)
//                     return;

//                 // add it to pattern history
//                 History history = (History) pattern.getModel ();
//                 if (!history.contains (expr))
//                     history.add (expr);

                Log.println ("find pattern:\n" + pattern.getText ().trim ());
 
                // compile pattern into a TC structure
                final TC system = 
                    makeTCFromTextComponent (pattern);
                
                // evaluate pattern in background
                runStoppableActivity (new RunnableWithExceptions () {
                    public void run () {
                        SwingDocument doc = getDocument ();
                        if (doc == null)
                            return;

                        final TCParseException warnings = new TCParseException ();

                        RegionSet result = system.evaluate (doc, warnings);

                        updateLabels ();

                        String canonicalLabel = null;
                        String label = system.getLabel ();
                        if (label != null) {
                            Labeling labels = doc.getLabeling ();
                            try {
                                canonicalLabel = labels.lookup (label);
                            } catch (Labeling.LookupException e) {
                            }
                        }

                        setRegions (canonicalLabel, result);

                        if (!warnings.isEmpty ())
                            SwingUtilities.invokeLater (new Runnable () {
                                public void run () {
                                    FeedbackForm.showMessageDialog 
                                        (Browser.this, 
                                         warnings.getMessage (),
                                         "Warning", 
                                         JOptionPane.ERROR_MESSAGE);
                                }
                            });
                    }
                });
                
            } catch (TCParseException e) {
                Log.println ("parse errors: " + e.getMessage ());

                FeedbackForm.showMessageDialog (Browser.this, 
                                                e.errors ().nextElement ().toString (), 
                                                "Parse Error",
                                                JOptionPane.ERROR_MESSAGE);
            } finally {
                blockFindAction = false;

                pattern.requestFocus ();
            }
        }
    };


    /**
     * Action invoked when user selects an earlier pattern from the Find drop-down list.
     */
//     public ItemListener findHistoryListener = new ItemListener () {
//         public void itemStateChanged(ItemEvent e) {
//             Object item = pattern.getSelectedItem ();
//             if (item == null)
//                 item = "";                
//             pattern.getEditor ().setItem (item);
//         }
//     };

    public static TC makeTCFromTextComponent (JTextComponent textbox) throws TCParseException {
        String text = textbox.getText ();
        try {
            return new TC (text);
        } catch (TCParseException e) {
            // Select the error, then re-throw the exception
            selectParseError (textbox, e);
            e.setStringContext (text);
            throw e;
        }
    }
    
    public static void selectParseError (JTextComponent textbox, 
                                         TCParseException e) {
        int len = textbox.getText ().length ();
        int start = Math.max (0, Math.min (len, e.getStart ()));
        int end = Math.max (0, Math.min (len, e.getEnd ()));
        textbox.select (start, end);
        if (start == end) {
            // insert a marker to make error position visible
            textbox.replaceSelection ("???");
            textbox.select (start, end+3);
        }
        textbox.requestFocus ();        
    }

    /**
     * Append a name to the pattern box (usually because it was selected from
     * a popup menu or the label tree).
     */
     /*
    protected void putNameInPattern (Label name) {
        ComboBoxEditor editor = pattern.getEditor ();
        String item = (String)editor.getItem ();
        if (item.length () != 0)
            item += ' ';
        item += name;
        editor.setItem (item);
    }
    */

    /**
     * Action invoked when user clicks on Name... button.
     * Pops up a dialog to assign a label to current highlight.
     */
    public Action labelAction = new AcceleratedAction 
        ("Name...",
         'M', KeyEvent.CTRL_MASK,
         "Give a name to the current highlight [Ctrl-M]") {
        public void actionPerformed (ActionEvent evt) {
            String name = promptForName (highlightedName);
            if (name == null)
                return;

            Log.println ("named " + name);

            Labeling labels = getDocument ().getLabeling ();
            boolean nameExists = labels.contains (name);
            labels.put (name, getRegions ());
            if (!nameExists)
                updateLabels ();
            setRegions (name, null);
        }
    };


    /**
     * Action invoked when user clicks on Remove Highlight on context menu.
     * Unhighlights the region under the cursor.
     */
    public Action removeHighlightAction = new AcceleratedAction ("Remove Highlight") {
        public void actionPerformed (ActionEvent evt) {
            if (clickedRegion == null)
                return;

            Log.println ("unhighlight " + clickedRegion);
            setRegions (null,
                        highlightedRegions.copy ()
                        .delete (clickedRegion));
        }
    };


    /**
     * Action invoked when user clicks on Clear button.
     * Clears the highlight.
     */
    public Action removeAllHighlightsAction = new AcceleratedAction 
        ("Remove All Highlights",
         KeyEvent.VK_ESCAPE, 0,
         "Clear all highlights and clear the Pattern box [Esc]") {
        public void actionPerformed (ActionEvent evt) {
            Log.println ("clear");
            setRegions (null, null);
            pattern.requestFocus ();
        }
    };


    /**
     * Action invoked when user clicks on TC Editor button.
     * Pops up a new TCFileEditor window.
     */
    public Action editorAction = new AcceleratedAction ("Pattern &Editor") {
        public void actionPerformed (ActionEvent evt) {
            showTCFileEditor ();
        }
    };

    public void showTCFileEditor () {
        if (tcEditor == null) {
            tcEditor = new TCFileEditor (Browser.this);
            tcEditor.show ();
            PopupDialog.centerWindow (tcEditor, Browser.this);
        } else
            tcEditor.show ();
    }
    
    /**
     * Action invoked when user selects Structure/Run Parser menu command.
     * Pops up a dialog box for parser name.
     */
    public Action parserAction = new AcceleratedAction ("&Run Parser...") {
        public void actionPerformed (ActionEvent evt) {
            ParserDialog dialog = new ParserDialog (Browser.this);
            dialog.show ();
            
            Parser parser = dialog.getParser ();
            if (parser == null)
                return;
                
            parse (parser);
        }
    };
    
    class ParserDialog extends OkCancelDialog {
        
        JTextField name;
        JButton browseButton;
        
        private Parser parser;  // created by OK button; null if dialog canceled
        
        public ParserDialog (Frame owner) {
            super (owner, "Run Parser");
            
            Layout.gridbag (getControlsPane ())
                .setAlignment (Layout.NORTHWEST)
                .add (new JLabel ("Parser (filename, URL or Java class): "),
                      Layout.FIXED_SIZE)
                .nextRow ()
                .add (name = new JTextField ())
                .add (browseButton = new JButton ("Browse..."), 
                      Layout.FIXED_SIZE);

            name.addActionListener (new ActionListener () {
                public void actionPerformed (ActionEvent event) {
                    ok ();
                }
            });
            
            browseButton.addActionListener (new ActionListener () {
                public void actionPerformed (ActionEvent event) {
                    FileChooser chooser = new FileChooser ();
                    if (chooser.showOpenDialog (ParserDialog.this) 
                        == JFileChooser.APPROVE_OPTION)
                        name.setText (chooser.getSelectedFile ().toString ());
                }
            });
            
            pack ();
        }
        
        public void ok () {
            try {
                parser = ParserGroup.loadParser (name.getText ());
            } catch (Exception e) {
                FeedbackForm.showExceptionDialog (Browser.this, e);
            }
            super.ok ();
        }
        
        public Parser getParser () {
            return parser;
        }
    }


    ////////////////////
    //////////////////// Refreshing the label tree
    ////////////////////

    boolean logExpansions = true;

    protected void updateLabels () {
        try {
            logExpansions = false;
            tree.setLabeling (getDocument ().getLabeling ());
        } finally {
            logExpansions = true;
        }
   }

    ////////////////////
    //////////////////// The highlighted region set
    ////////////////////

    /**
     * Set the highlighted region set with its name.  If name is null,
     * assumes region set is anonymous.  If r is null, looks up name
     * in document's labeling.  If both name and r are null, uses
     * EMPTY_SET.
     */
    public void setRegions (String name, RegionSet r) {
        if (r == null) {
            if (name != null) {
                Labeling labels = getDocument ().getLabeling ();

                // Lookup name 
                r = labels.get (name);

                // Display (abbreviated) name in pattern box
                pattern.setText (labels.getShortestName (name).toString ());
            } else {
                pattern.setText ("");
            }
        }

        highlightedRegions = r;
        highlightedName = name;

        changedRegions ();
    }

    /**
     * Called when the highlighted region set changes.
     */
    protected void changedRegions () {
        // Record the pattern used to generate the highlighted regions
        highlightedPattern = (highlightedRegions != null) 
            ? pattern.getText ()
            : null;

        // Highlight the region set name (if any) in the label tree
        try {
            logExpansions = false;
            tree.setSelectedLabel (highlightedName);
        } finally {
            logExpansions = true;
        }

        // Flatten the region set
        highlightedFlatRegions = (highlightedRegions != null) ? highlightedRegions.flatten () : null;

        // Highlight the matches in the browser pane
        activePane.setRegions (highlightedRegions);

        // Reset search
        resetFind ();

        // Display number of matches in status bar
        int n = (highlightedRegions != null)
            ? highlightedRegions.getSize ()
            : 0;
        if (((RegionHighlighter)activePane.getHighlighter ()).getFeedback ()
            != null)
            ++n;
        patternMatchCount.setText ( n + " match" + ((n == 1) ? "" : "es")
                                    + " highlighted");

        // Update debugging display
        if (regionSetDisplay != null)
            regionSetDisplay.setRegions (highlightedRegions);

//         // Scroll to first match
//         if (highlightedFlatRegions != null) {
//             Region firstMatch = highlightedFlatRegions.regions ().first ();
//             if (firstMatch != null)
//                 activePane.scrollRegionToVisible (firstMatch);
//         }
    }

    /**
     * Get the highlighted region set.
     */
    public RegionSet getRegions () {
        return highlightedRegions;
    }

    /**
     * Get the label of the highlighted region set (or null if not labeled).
     */
    public String getRegionSetName () {
        return highlightedName;
    }
    
    /**
     * Step to next match in highlighted region set.  Starts from current selection
     * or cursor position.
     */
    public Action nextMatchAction = new AcceleratedAction ("&Next Match", KeyEvent.VK_F3, 0) {
        public void actionPerformed (ActionEvent event) {
            findNext (true);
        }
    };

    public void resetFind () {
        isSearching = false;
    }

    public void findNext (boolean beepIfNone) {
        if (highlightedRegions == null) {
            if (beepIfNone)
                getToolkit().beep ();
            return;
        }
        
        // Find start point of search.
        Interval selection = null;    // Document position to start searching from.
        
        if (isSearching)
            // Next Match has been invoked since the highlighted
            // region set last changed.
            // Start after current selection.
            selection = activePane.getSelectedRegion ();
        else
            isSearching = true;
        
        if (selection == null)
            selection = new Interval (0, 0);
        
        // Find first highlighted region after selection
        Region r = highlightedFlatRegions.regions ((Region)selection.after ()).first ();
        if (r == null){
            if (beepIfNone)
                getToolkit().beep ();
        }
        else {
            // Select it
            activePane.selectRegion (r);
            activePane.requestFocus ();
        }
    }

    /**
     * Step to previous match in highlighted region set.  Looks backward from current
     * selection or cursor position.
     */
    public Action previousMatchAction = new AcceleratedAction ("&Previous Match", KeyEvent.VK_F3, KeyEvent.SHIFT_MASK) {
        public void actionPerformed (ActionEvent event) {
            // Fix: can't support this until we can enumerate backwards
            getToolkit().beep ();
        }
    };


    ////////////////////
    //////////////////// Naming regions manually
    ////////////////////

    /**
     * Action invoked by Label command.  Adds selected region to current region set.
     */
    public Action nameAction = new AcceleratedAction ("&Label", 'L', KeyEvent.CTRL_MASK) {
        public void actionPerformed (ActionEvent evt) {
            Interval r = activePane.getSelectedRegion ();
            if (r.start == r.end)
                return; // ignore 0-length selections

            if (highlightedName != null)
                nameRegion (highlightedName, r);
            else
                nameAsAction.actionPerformed (evt);
        }
    };

    /**
     * Action invoked by Label As command.  Pops up a dialog to give
     * selection region an arbitrary name.
     */
    public Action nameAsAction = new AcceleratedAction ("Label &As...") {
        public void actionPerformed (ActionEvent evt) {
            Interval r = activePane.getSelectedRegion ();
            if (r.start == r.end)
                return; // ignore 0-length selections

            String name = promptForName (highlightedName);
            activePane.selectRegion (r);
            if (name != null)
                nameRegion (name, r);
        }
    };

    /**
     * Action invoked by Unlabel command.  
     * Removes selected region from current region set.
     */
    public Action unnameAction = new AcceleratedAction ("&Unlabel", 'U', KeyEvent.CTRL_MASK) {
        public void actionPerformed (ActionEvent evt) {
            Interval r = activePane.getSelectedRegion ();
            if (r.start == r.end)
                return; // ignore 0-length selections

            if (highlightedName != null)
                unnameRegion (highlightedName, r);
        }
    };


    /**
     * Action invoked by Delete Label command.  Deletes selected label
     * from labeling.
     */
    public Action deleteLabelAction = new AcceleratedAction ("&Delete Label", 'D', KeyEvent.CTRL_MASK) {
        public void actionPerformed (ActionEvent evt) {
            if (highlightedName != null) {
                Labeling labels = getDocument ().getLabeling ();
                labels.remove (highlightedName);
                setRegions (null, null);
                updateLabels ();
            }
        }
    };

    void nameRegion (String name, Region r) {
        Labeling labels = getDocument ().getLabeling ();
        boolean nameExists = labels.contains (name);
        labels.add (name, r);
        setRegions (name, null);
        if (!nameExists)
            updateLabels ();
    }

    void unnameRegion (String name, Region r) {
        Labeling labels = getDocument ().getLabeling ();
        RegionSet s = labels.get (name);

        // delete all regions inside r (including fuzz)
        Region inR = (Region)r.in ();
        labels.get (SwingDocument.BACKGROUND).fuzzifyBoundingBox (inR);
        s = s.delete (inR);

        // trim all regions overlapping r (not including fuzz)
        s = s.trim (r);

        labels.put (name, s);
        setRegions (name, null);
    }

    String promptForName (String defaultName) {
        String answer = (String)
            JOptionPane.showInputDialog (null,
                                         "Name for current highlight: ",
                                         "Name Highlight",
                                         JOptionPane.PLAIN_MESSAGE,
                                         null,
                                         null,
                                         "");
        if (answer == null)
            return null;

        answer = answer.trim ();
        if (answer.length() == 0)
            return null;

        return answer;
//         try {
//             return getDocument ().getLabeling ().lookup (answer);
//         } catch (Labeling.LabelNotFoundException e) {
//             return answer;
//         } catch (Labeling.LabelAmbiguousException e) {
//             return (String)
//                 JOptionPane.showInputDialog (null,
//                                              "Several labels match that name.\n"
//                                              + "Please select the one you want.",
//                                              "Choose Label",
//                                              JOptionPane.PLAIN_MESSAGE,
//                                              null,
//                                              e.getChoices (),
//                                              null);
//         }
    }



    ////////////////////
    //////////////////// Selecting named region sets
    ////////////////////

    /**
     * Action invoked when user clicks on the tree display.
     * Clears the selection if the mouse isn't pointing at a name.
     */
    public MouseListener treeMouseListener = new MouseAdapter () {
        public void mousePressed (MouseEvent e) {
            tree.expandNodeAtLocation (e.getX(), e.getY());

            String label = tree.getLabelAtLocation (e.getX(), e.getY());
            if (label != null) {
                Log.println ("find name " + label);
                setRegions (label, null);
            }
        }

//         public void mouseClicked (MouseEvent e) {
//             TreePath path = tree.getPathForLocation(e.getX(), e.getY());
//             if(path != null && e.getClickCount() == 2)
//                 putNameInPattern ((Label)path.getPathComponent (path.getPathCount ()-1));
//         }
    };

    /**
     * Listener invoked when user shift-clicks on the browser pane.
     * Adds a region to the highlighted region set. 
     */

    MouseListener shiftClickListener = new MouseAdapter () {
        public void mousePressed (MouseEvent evt) {
            if (SwingUtilities.isLeftMouseButton (evt)) {
                RegionHighlighter highlighter = 
                    (RegionHighlighter) activePane.getHighlighter ();
                Point pt = new Point (evt.getX (), evt.getY ());

                RegionHighlighter.Handle h = highlighter.handleAtPoint (pt);
                if (h != null) {
                    Region r = h.getRegion ();

                    // prepare to move handle of highlighted region
                    Interval i = (Interval) r.crispen ();
                    shiftDragMark = h.isStart () ? i.end : i.start;
                    highlighter.setFeedback (i);

                    setRegions (null, 
                                highlightedRegions.copy ()
                                .delete (r));
                    Log.println ("adjust handle of " + r);
                } else {
                    // prepare to add new region to highlight.
                    // start dragging it out
                    Interval r = activePane.convertPointToRegion (pt);
                    shiftDragMark = r.start;
                    highlighter.setFeedback (r);
                    setRegions (highlightedName, highlightedRegions);
                }
            }
        }
        public void mouseReleased (MouseEvent evt) {
            if (shiftDragMark != -1) {
                RegionHighlighter highlighter = 
                    (RegionHighlighter) activePane.getHighlighter ();
                Interval r = highlighter.getFeedback ();
                highlighter.setFeedback (null);

                if (r != null && r.start != r.end) {
                    Log.println ("highlight " + r);

                    if (highlightedRegions == null)
                        setRegions (null, r);
                    else {
                        setRegions (null, 
                                    highlightedRegions.copy ()
                                    .insert (r));
                    }
                }

                shiftDragMark = -1;
            }
        }
    };

    MouseMotionListener shiftDragListener = new MouseMotionAdapter () {
        public void mouseDragged (MouseEvent evt) {
            if (shiftDragMark != -1) {
                Point pt = new Point (evt.getX (), evt.getY ());
                Interval r = activePane.convertPointToRegion (pt);

                Interval s = new Interval (Math.min (shiftDragMark, r.start),
                                           Math.max (shiftDragMark, r.start));

                ((RegionHighlighter) activePane.getHighlighter ())
                    .setFeedback (s);
            }
        }
    };

    /**
     * Action invoked when user right-clicks on the browser pane.
     * Pops up a menu showing region set names that coincide with
     * the current selection.
     */
    MouseListener rightClickListener = new MouseAdapter () {
        public void mousePressed (MouseEvent evt) {
            if (SwingUtilities.isRightMouseButton (evt))
                showPopupMenu ((Component)evt.getSource (), evt.getX (), evt.getY ());
        }
    };

    protected void showPopupMenu (Component source, int x, int y) {
        JPopupMenu menu = new JPopupMenu ();
//         menu.add (nameAction);
//         menu.add (nameAsAction);
//         menu.add (unnameAction);
//         menu.addSeparator ();
//         menu.add (possibleTextConstraints (activePane.getSelectedRegion ()));
//         menu.addSeparator ();
//         menu.add (copyAction);

        clickedRegion = activePane.regionAtPoint (new Point (x, y));
        removeHighlightAction.setEnabled (clickedRegion != null);

        menu.add (removeHighlightAction);
        menu.add (removeAllHighlightsAction);
        menu.show (source, x, y);
    }

    JMenu possibleTextConstraints (Region selection) {
        Labeling labels = getDocument ().getLabeling ();

        // Look for any region containing the selection.
        Region filter = (Region)selection.contains ();

        // Fuzzify filter.
        labels.get (SwingDocument.BACKGROUND).fuzzifyBoundingBox (filter);

        // Scan all the names in the label tree looking for region sets 
        // that intersect filter.
        Vector userNames = new Vector ();
        Region any = (Region)labels.get (SwingDocument.ANY);
        for (Enumeration e = labels.names (); e.hasMoreElements (); ) {
            String name = e.nextElement ().toString ();
            Region r = firstMatchingRegion (name, filter);
            if (r != null
                && !r.equals (any) 
                && !name.startsWith (SwingDocument.SYSTEM))
                userNames.addElement (new NameItem (name, r));
        }
        
        // FIX: put back sorting rules for (top-level names first,
        // then leaf names, then generalizations)

        // Create a popup menu with the names on it and show it
        JMenu menu = new JMenu ("Text Constraints");
        addItems (menu, userNames);

        return menu;
    }

    void addItems (JMenu menu, Vector names) {
        for (Enumeration e = names.elements (); e.hasMoreElements (); ) {
            Object o = e.nextElement ();
            if (o instanceof JSeparator)
                menu.addSeparator ();
            else
                menu.add ((JMenuItem) o);
        }
    }

    Region firstMatchingRegion (String name, Region r) {
        Labeling labels = getDocument ().getLabeling ();

        RegionSet s = labels.get (name);
        if (s == null)
            return null;
        RegionEnumeration e = s.regions (r);

        Region m = e.first ();
        if (m == null || m.equals (labels.get (lapis.Document.ANY)))
            return null;    // don't return useless names

        return m;
    }

    class NameItem extends JMenuItem {
        String name;
        Region region;

        public NameItem (String name, Region region) {
            super (name.toString ());
            this.name = name;
            this.region = region;
        }

        /**
         * When a menu item is selected, highlight the corresponding region
         * set in the document (and set the selection to the corresponding region
         * that includes it).
         */
        public void menuSelectionChanged(boolean isIncluded) {
            super.menuSelectionChanged (isIncluded);
            if (isIncluded) {
                setRegions (null, region);
            }
            else {
                setRegions (null, null);
            }
        }

        public void fireActionPerformed (ActionEvent evt) {
            super.fireActionPerformed (evt);
            //putNameInPattern (name);
            setRegions (name, null);
        }
    }

    /**
     * Handle change in user's selection.  Sets the named region SELECTION to the current
     * selection.
     */
    public CaretListener caretListener = new CaretListener () {
        public void caretUpdate(CaretEvent e) {
            SwingDocument doc = getDocument ();
            if (doc == null)
                return;

            Region sel = activePane.getSelectedRegion ();
            //System.err.println ("selection changed to " + sel);
            doc.getLabeling ().put (SwingDocument.SELECTION, sel);
        }
    };


    ////////////////////
    //////////////////// Scripting
    ////////////////////

    /**
     * Action invoked when user clicks on Script Editor menu item.
     * Pops up a new TCLFileEditor window.
     */
    public Action scriptEditorAction = new AcceleratedAction ("Script Editor") {
        public void actionPerformed (ActionEvent evt) {
            showTclEditor ();
        }
    };

    public void showTclEditor () {
        if (tclEditor == null) {
            tclEditor = new TCLFileEditor (Browser.this);
            tclEditor.show ();
            PopupDialog.centerWindow (tclEditor, this);
        } else
            tclEditor.show ();
    }

    /**
     * Action invoked by Demonstrate command.
     */
    public Action demonstrateAction = new AcceleratedAction ("&Demonstrate", 'D', KeyEvent.CTRL_MASK) {
        public void actionPerformed (ActionEvent evt) {
            Demonstrator d = new Demonstrator (Browser.this);
            d.show ();
            Point p = getLocation ();
            p.translate (50, 50);
            d.setLocation (p);
        }
    };

    /**
     * Action invoked by History to Script command.
     */
    public Action historyScriptAction = new AcceleratedAction ("Convert &History to Script", 'H', KeyEvent.CTRL_MASK) {
        public void actionPerformed (ActionEvent evt) {
            popupScriptEditor (convertHistoryToScript ());
        }
    };

    public String convertHistoryToScript () {
        History history = getHistory ();
        String script = "";
        for (Enumeration e = history.elements (); e.hasMoreElements (); )
            script += e.nextElement () + "\n";
        return script;
    }

    public void popupScriptEditor (String contents) {
        showTclEditor ();
        if (tclEditor.okToClear ()) {
            tclEditor.setText (contents);
            tclEditor.setFilename (null);
        }            
    }


    ////////////////////
    //////////////////// Text-processing tools
    ////////////////////

    public void addTool (Tool tool) {
        String description = Toolbox.getToolDescription (tool);
        String name;
        if (description.endsWith (".tcl"))
            // description is a filename; use the basename (deleting
            // directory and .tcl extension)
            name = URLUtil.getBasename (URLUtil.HrefToURL (description));
        else {
            // description is a Java class name; use the base name
            // (deleting the package)
            int lastPeriod = description.lastIndexOf ('.');
            if (lastPeriod > -1)
                name = description.substring (lastPeriod+1);
            else
                name = description;
        }

        Action action = new LoadedToolAction (tool, name);
        toolMenu.add (action);
        toolbar.add (action);
        factory.getInterpreter ().registerTool (name, tool);
    }

    class BrowserArguments extends Arguments {
        String command;

        public BrowserArguments () {
            super (Browser.this.factory);
        }
    }

    abstract class ToolAction extends AcceleratedAction {
        Tool tool;
        BrowserArguments args;
        boolean usesHighlight;

        public ToolAction (Tool tool, String label, boolean usesHighlight) {
            super (label);
            this.tool = tool;
            this.usesHighlight = usesHighlight;
        }

        public abstract BrowserArguments makeArguments ();

        public void actionPerformed (ActionEvent event) {
            stop ();

            if (usesHighlight && getRegions () == null) {
                getToolkit ().beep ();
                return;
            }

            args = makeArguments ();
            if (args == null)
                return;

            runStoppableActivity (new RunnableWithExceptions () {
                public void run () throws Exception {
                    lapis.Document result = tool.invoke (args);
                    if (! (result instanceof StatusWriter)) {
                        if (args.command != null)
                            result.putProperty (SwingDocument.CommandProperty, 
                                                args.command);
                        setDocument (result);
                    }
                }
            });
        }
    }

    class LoadedToolAction extends ToolAction {
        public LoadedToolAction (Tool tool, String name) {
            super (tool, name, false);
        }

        public BrowserArguments makeArguments () {
            BrowserArguments args = (BrowserArguments) new BrowserArguments ()
                .add ("doc", getDocument ());
            args.command = getValue (NAME).toString ();
            return args;
        }
    }


    public static String tclQuote (String s) {
        if (Str.indexOfAnyChar (s, " \r\n{}\"\'\\") == -1)
            return s; // no special characters
        else
            return "{" + s + "}";
    }

    /**
     * Action invoked by Keep command.  Keeps highlighted regions
     * to create a new document.
     */
    public Action keepAction = new ToolAction (new lapis.tools.Keep (), "&Keep", true) {
        public BrowserArguments makeArguments () {
            BrowserArguments args = (BrowserArguments) new BrowserArguments ()
                .add ("query", highlightedRegions)
                .add ("outof", (highlightedPattern != null) 
                      ? (Object)TC.make (highlightedPattern).getObjectPattern ()
                      : (Object)highlightedRegions)
                .add ("doc", getDocument ())
                ;
            args.command = "keep " + tclQuote (highlightedPattern);
            return args;
        }
    };


    /**
     * Action invoked by Delete command.  Deletes out highlighted regions
     * to create a new document.
     */
    public Action deleteAction = new ToolAction (new lapis.tools.Delete (), "&Delete", true) {
        public BrowserArguments makeArguments () {
            BrowserArguments args = (BrowserArguments) new BrowserArguments ()
                .add ("query", highlightedRegions)
                .add ("doc", getDocument ())
                ;
            args.command = "delete " + tclQuote (highlightedPattern);
            return args;
        }
    };


    /**
     * Action invoked by Sort command.  Sorts highlighted regions
     * to create a new document, and displays that.
     */
    public Action sortAction = new ToolAction (new lapis.tools.Sort (), "&Sort", true) {
        public BrowserArguments makeArguments () {
            SortDialog dialog = new SortDialog ();
            dialog.show ();
            return dialog.args;
        }
    };


    
    class SortDialog extends OkCancelDialog {

        ButtonGroup keyGroup;
        JRadioButton useEntireRecord;  // use entire record as sort key
        JRadioButton usePattern;       // use specified pattern as sort key
        JTextField keyPattern;
        
        JComboBox order;            // alphabetic, numeric, etc.        
        JComboBox direction;        // ascending or descending            
        BrowserArguments args;             // null until created by ok()
        
        public SortDialog () {
            super (Browser.this, "Sort");
            
            Layout layout = Layout.gridbag (getControlsPane ())
                .setAlignment (Layout.NORTHWEST);

            layout
                .add (new JLabel ("Sort Key: "), Layout.FIXED_SIZE)
                .add (useEntireRecord = new JRadioButton ("Full record"))
                .nextRow ()
                .nextColumn ()
                .add (usePattern = new JRadioButton ("Part of record matching: "))
                .nextRow ()
                .nextColumn ()
                .add (keyPattern = new JTextField ())
                .nextRow ();

            keyGroup = new ButtonGroup ();
            keyGroup.add (useEntireRecord);
            keyGroup.add (usePattern);
            useEntireRecord.setSelected (true);

            String yourLanguage = 
                java.util.Locale.getDefault ().getDisplayLanguage ();
            if (yourLanguage.equals (""))
                yourLanguage = "Dictionary";
            layout
                .add (new JLabel ("Order: "), Layout.FIXED_SIZE)
                .add (order = new JComboBox (new String[] {
                    yourLanguage, "Numeric", "ASCII", "Random" }),
                      Layout.FIXED_SIZE)
                .nextRow ();
            order.setEditable (false);

            layout
                .add (new JLabel ("Direction: "), Layout.FIXED_SIZE)
                .add (direction = new JComboBox (new String[] {
                    "Increasing", "Decreasing" }), Layout.FIXED_SIZE)
                .nextRow ();
            direction.setEditable (false);
            
            pack ();
        }

        public void ok () {
            try {
                // Create arguments
                args = (BrowserArguments)new BrowserArguments ()
                    .add ("query", highlightedRegions)
                    .add ("doc", getDocument ())
                    ;
                String tclCommand = "sort " + tclQuote (highlightedPattern);

                if (usePattern.isSelected ()) {
                    TC keys = Browser.makeTCFromTextComponent (keyPattern);
                    args.add ("by", keys);
                    tclCommand += " -by " + tclQuote (keyPattern.getText ());
                }

                boolean reverse = (direction.getSelectedIndex () == 1);

                switch (order.getSelectedIndex ()) {
                case 0: // natural
                    args.addName ("order");
                    if (reverse)
                        args.addValue ("reverse");
                    args.addValue ("dictionary");
                    tclCommand += " -order " + (reverse ? "reverse " : "") + "dictionary";
                    break;
                case 1: // numeric
                    args.addName ("order");
                    if (reverse)
                        args.addValue ("reverse");
                    args.addValue ("numeric");
                    tclCommand += " -order " + (reverse ? "reverse " : "") + "numeric";
                    break;
                case 2: // ASCII
                    args.addName ("order");
                    if (reverse)
                        args.addValue ("reverse");
                    args.addValue ("ascii");
                    tclCommand += " -order " + (reverse ? "reverse " : "") + "ascii";
                    break;
                case 3: // RANDOM
                    args.addName ("order");
                    args.addValue ("random");
                    tclCommand += " -order random";
                    break;
                }

                args.command = tclCommand;

                super.ok ();
            } catch (TCParseException e) {
                FeedbackForm.showMessageDialog (Browser.this, 
                                                e.getMessage (), 
                                                "Parse Error",
                                                JOptionPane.ERROR_MESSAGE);
                args = null;
            }
        }
    }


    /**
     * Action invoked by Replace command.  Replaces regions
     * to create a new document, and displays that.
     */
    public Action replaceAction = new ToolAction (new lapis.tools.Replace (), "&Replace", true) {
        public BrowserArguments makeArguments () {
            ReplaceDialog dialog = new ReplaceDialog ();
            dialog.show ();
            return dialog.args;
        }
    };

    class ReplaceDialog extends OkCancelDialog {

        JTextArea template;
        BrowserArguments args;  // null until created by ok()
        
        public ReplaceDialog () {
            super (Browser.this, "Replace");
            
            Layout.gridbag (getControlsPane ())
                .setAlignment (Layout.WEST)
                .add (new JLabel ("Replace With: "), Layout.FIXED_SIZE)
                .nextRow ()
                .add (template = new JTextArea (5, 40));
            
            pack ();
        }
        
        public void ok () {
            try {
                // Configure arguments
                args = (BrowserArguments) new BrowserArguments ()
                    .add ("query", highlightedRegions)
                    .add ("doc", getDocument ())
                    ;
                String tclCommand = "replace " + tclQuote (highlightedPattern);

                try {
                    String tmplt = template.getText ();
                    args.add ("replace",
                              lapis.tools.Template.makeFromString (tmplt));
                    tclCommand += " " + tclQuote (tmplt);
                } catch (TCParseException e) {
                    // Select the error, then re-throw the exception
                    selectParseError (template, e);
                    throw e;
                }

                args.command = tclCommand;

                super.ok ();
            } catch (TCParseException e) {
                FeedbackForm.showMessageDialog (Browser.this, 
                                                e.getMessage (), 
                                                "Parse Error",
                                                JOptionPane.ERROR_MESSAGE);
                args = null;
            }
        }
    }


    /**
     * Action invoked by Extract command.  Extracts regions
     * to create a new document, and displays that.
     */
    public Action extractAction = new ToolAction (new lapis.tools.Extract (), "&Extract", true) {
        public BrowserArguments makeArguments () {
            ExtractDialog dialog = new ExtractDialog ();
            dialog.show ();
            return dialog.args;
        }
    };

    class ExtractDialog extends OkCancelDialog {

        JTextArea template;
        JComboBox view;             // HTML or text
        BrowserArguments args;  // null until created by ok()
        
        public ExtractDialog () {
            super (Browser.this, "Extract");
            
            Layout.gridbag (getControlsPane ())
                .setAlignment (Layout.WEST)
                .add (new JLabel ("View As: "), Layout.FIXED_SIZE)
                .add (view = new JComboBox (), Layout.FIXED_SIZE);
            view.addItem (new ViewType ("Text", "text/plain"));
            view.addItem (new ViewType ("HTML", "text/html"));
            setViewContentType 
                (view, 
                 ((ViewType)Browser.this.view.getSelectedItem ()).contentType);

            pack ();
        }
        
        public void ok () {
//             try {
                ViewType vt = (ViewType)view.getSelectedItem ();

                // Configure arguments
                args = (BrowserArguments) new BrowserArguments ()
                    .add ("query", highlightedRegions)
                    .add ("doc", getDocument ())
                    .add ("as", vt.contentType)
                    ;
                args.command = "extract " + tclQuote (highlightedPattern) 
                    + " -as " + vt.uiName;

                if ("text/plain".equals (vt.contentType)) {
                    args.add ("endswith", "\n");
                    args.command += " -endswith \\n";
                } else {
                    args.add ("endswith", "<br>");
                    args.command += " -endswith <br>";
                }
                
//                 try {
//                     tool.template = lapis.tools.Template.makeFromString (template.getText (), warningListener);
//                 } catch (TCParseException e) {
//                     // Select the error, then re-throw the exception
//                     selectParseError (template, e);
//                     throw e;
//                }

                super.ok ();
//             } catch (TCParseException e) {
//                 FeedbackForm.showMessageDialog (Browser.this, 
//                                                 e.getMessage (), 
//                                                 "Parse Error",
//                                                 JOptionPane.ERROR_MESSAGE);
//                 tool = null;
//             }
        }
    }


    /**
     * Action invoked by Sum command.  Sums the highlighted regions and displays
     * the answer in the status bar.
     */
    public Action sumAction = new ToolAction (new lapis.tools.Calc (), "Su&m", true) {
        public BrowserArguments makeArguments () {
            StatusWriter out = new StatusWriter (Browser.this);
            out.print ("Sum is ");
            return (BrowserArguments) new BrowserArguments ()
                        .add ("query", highlightedRegions)
                        .add ("doc", getDocument ())
                        .add ("output", out)
                        .add ("function", new lapis.tools.Calc.Sum ())
                        ;
        }
    };

    /**
     * Action invoked by Average command.  Averages the highlighted regions and displays
     * the answer in the status bar.
     */
    public Action averageAction = new ToolAction (new lapis.tools.Calc (), "&Average", true) {
        public BrowserArguments makeArguments () {
            StatusWriter out = new StatusWriter (Browser.this);
            out.print ("Average is ");
            return (BrowserArguments) new BrowserArguments ()
                        .add ("query", highlightedRegions)
                        .add ("doc", getDocument ())
                        .add ("output", out)
                        .add ("function", new lapis.tools.Calc.Average ())
                        ;
        }
    };



        //////////////////// 
	//////////////////// MED: New Menu item for CIS enhancements
	////////////////////

    /**
     * Action invoked when user selects CIS/Focused Crawl command.
     * Pops up a dialog box for keyword and URL files
     */
    public Action focCrawlAction = new AcceleratedAction ("&Focused Crawl", 'F', KeyEvent.CTRL_MASK) {
			public void actionPerformed (ActionEvent evt) {
				FocCrawlDialog dialog = new FocCrawlDialog(Browser.this); 
				dialog.show ();
			}
		};


    ////////////////////
    //////////////////// Tool interface for Browser itself
    ////////////////////

    public Browser make () {
        return new Browser ();
    }

    public lapis.Document invoke (Arguments args) throws Exception {
        boolean startNewBrowser = false;
        Vector commands = new Vector ();

        args.setUsage (
        "Usage: Browser [options] [file/URL]\n"
        + "    file/URL  Go to file/URL\n"
        + "\n"
        + "Options:\n"
        + "    -new      Create a new browser\n"
        + "    -help     Display this message\n"
        );
        while (args.hasMoreElements ()) {
            String name = args.nextName ();
            if ("new".equals (name))
                startNewBrowser = true;
            else if ("log".equals (name))
                Log.open (args.nextString ());
            else if (name == null)
                commands.addElement (args.nextString ());
            else 
                args.consume (name);
        }
            
        Browser b = (startNewBrowser && isShowing ()) 
            ? make ()
            : this;

        b.show ();
        b.toFront ();

        Enumeration cmds = commands.elements ();
        if (cmds.hasMoreElements ()) {
            String s = cmds.nextElement ().toString ();
            if (s.trim ().length () > 0)
                b.eval (s);

            while (cmds.hasMoreElements ()) {
                Point p = b.getLocation ();
                b = new Browser ();
                b.show ();
                p.translate (50, 50);
                b.setLocation (p);

                s = cmds.nextElement ().toString ();
                if (s.trim ().length () > 0)
                    b.eval (s);
            }
        }

        return null;
    }
}


syntax highlighted by Code2HTML, v. 0.8.12