001/* An AWT and Swing implementation of the the DisplayInterface
002 that displays input data in a text area on the screen.
003
004 @Copyright (c) 1998-2014 The Regents of the University of California.
005 All rights reserved.
006
007 Permission is hereby granted, without written agreement and without
008 license or royalty fees, to use, copy, modify, and distribute this
009 software and its documentation for any purpose, provided that the
010 above copyright notice and the following two paragraphs appear in all
011 copies of this software.
012
013 IN NO EVENT SHALL THE UNIVERSITY OF CALIFORNIA BE LIABLE TO ANY PARTY
014 FOR DIRECT, INDIRECT, SPECIAL, INCIDENTAL, OR CONSEQUENTIAL DAMAGES
015 ARISING OUT OF THE USE OF THIS SOFTWARE AND ITS DOCUMENTATION, EVEN IF
016 THE UNIVERSITY OF CALIFORNIA HAS BEEN ADVISED OF THE POSSIBILITY OF
017 SUCH DAMAGE.
018
019 THE UNIVERSITY OF CALIFORNIA SPECIFICALLY DISCLAIMS ANY WARRANTIES,
020 INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
021 MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE SOFTWARE
022 PROVIDED HEREUNDER IS ON AN "AS IS" BASIS, AND THE UNIVERSITY OF
023 CALIFORNIA HAS NO OBLIGATION TO PROVIDE MAINTENANCE, SUPPORT, UPDATES,
024 ENHANCEMENTS, OR MODIFICATIONS.
025
026 PT_COPYRIGHT_VERSION 2
027 COPYRIGHTENDKEY
028 */
029
030package ptolemy.actor.lib.gui;
031
032import java.awt.Color;
033import java.awt.Container;
034import java.lang.ref.WeakReference;
035
036import javax.swing.BorderFactory;
037import javax.swing.JScrollPane;
038import javax.swing.JTextArea;
039import javax.swing.ScrollPaneConstants;
040import javax.swing.border.EmptyBorder;
041import javax.swing.border.LineBorder;
042import javax.swing.text.BadLocationException;
043
044import ptolemy.actor.gui.AbstractPlaceableJavaSE;
045import ptolemy.actor.gui.Configuration;
046import ptolemy.actor.gui.Effigy;
047import ptolemy.actor.gui.Tableau;
048import ptolemy.actor.gui.TextEditor;
049import ptolemy.actor.gui.TextEffigy;
050import ptolemy.actor.injection.PortableContainer;
051import ptolemy.data.IntToken;
052import ptolemy.data.Token;
053import ptolemy.gui.Top;
054import ptolemy.kernel.util.IllegalActionException;
055import ptolemy.kernel.util.NameDuplicationException;
056import ptolemy.util.MessageHandler;
057
058///////////////////////////////////////////////////////////////////
059//// DisplayJavaSE
060
061/**
062<p>
063DisplayJavaSE is the implementation of the DisplayInterface that uses AWT and Swing
064classes.  Values of the tokens arriving on the input channels in a
065text area on the screen.  Each input token is written on a
066separate line.  The input type can be of any type.
067Thus, string-valued tokens can be used to
068generate arbitrary textual output, at one token per line.
069</p><p>
070Note that because of complexities in Swing, if you resize the display
071window, then, unlike the plotters, the new size will not be persistent.
072That is, if you save the model and then re-open it, the new size is
073forgotten.  To control the size, you should set the <i>rowsDisplayed</i>
074and <i>columnsDisplayed</i> parameters.
075</p><p>
076Note that this actor internally uses JTextArea, a Java Swing object
077that is known to consume large amounts of memory. It is not advisable
078to use this actor to log large output streams.</p>
079
080@author Yuhong Xiong, Edward A. Lee Contributors: Ishwinder Singh
081@version $Id$
082@since Ptolemy II 10.0
083 */
084
085public class DisplayJavaSE extends AbstractPlaceableJavaSE
086        implements DisplayInterface {
087
088    ///////////////////////////////////////////////////////////////////
089    ////                         public methods                    ////
090
091    /** Free up memory when closing.
092     *  This is executed in the Swing event thread.
093     */
094    @Override
095    public void cleanUp() {
096        Runnable doIt = new Runnable() {
097            @Override
098            public void run() {
099                _tableau = null;
100
101                if (_scrollPane != null) {
102                    _scrollPane.removeAll();
103                    _scrollPane = null;
104                }
105                if (textArea != null) {
106                    textArea.removeAll();
107                    textArea = null;
108                }
109                _frame = null;
110                DisplayJavaSE.super.cleanUp();
111            }
112        };
113        Top.deferIfNecessary(doIt);
114    }
115
116    /** Append the string value of the token to the text area
117     *  on the screen.  Each value is terminated with a newline
118     *  character. This is executed in the Swing event thread.
119     *  @param value The string to be displayed
120     */
121    @Override
122    public void display(final String value) {
123        Runnable doIt = new Runnable() {
124            @Override
125            public void run() {
126                if (textArea == null) {
127                    return;
128                }
129
130                textArea.append(value);
131
132                // Append a newline character.
133                if (value.length() > 0 || !_display._isSuppressBlankLines) {
134                    textArea.append("\n");
135                }
136
137                // Regrettably, the default in swing is that the top
138                // of the textArea is visible, not the most recent text.
139                // So we have to manually move the scrollbar.
140                // The (undocumented) way to do this is to set the
141                // caret position (despite the fact that the caret
142                // is already where want it).
143                try {
144                    int lineOffset = textArea
145                            .getLineStartOffset(textArea.getLineCount() - 1);
146                    textArea.setCaretPosition(lineOffset);
147                } catch (BadLocationException ex) {
148                    // Ignore ... worst case is that the scrollbar
149                    // doesn't move.
150                }
151            }
152        };
153        Top.deferIfNecessary(doIt);
154    }
155
156    /** Return the object of the containing text area.
157     *  @return the text area.
158     */
159    @Override
160    public Object getTextArea() {
161        return textArea;
162    }
163
164    /** Set the number of rows for the text area.
165     * @param displayActor The display actor to be initialized.
166     * @exception IllegalActionException If the entity cannot be contained
167     * by the proposed container.
168     * @exception NameDuplicationException If the container already has an
169     * actor with this name.
170     */
171    @Override
172    public void init(Display displayActor)
173            throws IllegalActionException, NameDuplicationException {
174        _display = displayActor;
175        super.init(displayActor);
176    }
177
178    /** Open the display window if it has not been opened.
179     *  @exception IllegalActionException If there is a problem creating
180     *  the effigy and tableau.
181     *  This is executed in the Swing event thread.
182     */
183    @Override
184    public void openWindow() throws IllegalActionException {
185        Runnable doIt = new Runnable() {
186            @Override
187            public void run() {
188                if (textArea == null) {
189                    // No container has been specified for display.
190                    // Place the text area in its own frame.
191                    // Need an effigy and a tableau so that menu ops work properly.
192
193                    Effigy containerEffigy = Configuration
194                            .findEffigy(_display.toplevel());
195
196                    try {
197                        if (containerEffigy == null) {
198                            throw new IllegalActionException(
199                                    "Cannot find effigy for top level \""
200                                            + _display.toplevel().getFullName()
201                                            + "\".  This can happen when a is invoked"
202                                            + " with a non-graphical execution engine"
203                                            + " such as ptolemy.moml.MoMLSimpleApplication"
204                                            + " but the "
205                                            + " ptolemy.moml.filter.RemoveGraphicalClasses"
206                                            + " MoML filter is not replacing the"
207                                            + " class that extends Display.");
208                        }
209                        TextEffigy textEffigy = TextEffigy
210                                .newTextEffigy(containerEffigy, "");
211
212                        // The default identifier is "Unnamed", which is no good for
213                        // two reasons: Wrong title bar label, and it causes a save-as
214                        // to destroy the original window.
215
216                        textEffigy.identifier
217                                .setExpression(_display.getFullName());
218
219                        _tableau = new DisplayWindowTableau(_display,
220                                textEffigy, "tableau");
221                        _frame = _tableau.frame.get();
222
223                        // Require a vertical scrollbar always so that we don't get a horizontal
224                        // scrollbar when it appears.
225                        JScrollPane pane = ((TextEditor) _frame)
226                                .getScrollPane();
227                        if (pane != null) {
228                            pane.setVerticalScrollBarPolicy(
229                                    ScrollPaneConstants.VERTICAL_SCROLLBAR_ALWAYS);
230                        }
231
232                        textArea = ((TextEditor) _frame).text;
233
234                        int numRows = ((IntToken) _display.rowsDisplayed
235                                .getToken()).intValue();
236                        textArea.setRows(numRows);
237
238                        int numColumns = ((IntToken) _display.columnsDisplayed
239                                .getToken()).intValue();
240
241                        textArea.setColumns(numColumns);
242                        setFrame(_frame);
243                        _frame.pack();
244                    } catch (Exception ex) {
245                        MessageHandler.error(
246                                "Error opening window for Display actor.", ex);
247                    }
248                } else {
249                    // Erase previous text.
250                    textArea.setText(null);
251                }
252
253                if (_frame != null) {
254                    // show() used to override manual placement by calling pack.
255                    // No more.
256                    _frame.setVisible(true);
257                    _frame.toFront();
258                }
259            }
260        };
261        Top.deferIfNecessary(doIt);
262    }
263
264    /** Specify the container in which the data should be displayed.
265     *  An instance of JTextArea will be added to that container.
266     *  This method needs to be called before the first call to initialize().
267     *  Otherwise, an instance of JTextArea will be placed in its own frame.
268     *  The text area is also placed in its own frame if this method
269     *  is called with a null argument.
270     *  The background of the text area is set equal to that of the container
271     *  (unless it is null).
272     *  This is executed in the Swing event thread.
273     *  @param portableContainer The container into which to place the
274     *   text area, or null to specify that there is no current
275     *   container.
276     */
277    @Override
278    public void place(final PortableContainer portableContainer) {
279        Runnable doIt = new Runnable() {
280            @Override
281            public void run() {
282                Container container = (Container) (portableContainer != null
283                        ? portableContainer.getPlatformContainer()
284                        : null);
285                if (container == null) {
286                    // Reset everything.
287                    // NOTE: _remove() doesn't work here.  Why?
288                    if (_frame != null) {
289                        if (_frame instanceof Top) {
290                            Top top = (Top) _frame;
291                            if (!top.isDisposed()) {
292                                top.dispose();
293                            }
294                        } else {
295                            _frame.dispose();
296                        }
297                    }
298
299                    _frame = null;
300                    _scrollPane = null;
301                    textArea = null;
302                    return;
303                }
304
305                textArea = new JTextArea();
306                _scrollPane = new JScrollPane(textArea);
307
308                // java.awt.Component.setBackground(color) says that
309                // if the color "parameter is null then this component
310                // will inherit the  background color of its parent."
311                _scrollPane.setBackground(null);
312                _scrollPane.setBorder(new EmptyBorder(10, 10, 10, 10));
313                _scrollPane.setViewportBorder(new LineBorder(Color.black));
314
315                // Always have a vertical scrollbar so that we don't get a horizontal scrollbar when it appers.
316                _scrollPane.setVerticalScrollBarPolicy(
317                        ScrollPaneConstants.VERTICAL_SCROLLBAR_ALWAYS);
318
319                container.add(_scrollPane);
320                textArea.setBackground(Color.white);
321
322                String titleSpec;
323                try {
324                    titleSpec = _display.title.stringValue();
325                } catch (IllegalActionException e) {
326                    titleSpec = "Error in title: " + e.getMessage();
327                }
328
329                if (!titleSpec.trim().equals("")) {
330                    _scrollPane.setBorder(
331                            BorderFactory.createTitledBorder(titleSpec));
332                }
333
334                try {
335                    int numRows = ((IntToken) _display.rowsDisplayed.getToken())
336                            .intValue();
337                    textArea.setRows(numRows);
338
339                    int numColumns = ((IntToken) _display.columnsDisplayed
340                            .getToken()).intValue();
341                    textArea.setColumns(numColumns);
342
343                } catch (IllegalActionException ex) {
344                    // Ignore, and use default number of rows.
345                }
346
347                // Make sure the text is not editable.
348                textArea.setEditable(false);
349                _awtContainer = container;
350            }
351        };
352        Top.deferIfNecessary(doIt);
353    }
354
355    /** Remove the display from the current container, if there is one.
356     *  This is executed in the Swing thread later.
357     */
358    @Override
359    public void remove() {
360        Runnable doIt = new Runnable() {
361            @Override
362            public void run() {
363                if (textArea != null) {
364                    if (_awtContainer != null && _scrollPane != null) {
365                        _awtContainer.remove(_scrollPane);
366                        _awtContainer.invalidate();
367                        _awtContainer.repaint();
368                    } else if (_frame != null) {
369                        _frame.dispose();
370                    }
371                }
372            }
373        };
374        Top.deferIfNecessary(doIt);
375    }
376
377    /** Set the desired number of columns of the textArea, if there is one.
378     *  This is executed in the Swing event thread.
379     *  @param numberOfColumns The new value of the attribute.
380     *  @exception IllegalActionException If the specified attribute
381     *   is <i>rowsDisplayed</i> and its value is not positive.
382     */
383    @Override
384    public void setColumns(final int numberOfColumns)
385            throws IllegalActionException {
386        Runnable doIt = new Runnable() {
387            @Override
388            public void run() {
389                if (textArea != null) {
390                    // Unset any previously set size.
391                    try {
392                        _paneSize.setToken((Token) null);
393                    } catch (IllegalActionException e) {
394                        MessageHandler.error(
395                                "Unexpected error: Unable to unset previous pane size.",
396                                e);
397                    }
398                    setFrame(_frame);
399
400                    textArea.setColumns(numberOfColumns);
401
402                    if (_frame != null) {
403                        _frame.pack();
404                        _frame.setVisible(true);
405                    }
406                }
407            }
408        };
409        Top.deferIfNecessary(doIt);
410    }
411
412    /** Set the desired number of rows of the textArea, if there is one.
413     *  This is executed in the Swing event thread.
414     *  @param numberOfRows The new value of the attribute.
415     *  @exception IllegalActionException If the specified attribute
416     *   is <i>rowsDisplayed</i> and its value is not positive.
417     */
418    @Override
419    public void setRows(final int numberOfRows) throws IllegalActionException {
420        Runnable doIt = new Runnable() {
421            @Override
422            public void run() {
423                if (textArea != null) {
424                    // Unset any previously set size.
425                    try {
426                        _paneSize.setToken((Token) null);
427                    } catch (IllegalActionException e) {
428                        MessageHandler.error(
429                                "Unexpected error: Unable to unset previous pane size.",
430                                e);
431                    }
432                    setFrame(_frame);
433
434                    textArea.setRows(numberOfRows);
435
436                    if (_frame != null) {
437                        _frame.pack();
438                        _frame.setVisible(true);
439                    }
440                }
441            }
442        };
443        Top.deferIfNecessary(doIt);
444    }
445
446    /** Set the title of the window.
447     *  <p>If the <i>title</i> parameter is set to the empty string,
448     *  and the Display window has been rendered, then the title of
449     *  the Display window will be updated to the value of the name
450     *  parameter.</p>
451     *  This is executed in the Swing event thread.
452     * @param stringValue The title to be set.
453     * @exception IllegalActionException If the title cannot be set.
454     */
455    @Override
456    public void setTitle(final String stringValue)
457            throws IllegalActionException {
458        Runnable doIt = new Runnable() {
459            @Override
460            public void run() {
461                if (_tableau != null) {
462                    try {
463                        if (_display.title.stringValue().trim().equals("")) {
464                            _tableau.setTitle(stringValue);
465                        }
466                    } catch (IllegalActionException e) {
467                        _tableau.setTitle("Error getting title");
468                    }
469                }
470            }
471        };
472        Top.deferIfNecessary(doIt);
473    }
474
475    ///////////////////////////////////////////////////////////////////
476    ////                         public members                    ////
477
478    /** The text area in which the data will be displayed. */
479    public transient JTextArea textArea;
480
481    ///////////////////////////////////////////////////////////////////
482    ////                         private members                   ////
483
484    /** The AWT Container */
485    private Container _awtContainer;
486
487    /** Reference to the Display actor */
488    private Display _display;
489
490    /** The version of TextEditorTableau that creates a Display window. */
491    private DisplayWindowTableau _tableau;
492
493    /** The scroll pane. */
494    private JScrollPane _scrollPane;
495
496    ///////////////////////////////////////////////////////////////////
497    ////                         inner classes                     ////
498
499    /** Version of TextEditorTableau that creates DisplayWindow.
500     */
501    private static class DisplayWindowTableau extends Tableau {
502        // FindBugs suggested refactoring this into a static class.
503
504        /** Construct a new tableau for the model represented by the
505         *  given effigy.
506         *  @param display The Display actor associated with this tableau.
507         *  @param container The container.
508         *  @param name The name.
509         *  @exception IllegalActionException If the container does not accept
510         *   this entity (this should not occur).
511         *  @exception NameDuplicationException If the name coincides with an
512         *   attribute already in the container.
513         */
514        public DisplayWindowTableau(Display display, TextEffigy container,
515                String name)
516                throws IllegalActionException, NameDuplicationException {
517            super(container, name);
518
519            String title = display.title.stringValue();
520
521            if (title.trim().equals("")) {
522                title = display.getFullName();
523            }
524
525            TextEditor editor = new TextEditor(title, null, display);
526            frame = new WeakReference<TextEditor>(editor);
527
528            // Also need to set the title of this Tableau.
529            setTitle(title);
530
531            // Make sure that the effigy and the text area use the same
532            // Document (so that they contain the same data).
533            editor.text.setDocument(container.getDocument());
534            setFrame(editor);
535            editor.setTableau(this);
536        }
537
538        public WeakReference<TextEditor> frame;
539    }
540}