/*
 * Copyright (c) 2002-2008 Pixware. 
 *
 * Author: Hussein Shafie
 *
 * This file is part of the XMLmind XML Editor project.
 * For conditions of distribution and use, see the accompanying legal.txt file.
 */

import java.io.IOException;
import java.io.File;

import java.net.URL;

import java.awt.Point;
import java.awt.Color;
import java.awt.Font;
import java.awt.Image;
import java.awt.BorderLayout;
import java.awt.GridBagConstraints;
import java.awt.GridBagLayout;

import java.awt.event.MouseEvent;
import java.awt.event.KeyEvent;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.WindowEvent;
import java.awt.event.WindowAdapter;

import java.awt.print.PrinterException;
import java.awt.print.PageFormat;
import java.awt.print.PrinterJob;

import javax.swing.Timer;
import javax.swing.KeyStroke;
import javax.swing.ImageIcon;
import javax.swing.SwingUtilities;
import javax.swing.BorderFactory;
import javax.swing.JLabel;
import javax.swing.JButton;
import javax.swing.JTextField;
import javax.swing.JMenuBar;
import javax.swing.JPanel;
import javax.swing.JViewport;
import javax.swing.JScrollPane;
import javax.swing.JOptionPane;
import javax.swing.JMenuItem;
import javax.swing.JCheckBoxMenuItem;
import javax.swing.JMenu;
import javax.swing.JFrame;

import com.xmlmind.util.ThrowableUtil;
import com.xmlmind.util.FileUtil;
import com.xmlmind.util.Console;
import com.xmlmind.util.XMLText;

import com.xmlmind.xml.doc.Node;
import com.xmlmind.xml.doc.Element;
import com.xmlmind.xml.doc.Document;
import com.xmlmind.xml.doc.DocumentEvent;
import com.xmlmind.xml.doc.TextEvent;
import com.xmlmind.xml.doc.ProcessingInstructionEvent;
import com.xmlmind.xml.doc.AttributeEvent;
import com.xmlmind.xml.doc.TreeEvent;
import com.xmlmind.xml.doc.InclusionUpdatedEvent;
import com.xmlmind.xml.doc.EditEvent;
import com.xmlmind.xml.doc.DocumentListener;
import static com.xmlmind.xml.doc.Constants.NAMESPACE_PREFIX_MAP_PROPERTY;
import static com.xmlmind.xml.doc.Constants.SOURCE_URL_PROPERTY;

import com.xmlmind.xml.validate.Diagnostic;
import com.xmlmind.xml.validate.DiagnosticUtil;

import com.xmlmind.xml.load.SubDocumentTypeCacheImpl;
import com.xmlmind.xml.load.LoadDocument;

import com.xmlmind.xml.save.SaveDocument;

import com.xmlmind.guiutil.ImageResource;
import com.xmlmind.guiutil.DialogUtil;
import com.xmlmind.guiutil.ThinBorder;
import com.xmlmind.guiutil.Alert;
import com.xmlmind.guiutil.ChooseFile;
import com.xmlmind.guiutil.ShowStatus;

import com.xmlmind.xmledit.gadget.PrintInvoker;
import com.xmlmind.xmledit.gadget.GadgetPrinter;

import com.xmlmind.xmledit.edit.Commands;
import com.xmlmind.xmledit.edit.CommandHistory;
import com.xmlmind.xmledit.edit.NodeMark;
import com.xmlmind.xmledit.edit.TextLocation;
import com.xmlmind.xmledit.edit.ContextChangeEvent;
import com.xmlmind.xmledit.edit.ContextChangeListener;
import com.xmlmind.xmledit.edit.MarkManager;
import com.xmlmind.xmledit.edit.UndoManager;
import com.xmlmind.xmledit.edit.PrefixesChangedEvent;
import com.xmlmind.xmledit.edit.PendingChangesEvent;
import com.xmlmind.xmledit.edit.SendDocumentEvent;
import static com.xmlmind.xmledit.edit.Constants.COMMAND_HISTORY_PROPERTY;
import static com.xmlmind.xmledit.edit.Constants.MARK_MANAGER_PROPERTY;
import static com.xmlmind.xmledit.edit.Constants.UNDO_MANAGER_PROPERTY;

import com.xmlmind.xmledit.view.TreeViewFactory;
import com.xmlmind.xmledit.view.DocumentView;
import com.xmlmind.xmledit.view.DocumentPane;
import com.xmlmind.xmledit.view.JScrollPaneSupport;

import com.xmlmind.xmledit.cmd.select.NodePathEvent;
import com.xmlmind.xmledit.cmd.select.NodePathListener;
import com.xmlmind.xmledit.cmd.select.NodePath;

import com.xmlmind.xmledit.cmd.attribute.AttributeTool;

import com.xmlmind.xmledit.cmd.validate.DiagnosticsTool;

public class SimpleEditor implements ActionListener, DocumentListener, 
                                     ContextChangeListener, NodePathListener,
                                     PrintInvoker, Console,
                                     ShowStatus.StatusWindow {

    // -----------------------------------------------------------------------
    // Menu items
    // -----------------------------------------------------------------------

    protected static final int FILE_MENU    = 0;
    protected static final int TOOLS_MENU   = 1;
    protected static final int OPTIONS_MENU = 2;
    protected static final int HELP_MENU    = 3;

    protected static final int OPEN_COPY_ITEM  = 0;
    protected static final int OPEN_ITEM       = 1;
    protected static final int CLOSE_ITEM      = 2;
    protected static final int SAVE_ITEM       = 3;
    protected static final int SAVE_AS_ITEM    = 4;
    protected static final int PAGE_SETUP_ITEM = 5;
    protected static final int PRINT_ITEM      = 6;
    protected static final int QUIT_ITEM       = 7;

    protected static final int CHECK_VALIDITY_ITEM    = 0;
    protected static final int EDIT_ATTRIBUTES_ITEM   = 1;
    protected static final int DECLARE_NAMESPACE_ITEM = 2;

    protected static final int ATTRIBUTE_ITEM = 0;
    protected static final int TEXT_ITEM      = 1;
    protected static final int COMMENT_ITEM   = 2;
    protected static final int PI_ITEM        = 3;

    protected static final int HELP_ITEM = 0;

    // -----------------------------------------------------------------------
    // Internal state
    // -----------------------------------------------------------------------

    protected boolean showAttribute = true;
    protected boolean showText = true;
    protected boolean showComment = true;
    protected boolean showPI = true;

    protected JFrame frame;
    protected JMenuBar menuBar;
    protected JViewport viewport;
    protected JScrollPaneSupport scrollPaneSupport;
    protected DocumentPane docPane;
    protected DocumentView docView;
    protected NodePath nodePath;
    protected JButton invalid;
    protected ImageIcon[] invalidIcons = new ImageIcon[6];
    protected JTextField message;
    protected Timer messageTimeout = null;
    protected AttributeTool attributeTool;
    protected DiagnosticsTool diagnosticsTool;

    protected File lastOpenedFile = null;
    protected File docFile = null;
    protected boolean needSaveAs = false;
    protected boolean needSave = false;

    protected PageFormat pageFormat = new PageFormat();

    // -----------------------------------------------------------------------
    // Main
    // -----------------------------------------------------------------------

    public static void main(String[] args) {
        SimpleEditor app = new SimpleEditor();
        app.run(args);
    }

    protected void run(String[] args) {
        int i = parseOptions(args);
        if (i < args.length-1)
            usage();

        File openFile = null;
        if (i == args.length-1) {
            openFile = new File(args[i]);
            if (!openFile.isFile())
                usage();
        }

        start();

        if (openFile != null) {
            final File file = openFile;
            SwingUtilities.invokeLater(new Runnable() {
                public void run() {
                    SimpleEditor.this.open(file);
                }
            });
        }
    }

    protected int parseOptions(String[] args) {
        String cacheDirName = null;
        int i;

        for (i = 0; i < args.length; ++i) {
            String arg = args[i];

            if ("-s".equals(arg)) {
                if (i + 1 >= args.length)
                    usage();
                cacheDirName = args[++i];
            } else if ("-na".equals(arg)) {
                showAttribute = false;
            } else if ("-nt".equals(arg)) {
                showText = false;
            } else if ("-nc".equals(arg)) {
                showComment = false;
            } else if ("-np".equals(arg)) {
                showPI = false;
            } else {
                break;
            }
        }

        if (cacheDirName != null) {
            File cacheDir = new File(cacheDirName);
            if (!cacheDir.isDirectory()) {
                usage();
            }

            SubDocumentTypeCacheImpl cache = null;
            try {
                cache = new SubDocumentTypeCacheImpl(/*capacity*/ 5, cacheDir);
            } catch (IOException e) {
                // Probably no write permissions for cacheDir.
                usage();
            }

            LoadDocument.setSubDocumentTypeCache(cache);
        }

        return i;
    }

    protected void usage() {
        System.out.println(
            "usage: java SimpleEditor ?options? ?xml_document?\n" +
            "Options are:");
        listOptions();
        System.exit(1);
    }

    protected void listOptions() {
        System.out.println(
            "  -s directory Use directory to cache serialized\n" +
            "      schemas.\n" +
            "      Default: schemas are not serialized.\n" +
            "  -na In tree view, show attribute set outline.\n" +
            "      Default: show attribute names and values.\n" +
            "  -nt In tree view, show text node outline.\n" +
            "      Default: show character data of text nodes.\n" +
            "  -nc In tree view, show comment node outline.\n" +
            "      Default: show character data of comment nodes.\n" +
            "  -np In tree view, show processing instruction node outline.\n" +
            "      Default: show character data of PI nodes.");
    }

    // -----------------------------------------------------------------------
    // Initialization
    // -----------------------------------------------------------------------

    protected void start() {
        // Frame ---

        frame = new JFrame("SimpleEditor");
        frame.setIconImage(ImageResource.get(
            getClass(), 
            "icons" + File.separatorChar + "SimpleEditor.gif"));
        frame.setDefaultCloseOperation(JFrame.DO_NOTHING_ON_CLOSE);
        frame.addWindowListener(new WindowAdapter() {
            public void windowClosing(WindowEvent e) {
                SimpleEditor.this.quit();
            }
        });

        // workArea is a JPanel with a BorderLayout.
        JPanel workArea = (JPanel) frame.getContentPane();
        ((BorderLayout) workArea.getLayout()).setVgap(2);
        workArea.setBorder(BorderFactory.createEmptyBorder(2, 2, 2, 2));

        // DocumentPane ---

        com.xmlmind.xmledit.cmd.RegisterAll.registerAll();

        docPane = createDocumentPane();
        docView = docPane.getDocumentView();
        configureDocumentView(docView);

        JScrollPane scroller = new JScrollPane();
        workArea.add(scroller, BorderLayout.CENTER);

        scrollPaneSupport = docPane.addToScrollPane(scroller);
        viewport = scroller.getViewport();

        // Menus ---

        menuBar = new JMenuBar();
        frame.setJMenuBar(menuBar);
        configureMenuBar(menuBar);
        enableMenus();

        // NodePath ---

        JLabel templateLabel = new JLabel();
        Color bg = templateLabel.getBackground();
        Color fg = templateLabel.getForeground();
        Font font = templateLabel.getFont();

        nodePath = new NodePath();
        nodePath.setFont(font);
        nodePath.setForeground(fg);
        nodePath.setBackground(bg);
        Color darkBg = bg.darker().darker();
        nodePath.setSeparatorColor(darkBg);
        nodePath.setSeparatorRelief(NodePath.Relief.RAISED);
        darkBg = new Color(darkBg.getRed(), darkBg.getGreen(), 
                           Math.min(255, darkBg.getBlue() + 32));
        nodePath.setReadOnlyColor(darkBg);
        nodePath.setBorderRelief(NodePath.Relief.SUNKEN);
        nodePath.addNodePathListener(this);
        workArea.add(nodePath, BorderLayout.NORTH);

        // Status bar ---

        JPanel statusBar = new JPanel(new GridBagLayout());
        workArea.add(statusBar, BorderLayout.SOUTH);

        GridBagConstraints constraints = new GridBagConstraints();
        constraints.gridx = 0;
        constraints.gridy = 0;

        for (int j = 0; j < invalidIcons.length; ++j) {
            Image image = ImageResource.get(
                getClass(), 
                "icons" + File.separatorChar + "severity" + j + ".gif");
            invalidIcons[j] = new ImageIcon(image);
        }
        
        invalid = new JButton(invalidIcons[0]);
        DialogUtil.setIconic(invalid);
        constraints.fill = GridBagConstraints.VERTICAL;
        statusBar.add(invalid, constraints);
        ++constraints.gridx;

        invalid.addActionListener(new ActionListener() {
            public void actionPerformed(ActionEvent e) {
                if (getMenuItem(TOOLS_MENU, CHECK_VALIDITY_ITEM).isEnabled())
                    checkValidity();
            }
        });

        font = workArea.getFont();
        bg = workArea.getBackground();

        Font smallFont = new Font(font.getFamily(), font.getStyle(), 
                                  Math.max(10, font.getSize()-2));

        message = new JTextField();
        message.setEditable(false);
        message.setFont(smallFont);
        message.setBorder(new ThinBorder(bg, /*raised*/ false, 0, 0));
        constraints.weightx = 1;
        constraints.fill = GridBagConstraints.HORIZONTAL;
        constraints.insets.left = 2;
        statusBar.add(message, constraints);

        ShowStatus.setStatusWindow(this);

        // Show ---

        frame.setBounds(0, 0, 600, 700);
        frame.setVisible(true);
    }

    protected DocumentPane createDocumentPane() {
        return new DocumentPane();
    }

    protected void configureDocumentView(DocumentView docView) {
        TreeViewFactory viewFactory = 
            (TreeViewFactory) docView.getViewFactory();
        TreeViewFactory.Settings settings = viewFactory.getSettings();
        settings.showAttribute = showAttribute;
        settings.showText = showText;
        settings.showComment = showComment;
        settings.showPI = showPI;
    }

    protected void configureMenuBar(JMenuBar menuBar) {
        JMenu menu = new JMenu("File");

        int modifier = menu.getToolkit().getMenuShortcutKeyMask();

        JMenuItem item = new JMenuItem("Open Copy...");
        item.setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_N, modifier));
        item.setActionCommand("openCopy");
        item.addActionListener(this);
        menu.add(item);

        item = new JMenuItem("Open...");
        item.setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_O, modifier));
        item.setActionCommand("open");
        item.addActionListener(this);
        menu.add(item);

        item = new JMenuItem("Close");
        item.setActionCommand("close");
        item.addActionListener(this);
        menu.add(item);

        item = new JMenuItem("Save");
        item.setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_S, modifier));
        item.setActionCommand("save");
        item.addActionListener(this);
        menu.add(item);

        item = new JMenuItem("Save As...");
        item.setActionCommand("saveAs");
        item.addActionListener(this);
        menu.add(item);

        item = new JMenuItem("Page Setup...");
        item.setActionCommand("pageSetup");
        item.addActionListener(this);
        menu.add(item);

        item = new JMenuItem("Print...");
        item.setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_P, modifier));
        item.setActionCommand("print");
        item.addActionListener(this);
        menu.add(item);

        item = new JMenuItem("Quit");
        item.setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_Q, modifier));
        item.setActionCommand("quit");
        item.addActionListener(this);
        menu.add(item);

        menuBar.add(menu);

        menu = new JMenu("Tools");

        item = new JMenuItem("Check document validity");
        item.setAccelerator(
            KeyStroke.getKeyStroke(KeyEvent.VK_S, 
                                   modifier|KeyEvent.SHIFT_MASK));
        item.setActionCommand("checkValidity");
        item.addActionListener(this);
        menu.add(item);

        item = new JMenuItem("Edit attributes");
        item.setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_E, modifier));
        item.setActionCommand("editAttributes");
        item.addActionListener(this);
        menu.add(item);

        item = new JMenuItem("Declare Namespace...");
        item.setActionCommand("declareNamespace");
        item.addActionListener(this);
        menu.add(item);

        menuBar.add(menu);

        menu = new JMenu("Options");

        TreeViewFactory viewFactory = 
            (TreeViewFactory) docView.getViewFactory();
        TreeViewFactory.Settings settings = viewFactory.getSettings();

        JCheckBoxMenuItem toggle = new JCheckBoxMenuItem("Attribute");
        toggle.setState(settings.showAttribute);
        toggle.setActionCommand("toggleShowAttribute");
        toggle.addActionListener(this);
        menu.add(toggle);

        toggle = new JCheckBoxMenuItem("Text");
        toggle.setState(settings.showText);
        toggle.setActionCommand("toggleShowText");
        toggle.addActionListener(this);
        menu.add(toggle);

        toggle = new JCheckBoxMenuItem("Comment");
        toggle.setState(settings.showComment);
        toggle.setActionCommand("toggleShowComment");
        toggle.addActionListener(this);
        menu.add(toggle);

        toggle = new JCheckBoxMenuItem("PI");
        toggle.setState(settings.showPI);
        toggle.setActionCommand("toggleShowPI");
        toggle.addActionListener(this);
        menu.add(toggle);

        menuBar.add(menu);

        menu = new JMenu("Help");

        item = new JMenuItem("Help");
        item.setActionCommand("help");
        item.addActionListener(this);
        menu.add(item);

        menuBar.add(menu);
    }

    public void nodeSelected(NodePathEvent e) {
        Node node = e.node;
        int modifiers = e.modifiers;

        if (modifiers == MouseEvent.BUTTON1_MASK) {
            docView.selectNode(node);
        } else if (modifiers == 
                   (MouseEvent.BUTTON1_MASK|MouseEvent.SHIFT_MASK)) {
            docView.selectNode(node);
            docView.executeCommand("insertNode", "sameElementBefore", -1, -1);
        } else if (modifiers == 
                   (MouseEvent.BUTTON1_MASK|MouseEvent.CTRL_MASK)) {
            docView.selectNode(node);
            docView.executeCommand("insertNode", "sameElementAfter", -1, -1);
        }
    }

    public void actionPerformed(ActionEvent event) {
        String command = event.getActionCommand();
        
        if (command.equals("openCopy")) {
            openCopy();
        } else if (command.equals("open")) {
            open();
        } else if (command.equals("close")) {
            close();
        } else if (command.equals("save")) {
            save();
        } else if (command.equals("saveAs")) {
            saveAs();
        } else if (command.equals("pageSetup")) {
            pageSetup();
        } else if (command.equals("print")) {
            print();
        } else if (command.equals("quit")) {
            quit();
        } else if (command.equals("checkValidity")) {
            checkValidity();
        } else if (command.equals("editAttributes")) {
            editAttributes();
        } else if (command.equals("declareNamespace")) {
            declareNamespace();
        } else if (command.equals("toggleShowAttribute")) {
            toggleShowAttribute();
        } else if (command.equals("toggleShowText")) {
            toggleShowText();
        } else if (command.equals("toggleShowComment")) {
            toggleShowComment();
        } else if (command.equals("toggleShowPI")) {
            toggleShowPI();
        } else if (command.equals("help")) {
            help();
        }
    }

    protected void enableMenus() {
        Document doc = docView.getDocument();
        boolean docLoaded = (doc != null);

        getMenuItem(FILE_MENU, CLOSE_ITEM).setEnabled(docLoaded);
        getMenuItem(FILE_MENU, SAVE_ITEM).setEnabled(false);
        getMenuItem(FILE_MENU, SAVE_AS_ITEM).setEnabled(docLoaded);
        getMenuItem(FILE_MENU, PRINT_ITEM).setEnabled(docLoaded);

        boolean canCheck;
        if (docLoaded)
            canCheck = DiagnosticUtil.canCheckDocument(doc);
        else
            canCheck = false;
        getMenuItem(TOOLS_MENU, CHECK_VALIDITY_ITEM).setEnabled(canCheck);

        getMenuItem(TOOLS_MENU, DECLARE_NAMESPACE_ITEM).setEnabled(docLoaded);
    }

    protected JMenuItem getMenuItem(int menuIndex, int itemIndex) {
        JMenu menu = menuBar.getMenu(menuIndex);
        return menu.getItem(itemIndex);
    }

    // -----------------------------------------------------------------------
    // Commands
    // -----------------------------------------------------------------------

    // -----------------------------------
    // Open as Template
    // -----------------------------------

    protected void openCopy() {
        File file = chooseOpenFile();
        if (file == null)
            return;

        if (!checkNeedSave())
            return;

        openCopy(file);
    }

    protected File chooseOpenFile() {
        File template;
        if (lastOpenedFile != null)
            template = lastOpenedFile;
        else
            template = (new File("Untitled.xml")).getAbsoluteFile();

        return ChooseFile.chooseOpenFile(frame, template);
    }

    protected void openCopy(File file) {
        Document doc = loadDocument(file);
        if (doc == null)
            return;

        docFile = (new File("Untitled.xml")).getAbsoluteFile();
        doc.putProperty(SOURCE_URL_PROPERTY, FileUtil.fileToURL(docFile));

        needSaveAs = true;
        needSave = false;
        updateTitle();
        setDocument(doc);
        enableMenus();
        updateValidity();
    }

    protected void setDocument(Document doc) {
        Document prevDoc = docView.getDocument();
        if (prevDoc != null) 
            uninstallDocument(prevDoc);

        if (doc != null) 
            installDocument(doc);

        contextChanged(null);

        docView.refresh();
        viewport.setViewPosition(new Point(0, 0));
        viewport.repaint();
    }

    protected void uninstallDocument(Document doc) {
        SendDocumentEvent.closingDocument(doc);

        doc.removeDocumentListener(this);

        docView.getMarkManager().removeContextChangeListener(this);
        docView.setDocument(null);

        if (attributeTool != null)
            attributeTool.setDocument(null);
    }

    protected void installDocument(Document doc) {
        docView.setDocument(doc);
        docView.getMarkManager().addContextChangeListener(this);

        if (attributeTool != null)
            attributeTool.setDocument(doc);

        doc.addDocumentListener(this, /*hasData*/ false);
    }

    protected void updateValidity() {
        if (getMenuItem(TOOLS_MENU, CHECK_VALIDITY_ITEM).isEnabled()) {
            checkValidity();
        } else {
            if (diagnosticsTool != null)
                diagnosticsTool.close();
            updateValidityIcon(null);
        }
    }

    // -----------------------------------
    // Open
    // -----------------------------------

    protected void open() {
        File file = chooseOpenFile();
        if (file == null)
            return;

        if (!checkNeedSave())
            return;

        open(file);
    }

    protected void open(File file) {
        Document doc = loadDocument(file);
        if (doc == null)
            return;

        docFile = lastOpenedFile = file;
        needSave = false;
        needSaveAs = !docFile.canWrite();
        updateTitle();
        setDocument(doc);
        enableMenus();
        updateValidity();
    }

    protected Document loadDocument(File file) {
        setMessage("Loading document '" + file + "'...", false);

        Document doc = com.xmlmind.xmledit.cmd.include.LoadDocument.load(
            file, LoadDocument.getDefaultOptions(), /*console*/ this, frame);

        if (doc != null) {
            doc.putProperty(COMMAND_HISTORY_PROPERTY, new CommandHistory());
            doc.putProperty(MARK_MANAGER_PROPERTY, new MarkManager(doc));
            doc.putProperty(UNDO_MANAGER_PROPERTY, new UndoManager(doc));
        }

        setMessage(null);

        return doc;
    }

    // -----------------------------------
    // Close
    // -----------------------------------

    protected void close() {
        if (!checkNeedSave())
            return;

        setDocument(null);
        docFile = null;
        needSaveAs = false;
        needSave = false;
        updateTitle();
        setMessage(null);
        enableMenus();
        updateValidity();
    }

    // -----------------------------------
    // Save
    // -----------------------------------

    protected void save() {
        if (needSave) {
            if (needSaveAs) {
                saveAs();
            } else {
                backup(docFile);
                save(docFile, /*saveAs*/ false);
            }
        }
    }

    protected boolean backup(File file) {
        if (!file.exists()) 
            return false;

        File backupFile = new File(file.getPath() + '~');
        if (backupFile.exists())
            backupFile.delete();
        return file.renameTo(backupFile);
    }

    protected void save(File file, boolean saveAs) {
        Document doc = docView.getDocument();
        SendDocumentEvent.commitChanges(doc);

        setMessage("Saving document '" + file + "'...", false);

        if (!saveDocument(doc, file, saveAs)) {
            setMessage(null);
            return;
        }

        setMessage(null);

        needSave = false;
        if (saveAs) {
            needSaveAs = false;
            docFile = lastOpenedFile = file;
        }
        updateTitle();
        getMenuItem(FILE_MENU, SAVE_ITEM).setEnabled(false);

        if (getMenuItem(TOOLS_MENU, CHECK_VALIDITY_ITEM).isEnabled())
            checkValidity();
    }

    protected boolean saveDocument(Document doc, File file, boolean saveAs) {
        try {
            if (saveAs) {
                SaveDocument.saveAs(doc, file);
            } else {
                SaveDocument.save(doc, file);
            }
            return true;
        } catch (IOException e) {
            Alert.showError(frame, 
                            "Cannot save file '" + file + "':\n" + 
                            ThrowableUtil.reason(e));
            return false;
        }
    }

    // -----------------------------------
    // SaveAs
    // -----------------------------------

    protected void saveAs() {
        File file = ChooseFile.chooseSaveFile(frame, docFile);
        if (file == null)
            return;

        save(file, /*saveAs*/ true);
    }

    // --------------------------------------
    // Print
    // --------------------------------------

    protected void pageSetup() {
        PrinterJob printJob = PrinterJob.getPrinterJob();

        pageFormat = printJob.pageDialog(pageFormat);
    }

    protected void print() {
        PrinterJob printJob = PrinterJob.getPrinterJob();

        GadgetPrinter printer = new GadgetPrinter();
        PageFormat pageFormat2 = printJob.validatePage(pageFormat);
        printJob.setPrintable(printer, pageFormat2);

        if (!printJob.printDialog())
            return;

        setMessage("Printing...", false);

        printer.setInvoker(this);
        printer.setScreenResolution(100 /*dpi*/);
        printer.setFooterBegin(docFile.getPath());
        printer.setFooterEnd("%I/%C");
        printer.setFooterFont(new Font("SansSerif", Font.PLAIN, 10));
        printer.setFooterColor(Color.gray);
        printer.setFooterOverlined(true);

        scrollPaneSupport.setViewportWidthTracked(false);
        printer.begin(docView, pageFormat);

        String printError = null;
        try {
            printJob.print();
        } catch (PrinterException e) {
            printError = ThrowableUtil.reason(e);
        }

        setMessage("Printing...", false);

        scrollPaneSupport.setViewportWidthTracked(true);
        printer.end();

        setMessage(null);

        if (printError != null)
            Alert.showError(frame, "Cannot print: " + printError);
    }

    public boolean printingPage(GadgetPrinter printer, 
                                int pageIndex, int pageCount) {
        if (pageIndex < pageCount) {
            setMessage("Printing page " + (1+pageIndex) + " of " + pageCount,
                       false);
        }
        return true;
    }

    // -----------------------------------
    // Quit
    // -----------------------------------

    protected void quit() {
        if (!checkNeedSave())
            return;

        System.exit(0);
    }

    protected boolean checkNeedSave() {
        if (needSave) {
            int option = JOptionPane.showConfirmDialog(
                frame, 
                "Do you want to save changes made to\n" + docFile + "?", 
                "Save Document", JOptionPane.YES_NO_CANCEL_OPTION);

            switch (option) {
            case JOptionPane.YES_OPTION:
                save();
                return true;
            case JOptionPane.NO_OPTION:
                return true;
            default:
                return false;
            }
        } else {
            return true;
        }
    }
    
    // -----------------------------------
    // CheckValidity
    // -----------------------------------

    protected void checkValidity() {
        Document doc = docView.getDocument();
        SendDocumentEvent.commitChanges(doc);

        setMessage("Checking document validity...", false);

        Diagnostic[] diagnostics = DiagnosticUtil.checkDocument(doc);

        if (diagnostics != null && diagnostics.length > 0) {
            if (diagnosticsTool == null) {
                diagnosticsTool = 
                    new DiagnosticsTool(frame, "Check document validity");
                diagnosticsTool.setPosition(DiagnosticsTool.SOUTH_EAST);
                diagnosticsTool.setDocumentView(docView);
            }

            diagnosticsTool.setDiagnostics(diagnostics);
            diagnosticsTool.open();

            setMessage(null);
        } else {
            if (diagnosticsTool != null)
                diagnosticsTool.close();

            setMessage("Document is valid.");
        }

        updateValidityIcon(diagnostics);
    }

    protected void updateValidityIcon(Diagnostic[] diagnostics) {
        Diagnostic.Severity severity = Diagnostic.Severity.NONE;
        if (diagnostics != null)
            severity = DiagnosticUtil.getDiagnosticSeverity(diagnostics);

        invalid.setIcon(invalidIcons[severity.ordinal()]);
    }

    // -----------------------------------
    // EditAttributes
    // -----------------------------------

    protected void editAttributes() {
        if (attributeTool == null) {
            attributeTool = new AttributeTool(frame, "Edit attributes");
            attributeTool.setPosition(AttributeTool.NORTH_EAST);
            attributeTool.setDocument(docView.getDocument());
        }

        attributeTool.open();
        contextChanged(null);
    }

    // -----------------------------------
    // DeclareNamespace
    // -----------------------------------

    protected void declareNamespace() {
        docView.executeCommand("declareNamespace", null, -1, -1);
    }

    protected void prefixesChanged() {
        nodePath.setNode(nodePath.getNode());

        docView.refresh();

        if (attributeTool != null)
            attributeTool.resetElement();

        if (!needSave)
            setNeedSave();
    }

    // -----------------------------------
    // TreeViewFactory options
    // -----------------------------------

    protected void toggleShowAttribute() {
        TreeViewFactory viewFactory = 
            (TreeViewFactory) docView.getViewFactory();
        TreeViewFactory.Settings settings = viewFactory.getSettings();

        settings.showAttribute = !settings.showAttribute;
        viewFactory.applySettings();
    }

    protected void toggleShowText() {
        TreeViewFactory viewFactory = 
            (TreeViewFactory) docView.getViewFactory();
        TreeViewFactory.Settings settings = viewFactory.getSettings();

        settings.showText = !settings.showText;
        viewFactory.applySettings();
    }

    protected void toggleShowComment() {
        TreeViewFactory viewFactory = 
            (TreeViewFactory) docView.getViewFactory();
        TreeViewFactory.Settings settings = viewFactory.getSettings();

        settings.showComment = !settings.showComment;
        viewFactory.applySettings();
    }

    protected void toggleShowPI() {
        TreeViewFactory viewFactory = 
            (TreeViewFactory) docView.getViewFactory();
        TreeViewFactory.Settings settings = viewFactory.getSettings();

        settings.showPI = !settings.showPI;
        viewFactory.applySettings();
    }

    // -----------------------------------
    // Help
    // -----------------------------------

    protected void help() {
        StringBuilder html = new StringBuilder();

        html.append("<html><body><table border='1'>\n");
        html.append("<tr><th nowrap='nowrap'>User Input</th>");
        html.append("<th nowrap='nowrap'>Command</th>");
        html.append("<th nowrap='nowrap'>Parameter</th></tr>\n");

        String[][] descriptions = 
            Commands.getDescriptions(docView.getBindings());
        for (int i = 0; i < descriptions.length; ++i) {
            String[] description = descriptions[i];

            html.append("<tr><td nowrap='nowrap'>");
            XMLText.escapeXML(description[0], html);
            html.append("</td><td nowrap='nowrap'>");
            XMLText.escapeXML(description[1], html);
            html.append("</td><td nowrap='nowrap'>");
            if (description[2].length() == 0)
                html.append("&nbsp;");
            else
                XMLText.escapeXML(description[2], html);
            html.append("</td></tr>\n");
        }

        html.append("</table></body></html>");

        Alert.showInfoHTML(frame, "Mouse and keyboard bindings:", 
                           html.toString(), 500, 300);
    }

    // -----------------------------------------------------------------------
    // DocumentListener
    // -----------------------------------------------------------------------

    public void textChanged(com.xmlmind.xml.doc.TextEvent event, 
                            int listenerIndex) {
        if (!needSave)
            setNeedSave();
    }

    protected void setNeedSave() {
        needSave = true;
        updateTitle();
        getMenuItem(FILE_MENU, SAVE_ITEM).setEnabled(true);
    }

    protected void updateTitle() {
        StringBuilder title = new StringBuilder();

        if (docFile == null) {
            title.append("SimpleEditor");
        } else {
            title.append(docFile);
            if (needSave) 
                title.append(" (modified)");
        }

        frame.setTitle(title.toString());
    }

    public void attributeChanged(AttributeEvent event, int listenerIndex) {
        if (!needSave)
            setNeedSave();
    }

    public void treeChanged(TreeEvent event, int listenerIndex) {
        if (!needSave)
            setNeedSave();
    }

    public void processingInstructionChanged(ProcessingInstructionEvent event, 
                                             int listenerIndex) {
        if (!needSave)
            setNeedSave();
    }

    public void eventHappened(DocumentEvent event, int listenerIndex) {
        if (event instanceof PendingChangesEvent) {
            if (!needSave)
                setNeedSave();
        } else if (event instanceof PrefixesChangedEvent) {
            prefixesChanged();
        }
    }
    
    public void inclusionUpdated(InclusionUpdatedEvent event,
                                 int listenerIndex) {}
    public void editStarted(EditEvent event, int listenerIndex) {}
    public void editCompleted(EditEvent event, int listenerIndex) {}

    // -----------------------------------------------------------------------
    // ContextChangeListener
    // -----------------------------------------------------------------------

    public void contextChanged(ContextChangeEvent event) {
        Node selNode = null;
        Element selElement = null;

        MarkManager markManager = docView.getMarkManager();
        if (markManager != null) {
            NodeMark selected = markManager.getSelected();
            NodeMark selected2 = markManager.getSelected2();
            TextLocation dot = markManager.getDot();
            TextLocation mark = markManager.getMark();

            if (selected == null) {
                if (dot != null) {
                    selNode = dot.getNode();

                    if (mark == null ||
                        (mark.getTextNode() == selNode &&
                         mark.getOffset() == dot.getOffset())) {
                        selElement = selNode.getParentElement();
                    }
                }
            } else {
                selNode = selected.getNode();

                if (selected2 == null &&
                    selNode.getType() == Node.Type.ELEMENT) {
                    selElement = (Element) selNode;
                }
            }
        }

        nodePath.setNode(selNode);

        if (attributeTool != null)
            attributeTool.setElement(selElement);
    }

    // -----------------------------------------------------------------------
    // Console
    // -----------------------------------------------------------------------

    public void showMessage(String message, Console.MessageType messageType) {
        setMessage(message);
    }

    // -----------------------------------------------------------------------
    // Status bar message
    // -----------------------------------------------------------------------

    protected void setMessage(String text) {
        setMessage(text, true);
    }

    protected void setMessage(String text, boolean autoErase) {
        if (messageTimeout != null) {
            messageTimeout.stop();
            messageTimeout = null;
        }

        if (text == null)
            text = "";
        message.setText(text);
        message.paintImmediately(0, 0, 1000, 1000);

        if (autoErase && text.length() > 0) {
            messageTimeout = 
                new Timer(10000 /*ms*/, new ActionListener() {
                    public void actionPerformed(ActionEvent e) {
                        message.setText("");
                    }
                });
            messageTimeout.setRepeats(false);
            messageTimeout.start();
        }
    }

    // -----------------------------------------------------------------------
    // ShowStatus.StatusWindow
    // -----------------------------------------------------------------------

    public void showStatus(String message) {
        setMessage(message);
    }
}
