Chapter 15. A simple XML editor

Table of Contents

1. Compiling and running the code sample
2. Assembling the GUI
3. The NodePath component
3.1. What is a NodePath?
3.2. Using the NodePath to trigger Commands
3.3. Using menus and tool bars to trigger Commands
4. Loading a document
4.1. Opening a document
4.2. Closing a document
5. Updating the GUI
5.1. Enabling and disabling menu items
5.2. Being notified when the document is modified
5.3. Being notified when the editing context changes
6. Saving a document
7. Validating a document
8. Printing a document

1. Compiling and running the code sample

Figure 15.1. SimpleEditor

SimpleEditor

  • Compile SimpleEditor by executing ant (see build.xml) in the samples/simple_editor/ directory.

  • Run SimpleEditor by executing ant run in the samples/simple_editor/ directory. This will start SimpleEditor and load sample XHTML document samples/tests/in/sample1.html.

The code is too long, about 1,300 lines of Java™ code, to be displayed in its entirety in this document. We'll just show here excerpts from SimpleEditor.java.

2. Assembling the GUI

The main window of SimpleEditor is created as follows:

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

        frame = new JFrame("SimpleEditor");
        frame.setIconImage(ImageResource.get(getClass(), 
                                             "icons/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();1
        com.xmlmind.xmleditapp.cmd.RegisterAll.registerAll();

        docPane = new StyledDocumentPane();2
        docView = docPane.getStyledDocumentView();

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

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

        // Menus ---

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

        // NodePath ---

        nodePath = new NodePath();6
        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) {
            URL imageURL =
                getClass().getResource("icons/severity" + j + ".gif");
            invalidIcons[j] = new ImageIcon(imageURL);
        }
        
        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 font = workArea.getFont();
        Color 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(frame, this);

        // Show ---

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

1

RegisterAll.registerAll simply registers all the commands defined in the com.xmlmind.xmledit.cmd.* packages.

Packages com.xmlmind.xmledit.cmd.* contain the minimum set of commands needed to create a usable XML editor. More commands are defined in packages com.xmlmind.xmleditapp.cmd.* ; and therefore com.xmlmind.xmleditapp.cmd too has his RegisterAll.registerAll utility.

Registering a set of commands with the command registry Commands is absolutely required before creating first DocumentPane. Without this, all the bindings (that is, keystroke bound to command, mouse click bound to command) supported by the DocumentPane would report "command not defined" errors.

2

The Swing component which is used to display the XML document is a DocumentPane (a StyledDocumentPane for a styled XML editor). A DocumentPane derives from JPanel.

A DocumentPane is just the Swing host of the real XML editor component: DocumentView (a StyledDocumentView for a styled XML editor). That is, all the APIs needed to display and edit a XML document are in DocumentView and not in DocumentPane.

The DocumentView is not an AWT Component, nor a Swing JComponent. It is a Gadget. Gadgets are graphical, interactive, components similar to AWT Components except they are truly lightweight.

A DocumentView is a RootGadget, the root of the Gadget tree. It is designed to be able to contain thousands of Gadgets, used to represent graphically the document being edited.

3

The DocumentPane is added to a JScrollPane using docPane.addToScrollPane(scroller) rather than the usual new JScrollPane(docPane). This is unfortunate but we have not found an efficient way to make using a JScrollPane to scroll a DocumentPane transparent for the programmer as it is the case for JList, JTable or JTextArea.

addToScrollPane() returns a JScrollPaneSupport, a helper class, which is needed when printing the document.

4

There is not much to say about configureMenuBar(menuBar).

    private 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);
        menu = new JMenu("Help");
                            .
                            .
                            .
        item = new JMenuItem("Help");
        item.setActionCommand("help");
        item.addActionListener(this);
        menu.add(item);

        menuBar.add(menu);
    }

    public void actionPerformed(ActionEvent event) {
        String command = event.getActionCommand();
        
        if (command.equals("openCopy")) {
            openCopy();
                            .
                            .
                            .
        } else if (command.equals("help")) {
            help();
        }
    }

5

enableMenus() is described below.

6

NodePath is described below.

3. The NodePath component

3.1. What is a NodePath?

The NodePath is a JComponent which displays the containment hierarchy of a Node in a XML Document, called the path of the Node.

Figure 15.2. NodePath

NodePath

In the figure above, the caret is inside a Text node (or a Text node is explicitly selected) which is contained in a p Element, which itself is contained in a body Element and so forth.

The ``proprietary'' DOM (Document Object Model) used by XXE is explained in Programming the Document Object Model.

A NodePath is an indispensable component when a styled view of the document is displayed. Without it, it is often impossible to tell what is the node being edited. That is, without it, a tree view must be displayed along the styled view.

3.2. Using the NodePath to trigger Commands

A NodePath is also an interactive component. SimpleEditor has registered itself with the NodePath as a NodePathListener. Method nodeSelected(NodePathEvent) is invoked by the NodePath each time user clicks on a name in the NodePath.

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

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

This gives the opportunity to associate actions to user input inside the NodePath:

1

Clicking on a name such as body or #text, selects corresponding node.

2

Shift-clicking on an element name such as p, selects corresponding element, then triggers Command named "insertNode" with parameter "sameElementBefore".

This Command, with this parameter, inserts an element of the same type than selected element, before selected element.

3

Ctrl-clicking on an element name such as p, selects corresponding node, then triggers Command named "insertNode" with parameter "sameElementAfter".

This Command, with this parameter, inserts an element of the same type than selected element, after selected element.

DocumentView.executeCommand(commandName, commandParameter, mouseX, mouseY) is a convenience function which is equivalent to:

if (command.prepareCommand(docView, commandParameter, mouseX, mouseY)) {
    command.executeCommand(docView, commandParameter, mouseX, mouseY);
}
prepareCommand

Returns true if command can be executed given the context of its invocation: the content of the Document displayed by the DocumentView, the Marks, the content of the clipboard, etc. Returns false otherwise.

executeCommand

Really does the job. Assumes that prepareCommand has just returned true.

What is a Command, how to write one, how to associate a Command to a keyboard or mouse input is detailed in Writing a command.

3.3. Using menus and tool bars to trigger Commands

Note that SimpleEditor has no Edit menu, nor a tool bar that can be used to edit the XML document. The user must use standard Edit popup menu displayed when right mouse button is clicked (or standard keyboard shortcuts such as Ctrl+X) in order to do so.

Figure 15.3. Edit popup menu

Edit popup menu

If SimpleEditor had an Edit menu or a tool bar, the corresponding JMenuItems or (Swing Action objects) would also invoke Commands in a way which is similar to what is done in nodeSelected().

For example, what follows is the kind of Swing Actions used by the actual XMLmind XML Editor (that is, not by SimpleEditor) in its GUI:

public class EditAction extends AppAction {
    protected String commandName;
    protected String parameter;
    protected boolean editingContextSensitive;
    protected Command command;

    public EditAction(String commandName, String parameter,
                      boolean editingContextSensitive) {
        this.commandName = commandName;
        this.parameter = parameter;
        this.editingContextSensitive = editingContextSensitive;

        command = Commands.getCommand(commandName);
        if (command == null) {
            System.err.println("'" + commandName + "' unknown command");
            command = DisabledCommand.INSTANCE;
        }
    }

    public String getCommandName() {
        return commandName;
    }

    public String getParameter() {
        return parameter;
    }

    public void doIt() {
        DocumentView docView = app.getActiveDocumentView();
        if (docView == null) {
            return;
        }

        if (command.prepareCommand(docView, parameter, -1, -1)) {
            command.executeCommand(docView, parameter, -1, -1);
            docView.requestFocus();
        }
    }

    public void updateEnabled() {
        boolean enabled;
        DocumentView docView = app.getActiveDocumentView();
        if (docView == null) {
            enabled = false;
        } else {
            enabled = command.prepareCommand(docView, parameter, -1, -1);
        }

        setEnabled(enabled);
    }

    public boolean isEditingContextSensitive() {
        return editingContextSensitive;
    }
}

4. Loading a document

Opening a document is basically invoking docView.setDocument(doc) and closing a document is basically invoking docView.setDocument(null).

4.1. Opening a document

    private void open(File file) {
        Document doc = loadDocument(file);1
        if (doc == null) {
            return;
        }

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

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

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

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

            StyleSheetInfo[] prop = StyleSheetInfo.loadStyleSheetPI(doc);
            if (prop != null) {
                doc.putProperty(STYLE_SHEET_INFO_PROPERTY, prop);
            }
        }

        setMessage(null);

        return doc;
    }

    private void setDocument(Document doc) {6
        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();
    }

    private void uninstallDocument(Document doc) {
        SendDocumentEvent.closingDocument(doc);7

        doc.removeDocumentListener(this);8

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

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

        uninstallStyleSheets();
        setStyle(-1);
    }

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

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

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

        if (installStyleSheets(doc)) {
            setStyle(0);
        }
    }

1 2

Opening a document is done by first creating a Document object from the XML source, then by invoking docView.setDocument, passing the newly created Document to the DocumentView.

4

Creating a Document from a XML source is the job of the LoadDocument.

com.xmlmind.xmledit.cmd.include.LoadDocument is just a convenience class that invokes LoadDocument.load and displays a dialog box when LoadDocument.load reports some errors.

LoadDocument has the following features:

  • It can be programmed to provide a user feedback during document loading which is a rather lengthy operation. This is done by specifying a Console which is notified by the LoadDocument for each important step that occurs during document loading.

    SimpleEditor implements Console:

    public void showMessage(String message, Console.MessageType messageType) {
        setMessage(message);
    }
  • It systematically adds a DocumentType to loaded document.

    Attaching objects to a document is done by the means of Node properties. For example, the DocumentType of a Document, if any, may be obtained by invoking doc.getProperty(DOCUMENT_TYPE_PROPERTY).

  • Using DocumentType, it can intelligently strip ignorable whitespaces from loaded document.

  • It is XML catalog aware. Note that in build.xml we use system property xml.catalog.files to specify to these loaders which catalogs to use. This can also be done programmatically using XMLCatalogs.

  • It can cache the newly loaded and validated DocumentType in order to be able to reuse it if the same grammar constrains another document.

    DocumentTypes can even be serialized in order to be used in subsequent XML editing sessions. Doing so improves document loading speed when the document being loaded is conforming to a large DTD such as DocBook or to a mid-size W3C XML Schema.

    SimpleEditor uses a document type cache if command line option -s is specified:

    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);
    }

5

Attaching a number of objects CommandHistory, UndoManager, MarkManager to the newly opened document is absolutely required when this document is to be edited.

6

setDocument() is a wrapper around DocumentView.setDocument, which removes SimpleEditor as a listener from previously loaded document (if any) and adds SimpleEditor as a listener to newly loaded document (if any).

SimpleEditor is both a DocumentListener and a ContextChangeListener. These two interfaces are explained in Updating the GUI.

3

A document having a DocumentType may have validity errors. These validity errors are handled by updateValidity(), which is detailed in Validating the Document.

7

Not strictly needed. Notifies all the DocumentListeners that the document they are listening to is about to be closed. This allows them to clean-up their internal state if needed to.

8

Not strictly needed. Just cleaner.

4.2. Closing a document

Closing a document is done as follows:

    private void close() {
        if (!checkNeedSave()) {
            return;
        }

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

5. Updating the GUI

5.1. Enabling and disabling menu items

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

        getMenuItem(FILE_MENU, CLOSE_ITEM).setEnabled(docLoaded);1
        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) {2
            canCheck = DiagnosticUtil.canCheckDocument(doc);
        } else {
            canCheck = false;
        }
        getMenuItem(TOOLS_MENU, CHECK_VALIDITY_ITEM).setEnabled(canCheck);

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

1

The logic behind enableMenus() is extremely simple: some menu items cannot be used if a document is not being loaded in the DocumentView.

2

Document cannot be validated if it is not conforming to a schema. See convenience functions in DiagnosticUtil.

3

NamespacePrefixMap is one of the properties added by the LoadDocument to the Document at load time. This interface specifies how namespaces are mapped to prefixes and conversely how prefixes are mapped to namespaces. This interface is implemented by utility class PrefixPreferences.

An editor is available to edit a PrefixPreferences map: The PrefixPreferencesEditor which is also available as a modal dialog box, the PrefixPreferencesDialog.

Figure 15.4. PreferredPrefixDialog

PreferredPrefixDialog

This editor can be used to declare a namespace or to change the ``prefix'' used to represent a namespace (simply because the URI of the namespace is too long to be displayed in the GUI; therefore a better name than ``prefix'' would be short name, nick name or mnemonic).

5.2. Being notified when the document is modified

The Document notifies its DocumentListeners each time it is being modified.

SimpleEditor implements the DocumentListener interface to detect when the document being edited needs to be saved.

SimpleEditor implements the DocumentListener interface as follows:

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

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

    private 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 elementNameChanged(ElementNameEvent event, int listenerIndex) {
        if (!needSave) {
            setNeedSave();
        }
    }

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

    public void eventHappened(DocumentEvent event, int listenerIndex) {
        .
        .
        .
    }
    
    public void inclusionUpdated(InclusionUpdatedEvent event,
                                 int listenerIndex) {}
    public void editStarted(EditEvent event, int listenerIndex) {}
    public void editCompleted(EditEvent event, int listenerIndex) {}

5.3. Being notified when the editing context changes

The MarkManager notifies its ContextChangeListeners each time editing context changes.

Editing context changes when the Marks added to document Nodes change. Marks specify selected objects on which Commands can act. Therefore, when the Marks change, some Commands (and therefore corresponding menu items or tool bar buttons in the GUI) may have to be disabled while other Commands (and therefore corresponding menu items or tool bar buttons in the GUI) may have to be enabled.

SimpleEditor needs to know when the node being edited changes to inform the NodePath and the AttributeTool, a non-modal dialog box containing an attribute editor.

SimpleEditor implements the ContextChangeListener interface as follows:

    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);
        }
    }

DocumentView can be used to edit the node tree as well as textual content of the nodes. It cannot be used to edit the attributes of a element. Editing element attributes is the job of the AttributeEditor, which is also available as a non-modal dialog box called the AttributeTool.

Figure 15.5. AttributeTool

AttributeTool

6. Saving a document

Saving the document being edited is done using SaveDocument.

This convenience class uses DocumentType-aware DocumentIndenter to output indented XML or low-level DocumentWriter which, not being DocumentType-aware, cannot output indented XML.

    private void save(File file, boolean saveAs) {
        setMessage("Saving document '" + file + "'...", false);

        Document doc = docView.getDocument();
        if (!SendDocumentEvent.commitChanges(doc) ||
            !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();
        }
    }

    private 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;
        }
    }

7. Validating a document

If a document is constrained by a grammar, the validity of this document is automatically checked when the document is opened and each time the document is saved.

The validity state of a document is represented by a button displaying an icon at the bottom/left of SimpleEditor window.

A non-modal dialog box, the DiagnosticsTool can be opened at any time, using menu item ToolsCheck Validity, to explicitly check the validity of the document being edited and to display the list of found error messages, if any.

Figure 15.6. DiagnosticsTool

DiagnosticsTool

DiagnosticsTool is also available as an embeddable panel, DiagnosticsPane, rather than a dialog box.

    private void checkValidity() {
        Document doc = docView.getDocument();
        if (!SendDocumentEvent.commitChanges(doc)) {
            // Give up.
            return;
        }

        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);
    }

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

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

8. Printing a document

Printing the document being edited is done using a GadgetPrinter.

    private 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);1
        printer.setFooterBegin(docFile.getPath());2
        printer.setFooterEnd("%I/%C");
        printer.setFooterFont(new Font("SansSerif", Font.PLAIN, 10));
        printer.setFooterColor(Color.gray);
        printer.setFooterOverlined(true);

        scrollPaneSupport.setViewportWidthTracked(false);3
        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);
        }
    }

1

SimpleEditor implements the PrintInvoker interface. This interface is implemented by applications that provide a feedback to the user while the document is being printed.

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

This method can also be used to cancel printing but SimpleEditor does not implement this feature.

2

GadgetPrinter allows to add a header and/or a footer to each printed page.

3

This ``special canvas'' has the shape of the paper the Gadget is to be printed on. This shape is almost always different from the original, on screen, shape of the DocumentView.

That's why scrollPaneSupport.setViewportWidthTracked(false) is invoked. Without this invocation, the JScrollPane containing the DocumentPane/DocumentView would interfere badly when the DocumentView is temporarily (between printer.begin() and printer.end()) given the shape of the paper.