001package jmri.jmrix;
002
003import java.awt.Component;
004import java.awt.Dimension;
005import java.awt.datatransfer.StringSelection;
006import java.awt.event.ActionEvent;
007import java.io.File;
008import java.io.FileOutputStream;
009import java.io.PrintStream;
010import java.text.DateFormat;
011import java.text.SimpleDateFormat;
012import java.util.Date;
013
014import javax.annotation.concurrent.GuardedBy;
015import javax.swing.BoxLayout;
016import javax.swing.JButton;
017import javax.swing.JCheckBox;
018import javax.swing.JFileChooser;
019import javax.swing.JLabel;
020import javax.swing.JPanel;
021import javax.swing.JScrollPane;
022import javax.swing.JTextArea;
023import javax.swing.JTextField;
024import javax.swing.JToggleButton;
025import javax.swing.SwingUtilities;
026import javax.swing.text.AbstractDocument;
027import javax.swing.text.AttributeSet;
028import javax.swing.text.BadLocationException;
029import javax.swing.text.DocumentFilter;
030
031import jmri.InstanceManager;
032import jmri.UserPreferencesManager;
033import jmri.util.FileUtil;
034import jmri.util.JmriJFrame;
035import jmri.util.swing.JmriJOptionPane;
036import jmri.util.swing.JmriPanel;
037import jmri.util.swing.TextAreaFIFO;
038import jmri.util.swing.WrapLayout;
039
040/**
041 * Abstract base class for JPanels displaying communications monitor
042 * information.
043 *
044 * @author Bob Jacobsen Copyright (C) 2001, 2003, 2010
045 */
046public abstract class AbstractMonPane extends JmriPanel {
047
048    /**
049     * {@inheritDoc}
050     */
051    @Override
052    public abstract String getTitle();    // provide the title for the frame
053
054    /**
055     * Initialize the data source.
056     * <p>
057     * This is invoked at the end of the GUI initialization phase. Subclass
058     * implementations should connect to their data source here.
059     */
060    protected abstract void init();
061
062    /**
063     * {@inheritDoc}
064     */
065    @Override
066    public void dispose() {
067        UserPreferencesManager pm = InstanceManager.getDefault(UserPreferencesManager.class);
068        pm.setSimplePreferenceState(timeStampCheck, timeCheckBox.isSelected());
069        pm.setSimplePreferenceState(rawDataCheck, rawCheckBox.isSelected());
070        pm.setSimplePreferenceState(alwaysOnTopCheck, alwaysOnTopCheckBox.isSelected());
071        pm.setSimplePreferenceState(autoScrollCheck, !autoScrollCheckBox.isSelected());
072        pm.setProperty(filterFieldCheck, filterFieldCheck, filterField.getText());
073        monTextPane.dispose();
074        super.dispose();
075    }
076    // you'll also have to add the message(Foo) members to handle info to be logged.
077    // these should call nextLine(String line, String raw) with their updates
078
079    // member declarations
080    protected JButton copyToClipBoardButton = new JButton();
081    protected JButton clearButton = new JButton();
082    protected JToggleButton freezeButton = new JToggleButton();
083    protected JScrollPane jScrollPane1 = new JScrollPane();
084    protected TextAreaFIFO monTextPane = new TextAreaFIFO(MAX_LINES);
085    protected JToggleButton startLogButton = new JToggleButton();
086    protected JButton stopLogButton = new JButton();
087    protected JCheckBox rawCheckBox = new JCheckBox();
088    protected JCheckBox timeCheckBox = new JCheckBox();
089    protected JCheckBox alwaysOnTopCheckBox = new JCheckBox();
090    protected JCheckBox autoScrollCheckBox = new JCheckBox();
091    protected JTextField filterField = new JTextField();
092    protected JLabel filterLabel = new JLabel(Bundle.getMessage("LabelFilterBytes"), JLabel.LEFT); // NOI18N
093    protected JButton openFileChooserButton = new JButton();
094    protected JTextField entryField = new JTextField();
095    protected JButton enterButton = new JButton();
096    String rawDataCheck = this.getClass().getName() + ".RawData"; // NOI18N
097    String timeStampCheck = this.getClass().getName() + ".TimeStamp"; // NOI18N
098    String alwaysOnTopCheck = this.getClass().getName() + ".AlwaysOnTop"; // NOI18N
099    String autoScrollCheck = this.getClass().getName() + ".AutoScroll"; // NOI18N
100    String filterFieldCheck = this.getClass().getName() + ".FilterField"; // NOI18N
101
102    // to find and remember the log file
103    final javax.swing.JFileChooser logFileChooser = new jmri.util.swing.JmriJFileChooser(FileUtil.getUserFilesPath());
104
105    public AbstractMonPane() {
106        super();
107    }
108
109    /**
110     * By default, create just one place (one data pane) to put trace data.
111     */
112    protected void createDataPanes() {
113        configureDataPane(monTextPane);
114    }
115
116    /**
117     * Do default configuration of a data pane.
118     *
119     * @param textPane a TextAreaFIFO into which the data pane will be placed
120     */
121    protected void configureDataPane(TextAreaFIFO textPane) {
122        textPane.setVisible(true);
123        textPane.setToolTipText(Bundle.getMessage("TooltipMonTextPane")); // NOI18N
124        textPane.setEditable(false);
125    }
126
127    /**
128     * Provide initial preferred line length. Used to size the initial GUI.
129     *
130     * @return preferred initial number of columns
131     */
132    protected int getInitialPreferredLineLength() {
133        return 80;
134    }
135
136    /**
137     * Provide initial number of lines to display Used to size the initial GUI.
138     *
139     * @return preferred initial number of rows
140     */
141    protected int getInitialPreferredLineCount() {
142        return 10;
143    }
144
145    /**
146     * Put data pane(s) in the GUI.
147     */
148    protected void addDataPanes() {
149
150        // fix a width for current character set
151        JTextField t = new JTextField(getInitialPreferredLineLength());
152        int x = jScrollPane1.getPreferredSize().width + t.getPreferredSize().width;
153        int y = jScrollPane1.getPreferredSize().height + getInitialPreferredLineCount() * t.getPreferredSize().height;
154
155        jScrollPane1.getViewport().add(monTextPane);
156        jScrollPane1.setPreferredSize(new Dimension(x, y));
157        jScrollPane1.setVisible(true);
158
159        // add in a JPanel that stays sized as the window changes size
160        JPanel p = new JPanel();
161        p.setLayout(new BoxLayout(p, BoxLayout.X_AXIS));
162        p.add(jScrollPane1);
163        add(p);
164    }
165
166    /**
167     * {@inheritDoc}
168     */
169    @Override
170    public void initComponents() {
171        UserPreferencesManager pm = InstanceManager.getDefault(UserPreferencesManager.class);
172
173        // the following code sets the frame's initial state
174        copyToClipBoardButton.setText(java.util.ResourceBundle.getBundle("apps.AppsBundle").getString("ButtonCopyClip"));
175        copyToClipBoardButton.setVisible(true);
176
177        clearButton.setText(Bundle.getMessage("ButtonClearScreen")); // NOI18N
178        clearButton.setVisible(true);
179        clearButton.setToolTipText(Bundle.getMessage("TooltipClearMonHistory")); // NOI18N
180
181        freezeButton.setText(Bundle.getMessage("ButtonFreezeScreen")); // NOI18N
182        freezeButton.setVisible(true);
183        freezeButton.setToolTipText(Bundle.getMessage("TooltipStopScroll")); // NOI18N
184
185        enterButton.setText(Bundle.getMessage("ButtonAddMessage")); // NOI18N
186        enterButton.setVisible(true);
187        enterButton.setToolTipText(Bundle.getMessage("TooltipAddMessage")); // NOI18N
188
189        createDataPanes();
190
191        entryField.setToolTipText(Bundle.getMessage("TooltipEntryPane")); // NOI18N
192        // cap vertical size to avoid over-growth
193        Dimension currentPreferredSize = entryField.getPreferredSize();
194        Dimension currentMaximumSize = entryField.getMaximumSize();
195        currentMaximumSize.height = currentPreferredSize.height;
196        entryField.setMaximumSize(currentMaximumSize);
197
198        //setup filterField
199        filterField.setToolTipText(Bundle.getMessage("TooltipFilter")); // NOI18N
200        filterField.setMaximumSize(currentMaximumSize);
201        try {
202            filterField.setText(pm.getProperty(filterFieldCheck, filterFieldCheck).toString());  //restore prev values
203        } catch (NullPointerException e1) {
204            // leave blank if previous value not retrieved
205        }
206        //automatically uppercase input in filterField, and only accept spaces and valid hex characters
207        ((AbstractDocument) filterField.getDocument()).setDocumentFilter(new DocumentFilter() {
208            final private static String PATTERN = "[0-9a-fA-F ]*+"; // typing inserts individual characters
209
210            @Override
211            public void insertString(DocumentFilter.FilterBypass fb, int offset, String text,
212                    AttributeSet attrs) throws BadLocationException {
213                if (text.matches(PATTERN)) { // NOI18N
214                    fb.insertString(offset, text.toUpperCase(), attrs);
215                } else {
216                    fb.insertString(offset, "", attrs);
217                }
218            }
219
220            @Override
221            public void replace(DocumentFilter.FilterBypass fb, int offset, int length, String text,
222                    AttributeSet attrs) throws BadLocationException {
223                if (text.matches(PATTERN)) { // NOI18N
224                    fb.replace(offset, length, text.toUpperCase(), attrs);
225                } else {
226                    fb.replace(offset, length, "", attrs);
227                }
228            }
229        });
230
231        startLogButton.setText(Bundle.getMessage("ButtonStartLogging")); // NOI18N
232        startLogButton.setVisible(true);
233        startLogButton.setToolTipText(Bundle.getMessage("TooltipStartLogging")); // NOI18N
234
235        stopLogButton.setText(Bundle.getMessage("ButtonStopLogging")); // NOI18N
236        stopLogButton.setVisible(true);
237        stopLogButton.setToolTipText(Bundle.getMessage("TooltipStopLogging")); // NOI18N
238
239        rawCheckBox.setText(Bundle.getMessage("ButtonShowRaw")); // NOI18N
240        rawCheckBox.setVisible(true);
241        rawCheckBox.setToolTipText(Bundle.getMessage("TooltipShowRaw")); // NOI18N
242        rawCheckBox.setSelected(pm.getSimplePreferenceState(rawDataCheck));
243
244        timeCheckBox.setText(Bundle.getMessage("ButtonShowTimestamps")); // NOI18N
245        timeCheckBox.setVisible(true);
246        timeCheckBox.setToolTipText(Bundle.getMessage("TooltipShowTimestamps")); // NOI18N
247        timeCheckBox.setSelected(pm.getSimplePreferenceState(timeStampCheck));
248
249        alwaysOnTopCheckBox.setText(Bundle.getMessage("ButtonWindowOnTop")); // NOI18N
250        alwaysOnTopCheckBox.setVisible(true);
251        alwaysOnTopCheckBox.setToolTipText(Bundle.getMessage("TooltipWindowOnTop")); // NOI18N
252        alwaysOnTopCheckBox.setSelected(pm.getSimplePreferenceState(alwaysOnTopCheck));
253        Component ancestor = getTopLevelAncestor();
254        if (ancestor instanceof JmriJFrame) {
255            ((JmriJFrame) ancestor).setAlwaysOnTop(alwaysOnTopCheckBox.isSelected());
256        } else {
257            // this pane isn't yet part of a frame,
258            // which can be normal, but
259            if (alwaysOnTopCheckBox.isSelected()) {
260                // in this case we want to access the enclosing frame to setAlwaysOnTop.  So defer for a bit....
261                log.debug("Cannot set Always On Top from preferences due to no Top Level Ancestor");
262                timerCount = 0;
263                timer = new javax.swing.Timer(20, (java.awt.event.ActionEvent evt) -> {
264                    if ((getTopLevelAncestor() != null) && (timerCount > 3) && (getTopLevelAncestor() instanceof JmriJFrame)) {
265                        timer.stop();
266                        ((JmriJFrame) getTopLevelAncestor()).setAlwaysOnTop(alwaysOnTopCheckBox.isSelected());
267                        log.debug("set Always On Top");
268                    } else {
269                        log.debug("Have to repeat attempt to set Always on Top");
270                        timerCount++;
271                        if (timerCount > 50) {
272                            log.warn("Took too long to \"Set Always on Top\", failed");
273                            timer.stop();
274                        }
275                    }
276                });
277                timer.start();
278            }
279        }
280
281        autoScrollCheckBox.setText(Bundle.getMessage("ButtonAutoScroll")); // NOI18N
282        autoScrollCheckBox.setVisible(true);
283        autoScrollCheckBox.setToolTipText(Bundle.getMessage("TooltipAutoScroll")); // NOI18N
284        autoScrollCheckBox.setSelected(!pm.getSimplePreferenceState(autoScrollCheck));
285        monTextPane.setAutoScroll(!pm.getSimplePreferenceState(autoScrollCheck));
286
287        openFileChooserButton.setText(Bundle.getMessage("ButtonChooseLogFile")); // NOI18N
288        openFileChooserButton.setVisible(true);
289        openFileChooserButton.setToolTipText(Bundle.getMessage("TooltipChooseLogFile")); // NOI18N
290
291        setLayout(new BoxLayout(this, BoxLayout.Y_AXIS));
292
293        // add items to GUI
294        addDataPanes();
295
296        JPanel paneA = new JPanel();
297        paneA.setLayout(new BoxLayout(paneA, BoxLayout.Y_AXIS));
298
299        JPanel pane1 = new JPanel(){
300            @Override
301            public Dimension getPreferredSize() {
302                Dimension min = super.getMinimumSize();
303                Dimension max = super.getMaximumSize();
304                return new Dimension(max.width, min.height);
305            }
306            @Override
307            public Dimension getMaximumSize() {
308                return getPreferredSize();
309            }
310        };
311        pane1.setLayout(new WrapLayout());
312        pane1.add(copyToClipBoardButton);
313        pane1.add(clearButton);
314        pane1.add(freezeButton);
315        pane1.add(rawCheckBox);
316        pane1.add(timeCheckBox);
317        pane1.add(alwaysOnTopCheckBox);
318        pane1.add(autoScrollCheckBox);
319        paneA.add(pane1);
320        addCustomControlPanes(paneA);
321
322        JPanel pane2 = new JPanel();
323        pane2.setLayout(new BoxLayout(pane2, BoxLayout.X_AXIS));
324        pane2.add(filterLabel);
325        filterLabel.setLabelFor(filterField);
326        pane2.add(filterField);
327        pane2.add(openFileChooserButton);
328        pane2.add(startLogButton);
329        pane2.add(stopLogButton);
330        paneA.add(pane2);
331
332        JPanel pane3 = new JPanel();
333        pane3.setLayout(new BoxLayout(pane3, BoxLayout.X_AXIS));
334        pane3.add(enterButton);
335        pane3.add(entryField);
336        paneA.add(pane3);
337
338        add(paneA);
339
340        // connect actions to buttons
341        copyToClipBoardButton.addActionListener(this::copyToClipBoardButtonActionPerformed);
342
343        clearButton.addActionListener((java.awt.event.ActionEvent e) -> {
344            clearButtonActionPerformed(e);
345        });
346        startLogButton.addActionListener((java.awt.event.ActionEvent e) -> {
347            startLogButtonActionPerformed(e);
348        });
349        stopLogButton.addActionListener((java.awt.event.ActionEvent e) -> {
350            stopLogButtonActionPerformed(e);
351        });
352        openFileChooserButton.addActionListener((java.awt.event.ActionEvent e) -> {
353            openFileChooserButtonActionPerformed(e);
354        });
355
356        enterButton.addActionListener((java.awt.event.ActionEvent e) -> {
357            enterButtonActionPerformed(e);
358        });
359
360        alwaysOnTopCheckBox.addActionListener((java.awt.event.ActionEvent e) -> {
361            if ((getTopLevelAncestor() != null) && (getTopLevelAncestor() instanceof JmriJFrame)) {
362                ((JmriJFrame) getTopLevelAncestor()).setAlwaysOnTop(alwaysOnTopCheckBox.isSelected());
363            }
364        });
365
366        autoScrollCheckBox.addActionListener((ActionEvent e) -> {
367            monTextPane.setAutoScroll(autoScrollCheckBox.isSelected());
368        });
369
370        // set file chooser to a default
371        logFileChooser.setSelectedFile(new File("monitorLog.txt"));
372
373        // connect to data source
374        init();
375
376    }
377
378    /**
379     * Expand the display with additional options specific to the hardware.
380     * @param parent a Panel (with vertical BoxLayout); overrides should add a new Panel with horizontal BoxLayout to hold the additional options.
381     */
382    protected void addCustomControlPanes(JPanel parent) {
383    }
384
385    private int timerCount = 0;
386    private javax.swing.Timer timer;
387
388    /**
389     * Set the display window to fixed width font, so that e.g. columns line up.
390     */
391    public void setFixedWidthFont() {
392        monTextPane.setFont(new java.awt.Font("Monospaced", java.awt.Font.PLAIN, monTextPane.getFont().getSize()));
393    }
394
395    /**
396     * {@inheritDoc}
397     */
398    @Override
399    public String getHelpTarget() {
400        return "package.jmri.jmrix.AbstractMonFrame"; // NOI18N
401    }
402        
403    /**
404     * Log an Message derived message.
405     *
406     * @param message message object to log.
407     */
408    public void logMessage(Message message) {
409        logMessage("", "", message);
410    }
411
412    /**
413     * Log an Message derived message.
414     *
415     * @param messagePrefix text to prefix the message with.
416     * @param message       message object to log.
417     */
418    public void logMessage(String messagePrefix, Message message) {
419        logMessage(messagePrefix, "", message);
420    }
421
422    /**
423     * Log an Message derived message with a prefixed label.
424     *
425     * @param messagePrefix text to prefix the message with.
426     * @param rawPrefix     label to add to the start of the message.
427     * @param message       message object to log.
428     */
429    public void logMessage(String messagePrefix, String rawPrefix, Message message){
430        // display the raw data if requested  
431        StringBuilder raw = new StringBuilder(rawPrefix);
432        if (rawCheckBox.isSelected()) {
433            raw.append(message.toString());
434        }
435
436        // display the decoded data
437        String text = message.toMonitorString();
438        nextLine(messagePrefix + " " + text + "\n", raw.toString());
439    }
440
441
442    public void nextLine(String line, String raw) {
443        nextLineWithTime(new Date(), line, raw);
444    }
445
446    /**
447     * Handle display of traffic.
448     *
449     * @param timestamp timestamp to be pre-pended to the output line (if
450     *                  timestamping is enabled)
451     * @param line      The traffic in normal parsed form, ending with \n
452     * @param raw       The traffic in raw form, ending with \n
453     */
454    public void nextLineWithTime(Date timestamp, String line, String raw) {
455
456        StringBuilder sb = new StringBuilder(120);
457
458        // display the timestamp if requested
459        if (timeCheckBox.isSelected()) {
460            sb.append(df.format(timestamp)).append(": ");
461        }
462
463        // display the raw data if available and requested
464        if (raw != null && rawCheckBox.isSelected()) {
465            sb.append('[').append(raw).append("]  "); // NOI18N
466        }
467
468        // display parsed data
469        sb.append(line);
470        synchronized (this) {
471            linesBuffer.append(sb.toString());
472        }
473
474        // if requested, log to a file.
475        synchronized (this) {
476            if (logStream != null) {
477                String logLine = sb.toString();
478                if (!newline.equals("\n")) { // NOI18N
479                    // have to massage the line-ends
480                    int lim = sb.length();
481                    StringBuilder out = new StringBuilder(sb.length() + 10);  // arbitrary guess at space
482                    for (int i = 0; i < lim; i++) {
483                        if (sb.charAt(i) == '\n') { // NOI18N
484                            out.append(newline);
485                        } else {
486                            out.append(sb.charAt(i));
487                        }
488                    }
489                    logLine = out.toString();
490                }
491                logStream.print(logLine);
492            }
493        }
494
495        // if frozen, exit without adding to the Swing thread
496        if (freezeButton.isSelected()) {
497            return;
498        }
499
500        // if this message is filtered out, end
501        if (isFiltered(raw)) {
502            return;
503        }
504
505        SwingUtilities.invokeLater(() -> {
506            synchronized (AbstractMonPane.this) {
507                monTextPane.append(linesBuffer.toString());
508                linesBuffer.setLength(0);
509            }
510        });
511    }
512
513    /**
514     * Default filtering implementation, more of an example than anything else,
515     * not clear it really works for any system. Override this in
516     * system-specific subclasses to do something useful.
517     *
518     * @param raw A string containing the raw message hex information, in ASCII
519     *            encoding, with some "header" information pre-pended.
520     * @return True if the opcode in the raw message matches one of the "filter"
521     *         opcodes. False if the opcode does not match any of the "filter"
522     *         opcodes.
523     */
524    protected boolean isFiltered(String raw) {
525        String checkRaw = getOpCodeForFilter(raw);
526        //don't bother to check filter if no raw value passed
527        if (raw != null) {
528            // if first bytes are in the skip list,  exit without adding to the Swing thread
529            String[] filters = filterField.getText().toUpperCase().split(" ");
530
531            for (String s : filters) {
532                if (s.equals(checkRaw)) {
533                    synchronized (this) {
534                        linesBuffer.setLength(0);
535                    }
536                    return true;
537                }
538            }
539        }
540        return false;
541    }
542
543    /**
544     * Get hex opcode for filtering.
545     * <p>
546     * Reports the "opcode" byte from the string containing the ASCII string
547     * representation of the message. Assumes that there is a generic header on
548     * string, like "Tx - ", and ignores it.
549     *
550     * @param raw a String containing the generic raw hex information, with
551     *            pre-pended header.
552     *
553     * @return a two character String containing only the hex representation of
554     *         the opcode from the raw message.
555     */
556    protected String getOpCodeForFilter(String raw) {
557        // note: Generic raw is formatted like "Tx - BB 01 00 45", so extract the correct bytes from it (BB) for comparison
558        if (raw != null && raw.length() >= 7) {
559            return raw.substring(5, 7);
560        } else {
561            return null;
562        }
563    }
564
565    private static final String newline = System.getProperty("line.separator"); // NOI18N
566
567    public synchronized void clearButtonActionPerformed(java.awt.event.ActionEvent e) {
568        // clear the monitoring history
569        linesBuffer.setLength(0);
570        monTextPane.setText("");
571    }
572
573    public synchronized void copyToClipBoardButtonActionPerformed(java.awt.event.ActionEvent e) {
574        StringSelection text = new StringSelection(monTextPane.getText());
575        java.awt.Toolkit.getDefaultToolkit().getSystemClipboard().setContents(text, text);
576    }
577
578    public String getFilePathAndName() {
579        String returnString = "";
580        java.nio.file.Path p = logFileChooser.getSelectedFile().toPath();
581        if (p.getParent() == null) {
582            // This case is a file path with a "parent" of "null"
583            //
584            // Should instead use the profile directory, as "null" can default to
585            // the JMRI program directory, which might not be user-writable.
586            java.nio.file.Path fileName = p.getFileName();
587            if (fileName != null) { // getUserFilesPath() never null
588                returnString = FileUtil.getUserFilesPath() + fileName.toString();
589            } else {
590                log.error("User Files File Path not valid");
591            }
592            log.warn("File selection dialog box did not provide a path to the specified file. Log will be saved to {}",
593                    returnString);
594        } else {
595            returnString = p.toString();
596        }
597        return returnString;
598    }
599
600    public synchronized void startLogButtonActionPerformed(java.awt.event.ActionEvent e) {
601        // start logging by creating the stream
602        if (logStream == null) {  // successive clicks won't restart the file once running
603            // start logging
604            String filePathAndName = getFilePathAndName();
605            log.warn("startLogButtonActionPerformed: getSelectedFile() returns {} {}",
606                    logFileChooser.getSelectedFile().getPath(), logFileChooser.getSelectedFile().getName());
607            log.warn("startLogButtonActionPerformed: is attempting to use returned file path and file name {}",
608                    filePathAndName);
609            File logFile = new File(filePathAndName);
610            try {
611                logStream = new PrintStream(new FileOutputStream(logFile));
612            } catch (java.io.FileNotFoundException ex) {
613                stopLogButtonActionPerformed(null);
614                log.error("startLogButtonActionPerformed: FileOutputStream cannot open the file '{}'.  Exception: {}", logFileChooser.getSelectedFile().getName(), ex.getMessage());
615                JmriJOptionPane.showMessageDialog(this,
616                        (Bundle.getMessage("ErrorCannotOpenFileForWriting",
617                                logFileChooser.getSelectedFile().getName(),
618                                Bundle.getMessage("ErrorPossibleCauseCannotOpenForWrite"))),
619                        Bundle.getMessage("ErrorTitle"), JmriJOptionPane.ERROR_MESSAGE);
620            }
621        } else {
622            startLogButton.setSelected(true); // keep toggle on
623        }
624    }
625
626    public synchronized void stopLogButtonActionPerformed(java.awt.event.ActionEvent e) {
627        // stop logging by removing the stream
628        if (logStream != null) {
629            logStream.flush();
630            logStream.close();
631            logStream = null;
632        }
633        startLogButton.setSelected(false);
634    }
635
636    public void openFileChooserButtonActionPerformed(java.awt.event.ActionEvent e) {
637        // start at current file, show dialog
638        int retVal = logFileChooser.showSaveDialog(this);
639
640        // handle selection or cancel
641        if (retVal == JFileChooser.APPROVE_OPTION) {
642            synchronized (this) {
643                boolean loggingNow = (logStream != null);
644                stopLogButtonActionPerformed(e);  // stop before changing file
645                //File file = logFileChooser.getSelectedFile();
646                // if we were currently logging, start the new file
647                if (loggingNow) {
648                    startLogButtonActionPerformed(e);
649                }
650            }
651        }
652    }
653
654    public void enterButtonActionPerformed(java.awt.event.ActionEvent e) {
655        nextLine(entryField.getText() + "\n", null); // NOI18N
656    }
657
658    public synchronized String getFrameText() {
659        return monTextPane.getText();
660    }
661
662    /**
663     * Get access to the main text area. This is intended for use in e.g.
664     * scripting to extend the behavior of the window.
665     *
666     * @return the main text area
667     */
668    public final synchronized JTextArea getTextArea() {
669        return monTextPane;
670    }
671
672    public synchronized String getFilterText() {
673        return filterField.getText();
674    }
675
676    public synchronized void setFilterText(String text) {
677        filterField.setText(text);
678    }
679
680    @GuardedBy("this")
681    private volatile PrintStream logStream = null;
682
683    // to get a time string
684    private final DateFormat df = new SimpleDateFormat("HH:mm:ss.SSS");
685
686    @GuardedBy("this")
687    protected StringBuffer linesBuffer = new StringBuffer();
688    private static final int MAX_LINES = 500;
689
690    private static final org.slf4j.Logger log = org.slf4j.LoggerFactory.getLogger(AbstractMonPane.class);
691
692}