001/* Top-level window containing a simple text editor or viewer.
002
003 Copyright (c) 1998-2016 The Regents of the University of California.
004 All rights reserved.
005 Permission is hereby granted, without written agreement and without
006 license or royalty fees, to use, copy, modify, and distribute this
007 software and its documentation for any purpose, provided that the above
008 copyright notice and the following two paragraphs appear in all copies
009 of this software.
010
011 IN NO EVENT SHALL THE UNIVERSITY OF CALIFORNIA BE LIABLE TO ANY PARTY
012 FOR DIRECT, INDIRECT, SPECIAL, INCIDENTAL, OR CONSEQUENTIAL DAMAGES
013 ARISING OUT OF THE USE OF THIS SOFTWARE AND ITS DOCUMENTATION, EVEN IF
014 THE UNIVERSITY OF CALIFORNIA HAS BEEN ADVISED OF THE POSSIBILITY OF
015 SUCH DAMAGE.
016
017 THE UNIVERSITY OF CALIFORNIA SPECIFICALLY DISCLAIMS ANY WARRANTIES,
018 INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
019 MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE SOFTWARE
020 PROVIDED HEREUNDER IS ON AN "AS IS" BASIS, AND THE UNIVERSITY OF
021 CALIFORNIA HAS NO OBLIGATION TO PROVIDE MAINTENANCE, SUPPORT, UPDATES,
022 ENHANCEMENTS, OR MODIFICATIONS.
023
024 PT_COPYRIGHT_VERSION_2
025 COPYRIGHTENDKEY
026
027 */
028package ptolemy.actor.gui;
029
030import java.awt.BorderLayout;
031import java.awt.Color;
032import java.awt.Dimension;
033import java.awt.Graphics;
034import java.awt.Graphics2D;
035import java.awt.Point;
036import java.awt.Rectangle;
037import java.awt.RenderingHints;
038import java.awt.Toolkit;
039import java.awt.event.ActionEvent;
040import java.awt.event.KeyEvent;
041import java.awt.image.BufferedImage;
042import java.awt.print.PageFormat;
043import java.awt.print.Printable;
044import java.awt.print.PrinterException;
045import java.io.File;
046import java.io.FileOutputStream;
047import java.io.IOException;
048import java.io.OutputStream;
049import java.util.LinkedList;
050import java.util.Locale;
051
052import javax.imageio.ImageIO;
053import javax.swing.AbstractAction;
054import javax.swing.Action;
055import javax.swing.JFileChooser;
056import javax.swing.JMenu;
057import javax.swing.JMenuItem;
058import javax.swing.JOptionPane;
059import javax.swing.JScrollPane;
060import javax.swing.JTextArea;
061import javax.swing.KeyStroke;
062import javax.swing.event.DocumentEvent;
063import javax.swing.event.DocumentListener;
064import javax.swing.text.BadLocationException;
065import javax.swing.text.DefaultHighlighter;
066import javax.swing.text.Document;
067import javax.swing.text.Highlighter;
068import javax.swing.text.Highlighter.Highlight;
069import javax.swing.undo.CannotRedoException;
070import javax.swing.undo.CannotUndoException;
071
072import diva.gui.GUIUtilities;
073import ptolemy.actor.injection.PortablePlaceable;
074import ptolemy.gui.ComponentDialog;
075import ptolemy.gui.ExtensionFilenameFilter;
076import ptolemy.gui.ImageExportable;
077import ptolemy.gui.JFileChooserBugFix;
078import ptolemy.gui.Query;
079import ptolemy.gui.QueryListener;
080import ptolemy.gui.UndoListener;
081import ptolemy.util.MessageHandler;
082import ptolemy.util.StringUtilities;
083
084///////////////////////////////////////////////////////////////////
085//// TextEditor
086
087/**
088
089 A top-level window containing a simple text editor or viewer.
090 You can access the public member text to set the text, get the text,
091 or set the number of rows or columns.
092 After creating this, it is necessary to call show() for it to appear.
093
094 @author Edward A. Lee, contributors: Christopher Brooks, Ben Leinfelder
095 @version $Id$
096 @since Ptolemy II 1.0
097 @Pt.ProposedRating Yellow (eal)
098 @Pt.AcceptedRating Red (eal)
099 */
100@SuppressWarnings("serial")
101public class TextEditor extends TableauFrame
102        implements DocumentListener, ImageExportable, Printable, QueryListener {
103
104    /** Construct an empty text editor with no name.
105     *  After constructing this, it is necessary
106     *  to call setVisible(true) to make the frame appear.
107     */
108    public TextEditor() {
109        this("Unnamed");
110    }
111
112    /** Construct an empty text editor with the specified title.
113     *  After constructing this, it is necessary
114     *  to call setVisible(true) to make the frame appear.
115     *  @param title The title to put in the title bar.
116     */
117    public TextEditor(String title) {
118        this(title, null);
119    }
120
121    /** Construct an empty text editor with the specified title and
122     *  document.  After constructing this, it is necessary
123     *  to call setVisible(true) to make the frame appear.
124     *  @param title The title to put in the title bar.
125     *  @param document The document containing text, or null if none.
126     */
127    public TextEditor(String title, Document document) {
128        this(title, document, (Placeable) null);
129    }
130
131    /** Construct an empty text editor with the specified title and
132     *  document and associated placeable.  After constructing this,
133     *  it is necessary to call setVisible(true) to make the frame
134     *  appear.
135     *  @param title The title to put in the title bar.
136     *  @param document The document containing text, or null if none.
137     *  @param placeable The associated placeable.
138     */
139    public TextEditor(String title, Document document, Placeable placeable) {
140        // NOTE: Create with no status bar, since we have no use for it now.
141        super(null, null, placeable);
142        _init(title, document);
143    }
144
145    /** Construct an empty text editor with the specified title and
146     *  document and associated poratalbeplaceable.  After constructing this,
147     *  it is necessary to call setVisible(true) to make the frame
148     *  appear.
149     *  @param title The title to put in the title bar.
150     *  @param document The document containing text, or null if none.
151     *  @param portablePlaceable The associated PortablePlaceable.
152     */
153    public TextEditor(String title, Document document,
154            PortablePlaceable portablePlaceable) {
155        // NOTE: Create with no status bar, since we have no use for it now.
156        super(null, null, portablePlaceable);
157        _init(title, document);
158    }
159
160    ///////////////////////////////////////////////////////////////////
161    ////                         public variables                  ////
162
163    /** The text area. */
164    public JTextArea text;
165
166    ///////////////////////////////////////////////////////////////////
167    ////                         public methods                    ////
168
169    /** Allow subclasses to adjust the file menu after packing.
170     *  This has to be called after pack().
171     *  This base class does nothing.
172     */
173    public void adjustFileMenu() {
174    }
175
176    /** React to a change in the find-and-replace query.
177     *  @param name The field that changed.
178     */
179    @Override
180    public void changed(String name) {
181        if (_query != null) {
182            switch (name) {
183            case "Find":
184                Highlighter highlighter = text.getHighlighter();
185                highlighter.removeAllHighlights();
186
187                String textValue = text.getText();
188                String search = _query.getStringValue("Find");
189                _previousSearch = search;
190                int start = text.getCaretPosition();
191                int location = textValue.indexOf(search, start);
192                // If nothing is found from the start position, search again from the top.
193                if (location < 0) {
194                    location = textValue.indexOf(search);
195                }
196                if (location >= 0) {
197                    int firstMatch = location;
198                    // Highlight the first match.
199                    int end = location + search.length();
200                    Highlighter.HighlightPainter painter = new DefaultHighlighter.DefaultHighlightPainter(
201                            Color.YELLOW);
202                    try {
203                        highlighter.addHighlight(location, end, painter);
204                    } catch (BadLocationException e1) {
205                        // Ignore. Should not occur.
206                    }
207
208                    // Select the first match.
209                    text.setCaretPosition(location);
210                    text.moveCaretPosition(end);
211                    // Highlight the remaining matches.
212                    painter = new DefaultHighlighter.DefaultHighlightPainter(
213                            Color.CYAN);
214                    int count = 1;
215                    while (location >= 0 && end < textValue.length()) {
216                        location = textValue.indexOf(search, end);
217                        if (location >= 0) {
218                            count++;
219                            end = location + search.length();
220                            try {
221                                highlighter.addHighlight(location, end,
222                                        painter);
223                            } catch (BadLocationException e) {
224                                // Ignore. Should not occur.
225                            }
226                        }
227                    }
228                    // Reached the end. Search again from the top.
229                    location = textValue.indexOf(search);
230                    while (location >= 0 && location < firstMatch) {
231                        count++;
232                        end = location + search.length();
233                        try {
234                            highlighter.addHighlight(location, end, painter);
235                        } catch (BadLocationException e) {
236                            // Ignore. Should not occur.
237                        }
238                        location = textValue.indexOf(search, end);
239                    }
240                    _query.set("Result", "Found " + count
241                            + ((count > 1) ? " matches" : " match"));
242                } else {
243                    _query.set("Result", "Not found");
244                }
245                return;
246            case "Replacement":
247                _previousReplacement = _query.getStringValue("Replacement");
248                return;
249            }
250        }
251    }
252
253    /** React to notification that an attribute or set of attributes
254     *  changed.
255     */
256    @Override
257    public void changedUpdate(DocumentEvent e) {
258        // Do nothing... We don't care about attributes.
259    }
260
261    /** Dispose of this frame.
262     *     Override this dispose() method to unattach any listeners that may keep
263     *  this model from getting garbage collected.  This method invokes the
264     *  dispose() method of the superclass,
265     *  {@link ptolemy.actor.gui.TableauFrame}.
266     */
267    @Override
268    public void dispose() {
269        if (_debugClosing) {
270            System.out.println("TextEditor.dispose() : " + this.getName());
271        }
272
273        super.dispose();
274    }
275
276    /** Get the background color.
277     *  @return The background color of the scroll pane.
278     *  If _scrollPane is null, then null is returned.
279     *  @see #setBackground(Color)
280     */
281    @Override
282    public Color getBackground() {
283        // Under Java 1.7 on the Mac, the _scrollbar is sometimes null.
284        // See http://bugzilla.ecoinformatics.org/show_bug.cgi?id=5574
285        if (_scrollPane != null) {
286            return _scrollPane.getBackground();
287        } else {
288            return null;
289        }
290    }
291
292    /** Return the scroll pane, if there is one, and null if not.
293     *  @return The scroll pane.
294     */
295    public JScrollPane getScrollPane() {
296        return _scrollPane;
297    }
298
299    // CONTRIBUTED CODE.  The exportImage() methods are from PlotBox,
300    // which says:
301
302    // I wanted the ability to use the Plot object in a servlet and to
303    // write out the resultant images. The following routines,
304    // particularly exportImage(), permit this. I also had to make some
305    // minor changes elsewhere. Rob Kroeger, May 2001.
306
307    // NOTE: This code has been modified by EAL to conform with Ptolemy II
308    // coding style.
309
310    /** Create a BufferedImage and draw this plot to it.
311     *  The size of the returned image matches the current size of the plot.
312     *  This method can be used, for
313     *  example, by a servlet to produce an image, rather than
314     *  requiring an applet to instantiate a PlotBox.
315     *  @return An image filled by the plot.
316     */
317    public synchronized BufferedImage exportImage() {
318        Dimension dimension = getSize();
319        Rectangle rectangle = new Rectangle(dimension.height, dimension.width);
320        return exportImage(
321                new BufferedImage(rectangle.width, rectangle.height,
322                        BufferedImage.TYPE_INT_ARGB),
323                rectangle, _defaultImageRenderingHints(), false);
324    }
325
326    /** Draw this plot onto the specified image at the position of the
327     *  specified rectangle with the size of the specified rectangle.
328     *  The plot is rendered using anti-aliasing.
329     *  This can be used to paint a number of different
330     *  plots onto a single buffered image.  This method can be used, for
331     *  example, by a servlet to produce an image, rather than
332     *  requiring an applet to instantiate a PlotBox.
333     *  @param bufferedImage Image onto which the plot is drawn.
334     *  @param rectangle The size and position of the plot in the image.
335     *  @param hints Rendering hints for this plot.
336     *  @param transparent Indicator that the background of the plot
337     *   should not be painted.
338     *  @return The modified bufferedImage.
339     */
340    public synchronized BufferedImage exportImage(BufferedImage bufferedImage,
341            Rectangle rectangle, RenderingHints hints, boolean transparent) {
342        Graphics2D graphics = bufferedImage.createGraphics();
343        graphics.addRenderingHints(_defaultImageRenderingHints());
344
345        if (!transparent) {
346            graphics.setColor(Color.white); // set the background color
347            graphics.fill(rectangle);
348        }
349
350        print(graphics, rectangle);
351        return bufferedImage;
352    }
353
354    /** Export an image of the plot in the specified format.
355     *  If the specified format is not supported, then pop up a message
356     *  window apologizing.
357     *  @param out An output stream to which to send the description.
358     *  @param formatName A format name, such as "gif" or "png".
359     */
360    public synchronized void exportImage(OutputStream out, String formatName) {
361        try {
362            boolean match = false;
363            String[] supportedFormats = ImageIO.getWriterFormatNames();
364            for (String supportedFormat : supportedFormats) {
365                if (formatName.equalsIgnoreCase(supportedFormat)) {
366                    match = true;
367                    break;
368                }
369            }
370            if (!match) {
371                // This exception is caught and reported below.
372                throw new Exception("Format " + formatName + " not supported.");
373            }
374            BufferedImage image = exportImage();
375            if (out == null) {
376                // FIXME: Write image to the clipboard.
377                // final Clipboard clipboard = getToolkit().getSystemClipboard();
378                String message = "Copy to the clipboard is not implemented yet.";
379                JOptionPane.showMessageDialog(this, message,
380                        "Ptolemy Plot Message", JOptionPane.ERROR_MESSAGE);
381                return;
382            }
383            ImageIO.write(image, formatName, out);
384        } catch (Exception ex) {
385            String message = "Export failed: " + ex.getMessage();
386            JOptionPane.showMessageDialog(this, message, "Ptolemy Plot Message",
387                    JOptionPane.ERROR_MESSAGE);
388
389            // Rethrow the exception so that we don't report success,
390            // and so the stack trace is displayed on standard out.
391            throw (RuntimeException) ex.fillInStackTrace();
392        }
393    }
394
395    /** React to notification that there was an insert into the document.
396     */
397    @Override
398    public void insertUpdate(DocumentEvent e) {
399        setModified(true);
400    }
401
402    /** Print the text to a printer, which is represented by the
403     *  specified graphics object.
404     *  @param graphics The context into which the page is drawn.
405     *  @param format The size and orientation of the page being drawn.
406     *  @param index The zero based index of the page to be drawn.
407     *  @return PAGE_EXISTS if the page is rendered successfully, or
408     *   NO_SUCH_PAGE if pageIndex specifies a non-existent page.
409     *  @exception PrinterException If the print job is terminated.
410     */
411    @Override
412    public int print(Graphics graphics, PageFormat format, int index)
413            throws PrinterException {
414        if (graphics == null) {
415            return Printable.NO_SUCH_PAGE;
416        }
417
418        Graphics2D graphics2D = (Graphics2D) graphics;
419
420        double bottomMargin = format.getHeight() - format.getImageableHeight()
421                - format.getImageableY();
422
423        double lineHeight = graphics2D.getFontMetrics().getHeight()
424                - graphics2D.getFontMetrics().getLeading() / 2;
425
426        int linesPerPage = (int) Math.floor(
427                (format.getHeight() - format.getImageableY() - bottomMargin)
428                        / lineHeight);
429
430        int lineYPosition = (int) Math
431                .ceil(format.getImageableY() + lineHeight);
432
433        return _print(graphics2D, index, linesPerPage, lineHeight,
434                (int) format.getImageableX(), lineYPosition,
435                format.getHeight() - bottomMargin);
436
437    }
438
439    /** Print the text to a printer, which is represented by the
440     *  specified graphics object.
441     *  @param graphics The context into which the page is drawn.
442     *  @param drawRect specification of the size.
443     *  @return PAGE_EXISTS if the page is rendered successfully, or
444     *   NO_SUCH_PAGE if pageIndex specifies a non-existent page.
445     */
446    public int print(Graphics graphics, Rectangle drawRect) {
447        if (graphics == null) {
448            return Printable.NO_SUCH_PAGE;
449        }
450
451        graphics.setPaintMode();
452
453        Graphics2D graphics2D = (Graphics2D) graphics;
454
455        // Loosely based on
456        // http://forum.java.sun.com/thread.jspa?threadID=217823&messageID=2361189
457        // Found it unwise to use the TextArea font's size,
458        // We area just printing text so use a a font size that will
459        // be generally useful.
460        graphics2D.setFont(getFont().deriveFont(9.0f));
461
462        // FIXME: we should probably get the color somehow.  Probably exportImage() is being
463        // called with transparent set to false?
464        graphics2D.setColor(java.awt.Color.BLACK);
465
466        // FIXME: Magic Number 5, similar to what is in PlotBox.
467        double bottomMargin = 5;
468
469        double lineHeight = graphics2D.getFontMetrics().getHeight()
470                - graphics2D.getFontMetrics().getLeading() / 2;
471
472        int linesPerPage = (int) Math
473                .floor((drawRect.height - bottomMargin) / lineHeight);
474
475        int lineYPosition = (int) Math.ceil(lineHeight);
476
477        return _print(graphics2D, 0 /* page */, linesPerPage, lineHeight, 0,
478                lineYPosition, drawRect.height - bottomMargin);
479    }
480
481    /** React to notification that there was a removal from the document.
482     */
483    @Override
484    public void removeUpdate(DocumentEvent e) {
485        setModified(true);
486    }
487
488    /** Scroll as necessary so that the last line is visible.
489     */
490    public void scrollToEnd() {
491        // Song and dance to scroll to the new line.
492        text.scrollRectToVisible(new Rectangle(new Point(0, text.getHeight())));
493    }
494
495    /** Set background color.  This overrides the base class to set the
496     *  background of contained scroll pane and text area.
497     *  @param background The background color.
498     *  @see #getBackground()
499     */
500    @Override
501    public void setBackground(Color background) {
502        super.setBackground(background);
503
504        // This seems to be called in a base class constructor, before
505        // this variable has been set. Hence the test against null.
506        if (_scrollPane != null) {
507            _scrollPane.setBackground(background);
508        }
509
510        if (text != null) {
511            // NOTE: Should the background always be white?
512            text.setBackground(background);
513        }
514    }
515
516    /** Write an image to the specified output stream in the specified
517     *  format.  Supported formats include at least "gif" and "png",
518     *  standard image file formats.  The image is a rendition of the
519     *  current view of the model.
520     *  @param stream The output stream to write to.
521     *  @param format The image format to generate.
522     *  @exception IOException If writing to the stream fails.
523     *  @exception PrinterException  If the specified format is not supported.
524     */
525    @Override
526    public void writeImage(OutputStream stream, String format)
527            throws PrinterException, IOException {
528        exportImage(stream, format);
529    }
530
531    ///////////////////////////////////////////////////////////////////
532    ////                         protected methods                 ////
533
534    /** Create an edit menu.
535     */
536    @Override
537    protected void _addMenus() {
538        super._addMenus();
539
540        _editMenu = new JMenu("Edit");
541        _editMenu.setMnemonic(KeyEvent.VK_E);
542        _menubar.add(_editMenu);
543
544        GUIUtilities.addMenuItem(_editMenu, new UndoAction());
545        GUIUtilities.addMenuItem(_editMenu, new RedoAction());
546
547        _editMenu.addSeparator();
548
549        GUIUtilities.addMenuItem(_editMenu, new CutAction());
550        GUIUtilities.addMenuItem(_editMenu, new CopyAction());
551        GUIUtilities.addMenuItem(_editMenu, new PasteAction());
552
553        _editMenu.addSeparator();
554
555        GUIUtilities.addMenuItem(_editMenu, _findAction);
556
557    }
558
559    /** Clear the current contents.  First, check to see whether
560     *  the contents have been modified, and if so, then prompt the user
561     *  to save them.  A return value of false
562     *  indicates that the user has canceled the action.
563     *  @return False if the user cancels the clear.
564     */
565    @Override
566    protected boolean _clear() {
567        if (super._clear()) {
568            text.setText("");
569            return true;
570        } else {
571            return false;
572        }
573    }
574
575    /** Create the items in the File menu's Export section
576     *  This method adds a menu items to export images of the plot
577     *  in GIF, PNG, and possibly PDF.
578     *  @return The items in the File menu.
579     */
580    @Override
581    protected JMenuItem[] _createFileMenuItems() {
582        // This method is similar to ptolemy/actor/gui/PlotTableauFrame.java, but we don't
583        // handle pdfs.
584
585        JMenuItem[] fileMenuItems = super._createFileMenuItems();
586
587        JMenu exportMenu = (JMenu) fileMenuItems[_EXPORT_MENU_INDEX];
588        exportMenu.setEnabled(true);
589
590        // Next do the export GIF action.
591        if (_exportGIFAction == null) {
592            _exportGIFAction = new ExportImageAction("GIF");
593        }
594        JMenuItem exportItem = new JMenuItem(_exportGIFAction);
595        exportMenu.add(exportItem);
596
597        // Next do the export PNG action.
598        if (_exportPNGAction == null) {
599            _exportPNGAction = new ExportImageAction("PNG");
600        }
601        exportItem = new JMenuItem(_exportPNGAction);
602        exportMenu.add(exportItem);
603
604        return fileMenuItems;
605    }
606
607    /** Find and replace. */
608    protected void _find() {
609        _query = new Query();
610        _query.addLine("Find", "Find", _previousSearch);
611        _query.addLine("Replacement", "Replacement", _previousReplacement);
612        _query.addDisplay("Result", "", "");
613
614        // If there was a previous search, perform that search now.
615        if (_previousSearch != null && _previousSearch.length() > 0) {
616            changed("Find");
617        }
618
619        _query.addQueryListener(this);
620        String[] buttons = { "Next", "Close", "Replace", "Replace and Find",
621                "Replace All" };
622        new ComponentDialog(this, "Find and Replace", _query, buttons) {
623            /** If the contents of this dialog implements the CloseListener
624             *  interface, then notify it that the window has closed, unless
625             *  notification has already been done (it is guaranteed to be done
626             *  only once).
627             */
628            @Override
629            protected void _handleClosing() {
630                switch (_buttonPressed) {
631                case "Close":
632                    super._handleClosing();
633                    return;
634                case "Next":
635                    changed("Find");
636                    return;
637                case "Replace":
638                    _undo.startCompoundEdit();
639                    text.replaceSelection(_previousReplacement);
640                    _undo.endCompoundEdit();
641                    return;
642                case "Replace and Find":
643                    _undo.startCompoundEdit();
644                    text.replaceSelection(_previousReplacement);
645                    changed("Find");
646                    _undo.endCompoundEdit();
647                    return;
648                case "Replace All":
649                    _undo.startCompoundEdit();
650                    Highlighter highlighter = text.getHighlighter();
651                    Highlight[] highlights = highlighter.getHighlights();
652                    // No idea why, but first highlight is listed twice.
653                    // Perhaps because it is the selection as well?
654                    boolean first = true;
655                    for (Highlight highlight : highlights) {
656                        if (first) {
657                            first = false;
658                            continue;
659                        }
660                        text.replaceRange(_previousReplacement,
661                                highlight.getStartOffset(),
662                                highlight.getEndOffset());
663                    }
664                    _undo.endCompoundEdit();
665                    return;
666                default:
667                    // Some other closing event, like Esc.
668                    super._handleClosing();
669                }
670            }
671        };
672        text.getHighlighter().removeAllHighlights();
673        _query = null;
674    }
675
676    /** Display more detailed information than given by _about().
677     */
678    @Override
679    protected void _help() {
680        // FIXME: Give instructions for the editor here.
681        _about();
682    }
683
684    /** Initializes an empty text editor with the specified title and
685     *  document and associated placeable.  After constructing this,
686     *  it is necessary to call setVisible(true) to make the frame
687     *  appear.
688     *
689     *  @param title The title to put in the title bar.
690     *  @param document The document containing text.
691     */
692    protected void _init(String title, Document document) {
693        setTitle(title);
694
695        text = new JTextArea(document);
696
697        // Since the document may have been null, request it...
698        document = text.getDocument();
699        document.addDocumentListener(this);
700        _scrollPane = new JScrollPane(text);
701
702        getContentPane().add(_scrollPane, BorderLayout.CENTER);
703        _initialSaveAsFileName = "data.txt";
704
705        // Set the undo listener, with default key mappings.
706        _undo = new UndoListener(text);
707        text.getDocument().addUndoableEditListener(_undo);
708    }
709
710    /** Print the contents.
711     */
712    @Override
713    protected void _print() {
714        // FIXME: What should we print?
715        super._print();
716    }
717
718    /** Redo the last undo action.
719     */
720    protected void _redo() {
721        _undo.redo();
722    }
723
724    /** Query the user for a filename, save the model to that file,
725     *  and open a new window to view the model.
726     *  This overrides the base class to use the ".txt" extension.
727     *  @return True if the save succeeds.
728     */
729    @Override
730    protected boolean _saveAs() {
731        return _saveAs(".txt");
732    }
733
734    /** Undo the last action.
735     */
736    protected void _undo() {
737        _undo.undo();
738    }
739
740    // FIXME: Listen for window closing.
741
742    ///////////////////////////////////////////////////////////////////
743    ////                         protected variables               ////
744
745    /** The edit menu. */
746    protected JMenu _editMenu;
747
748    /** The export to GIF action. */
749    protected Action _exportGIFAction;
750
751    /** The export to PNG action. */
752    protected Action _exportPNGAction;
753
754    /** The scroll pane containing the text area. */
755    protected JScrollPane _scrollPane;
756
757    /** The undo listener. */
758    protected UndoListener _undo;
759
760    ///////////////////////////////////////////////////////////////////
761    ////                         private methods                   ////
762
763    /** Return a default set of rendering hints for image export, which
764     *  specifies the use of anti-aliasing.
765     */
766    private RenderingHints _defaultImageRenderingHints() {
767        // From PlotBox
768        RenderingHints hints = new RenderingHints(null);
769        hints.put(RenderingHints.KEY_ANTIALIASING,
770                RenderingHints.VALUE_ANTIALIAS_ON);
771        return hints;
772    }
773
774    /** Print the contents of the editor to a Graphics.
775     *  This used both by the print facility and the exportImage facility.
776     *  @param graphics2D The context into which the page is drawn.
777     *  @return PAGE_EXISTS if the page is rendered successfully, or
778     *   NO_SUCH_PAGE if pageIndex specifies a non-existent page.
779     */
780    private int _print(Graphics2D graphics2D, int index, int linesPerPage,
781            double lineHeight, int lineXPosition, int linePosition,
782            double bottomLinePosition) {
783
784        int startLine = linesPerPage * index;
785
786        if (startLine > text.getLineCount()) {
787            return NO_SUCH_PAGE;
788        }
789
790        int endLine = startLine + linesPerPage;
791        for (int line = startLine; line < endLine; line++) {
792            try {
793                String linetext = text.getText(text.getLineStartOffset(line),
794                        text.getLineEndOffset(line)
795                                - text.getLineStartOffset(line));
796                graphics2D.drawString(linetext, lineXPosition, linePosition);
797            } catch (BadLocationException e) {
798                // Ignore. Never a bad location.
799            }
800
801            linePosition += lineHeight;
802            if (linePosition > bottomLinePosition) {
803                break;
804            }
805        }
806        return PAGE_EXISTS;
807    }
808
809    ///////////////////////////////////////////////////////////////////
810    ////                         private variables                 ////
811
812    /** Action to find and replace. */
813    private Action _findAction = new FindAction();
814
815    /** Find and replace query, or null if there is none. */
816    private Query _query;
817
818    /** Previous replacement string, if any. */
819    private String _previousReplacement;
820
821    /** Previous search string, if any. */
822    private String _previousSearch;
823
824    ///////////////////////////////////////////////////////////////////
825    ////                         inner classes                     ////
826
827    ///////////////////////////////////////////////////////////////////
828    //// CopyAction
829
830    /** Copy the contents of the selection and put on clipboard. */
831    private class CopyAction extends AbstractAction {
832        public CopyAction() {
833            super("Copy");
834            putValue("tooltip", "Copy to clipboard.");
835            putValue(diva.gui.GUIUtilities.ACCELERATOR_KEY,
836                    KeyStroke.getKeyStroke(KeyEvent.VK_C, Toolkit
837                            .getDefaultToolkit().getMenuShortcutKeyMask()));
838            putValue(diva.gui.GUIUtilities.MNEMONIC_KEY,
839                    Integer.valueOf(KeyEvent.VK_O));
840        }
841
842        @Override
843        public void actionPerformed(ActionEvent e) {
844            text.copy();
845        }
846    }
847
848    ///////////////////////////////////////////////////////////////////
849    //// CutAction
850
851    /** Cut the contents of the selection and put on clipboard. */
852    private class CutAction extends AbstractAction {
853        public CutAction() {
854            super("Cut");
855            putValue("tooltip", "Cut to clipboard.");
856            putValue(diva.gui.GUIUtilities.ACCELERATOR_KEY,
857                    KeyStroke.getKeyStroke(KeyEvent.VK_X, Toolkit
858                            .getDefaultToolkit().getMenuShortcutKeyMask()));
859            putValue(diva.gui.GUIUtilities.MNEMONIC_KEY,
860                    Integer.valueOf(KeyEvent.VK_C));
861        }
862
863        @Override
864        public void actionPerformed(ActionEvent e) {
865            text.cut();
866        }
867    }
868
869    ///////////////////////////////////////////////////////////////////
870    //// ExportImageAction
871
872    /** Export an image. */
873    public class ExportImageAction extends AbstractAction {
874        // FIXME: this is very similar to PlotTableaFrame.ExportImageAction.
875
876        /** Create a new action to export an image.
877         *  @param formatName The name of the format, currently PNG and
878         *  GIF are supported.
879         */
880        public ExportImageAction(String formatName) {
881            super("Export " + formatName);
882            _formatName = formatName.toLowerCase(Locale.getDefault());
883            putValue("tooltip", "Export " + formatName + " image to a file.");
884            // putValue(GUIUtilities.MNEMONIC_KEY, Integer.valueOf(KeyEvent.VK_G));
885        }
886
887        /** Export an image.
888         *  @param e The ActionEvent that invoked this action.
889         */
890        @Override
891        public void actionPerformed(ActionEvent e) {
892            JFileChooserBugFix jFileChooserBugFix = new JFileChooserBugFix();
893            Color background = null;
894            try {
895                background = jFileChooserBugFix.saveBackground();
896
897                JFileChooser fileDialog = new JFileChooser();
898                fileDialog.setDialogTitle("Specify a file to write to.");
899                LinkedList extensions = new LinkedList();
900                extensions.add(_formatName);
901                fileDialog.addChoosableFileFilter(
902                        new ExtensionFilenameFilter(extensions));
903
904                if (_directory != null) {
905                    fileDialog.setCurrentDirectory(_directory);
906                } else {
907                    // The default on Windows is to open at user.home, which is
908                    // typically an absurd directory inside the O/S installation.
909                    // So we use the current directory instead.
910                    // This will throw a security exception in an applet.
911                    // FIXME: we should support users under applets opening files
912                    // on the server.
913                    String currentWorkingDirectory = StringUtilities
914                            .getProperty("user.dir");
915                    if (currentWorkingDirectory != null) {
916                        fileDialog.setCurrentDirectory(
917                                new File(currentWorkingDirectory));
918                    }
919                }
920
921                // Here, we differ from PlotTableauFrame:
922                int returnVal = fileDialog.showDialog(TextEditor.this, "Export "
923                        + _formatName.toUpperCase(Locale.getDefault()));
924
925                if (returnVal == JFileChooser.APPROVE_OPTION) {
926                    _directory = fileDialog.getCurrentDirectory();
927                    File file = fileDialog.getSelectedFile().getCanonicalFile();
928
929                    if (file.getName().indexOf(".") == -1) {
930                        // If the user has not given the file an extension, add it
931                        file = new File(
932                                file.getAbsolutePath() + "." + _formatName);
933                    }
934                    if (file.exists()) {
935                        if (!MessageHandler.yesNoQuestion(
936                                "Overwrite " + file.getName() + "?")) {
937                            return;
938                        }
939                    }
940                    OutputStream out = null;
941                    try {
942                        out = new FileOutputStream(file);
943                        exportImage(out, _formatName);
944                    } finally {
945                        if (out != null) {
946                            out.close();
947                        }
948                    }
949
950                    // Open the PNG file.
951                    // FIXME: We don't do the right thing with PNG files.
952                    // It just opens in a text editor.
953                    // _read(file.toURI().toURL());
954                    MessageHandler.message(
955                            "Image file exported to " + file.getName());
956                }
957            } catch (Exception ex) {
958                MessageHandler.error("Export to "
959                        + _formatName.toUpperCase(Locale.getDefault())
960                        + " failed", ex);
961            } finally {
962                jFileChooserBugFix.restoreBackground(background);
963            }
964        }
965
966        private String _formatName;
967    }
968
969    ///////////////////////////////////////////////////////////////////
970    //// FindAction
971
972    /** Find and replace. */
973    private class FindAction extends AbstractAction {
974        public FindAction() {
975            super("Find");
976            putValue("tooltip", "Find and replace.");
977            putValue(diva.gui.GUIUtilities.ACCELERATOR_KEY,
978                    KeyStroke.getKeyStroke(KeyEvent.VK_F, Toolkit
979                            .getDefaultToolkit().getMenuShortcutKeyMask()));
980            putValue(diva.gui.GUIUtilities.MNEMONIC_KEY,
981                    Integer.valueOf(KeyEvent.VK_F));
982        }
983
984        @Override
985        public void actionPerformed(ActionEvent e) {
986            try {
987                _find();
988            } catch (CannotRedoException ex) {
989                // Ignore.
990            }
991        }
992    }
993
994    ///////////////////////////////////////////////////////////////////
995    //// PasteAction
996
997    /** Copy the contents of the selection and put on clipboard. */
998    private class PasteAction extends AbstractAction {
999        public PasteAction() {
1000            super("Paste");
1001            putValue("tooltip", "Paste from clipboard.");
1002            putValue(diva.gui.GUIUtilities.ACCELERATOR_KEY,
1003                    KeyStroke.getKeyStroke(KeyEvent.VK_V, Toolkit
1004                            .getDefaultToolkit().getMenuShortcutKeyMask()));
1005            putValue(diva.gui.GUIUtilities.MNEMONIC_KEY,
1006                    Integer.valueOf(KeyEvent.VK_P));
1007        }
1008
1009        @Override
1010        public void actionPerformed(ActionEvent e) {
1011            text.paste();
1012        }
1013    }
1014
1015    ///////////////////////////////////////////////////////////////////
1016    //// RedoAction
1017
1018    /** Redo the last undo change. */
1019    private class RedoAction extends AbstractAction {
1020        public RedoAction() {
1021            super("Redo");
1022            putValue("tooltip", "Redo the last undo.");
1023            putValue(diva.gui.GUIUtilities.ACCELERATOR_KEY,
1024                    KeyStroke.getKeyStroke(KeyEvent.VK_Y, Toolkit
1025                            .getDefaultToolkit().getMenuShortcutKeyMask()));
1026            putValue(diva.gui.GUIUtilities.MNEMONIC_KEY,
1027                    Integer.valueOf(KeyEvent.VK_R));
1028        }
1029
1030        @Override
1031        public void actionPerformed(ActionEvent e) {
1032            try {
1033                _redo();
1034            } catch (CannotRedoException ex) {
1035                // Ignore.
1036            }
1037        }
1038    }
1039
1040    ///////////////////////////////////////////////////////////////////
1041    //// UndoAction
1042
1043    /** Undo the last change to the text. */
1044    private class UndoAction extends AbstractAction {
1045        public UndoAction() {
1046            super("Undo");
1047            putValue("tooltip", "Undo the last change.");
1048            putValue(diva.gui.GUIUtilities.ACCELERATOR_KEY,
1049                    KeyStroke.getKeyStroke(KeyEvent.VK_Z, Toolkit
1050                            .getDefaultToolkit().getMenuShortcutKeyMask()));
1051            putValue(diva.gui.GUIUtilities.MNEMONIC_KEY,
1052                    Integer.valueOf(KeyEvent.VK_U));
1053        }
1054
1055        @Override
1056        public void actionPerformed(ActionEvent e) {
1057            try {
1058                _undo();
1059            } catch (CannotUndoException ex) {
1060                // Ignore.
1061            }
1062        }
1063    }
1064}