001package jmri.jmrix;
002
003import java.awt.Dimension;
004import java.awt.event.ActionEvent;
005
006import java.io.File;
007import java.io.FileOutputStream;
008import java.io.PrintStream;
009import java.text.DateFormat;
010import java.text.SimpleDateFormat;
011import java.util.Date;
012import javax.swing.BoxLayout;
013import javax.swing.JButton;
014import javax.swing.JCheckBox;
015import javax.swing.JFileChooser;
016import javax.swing.JPanel;
017import javax.swing.JScrollPane;
018import javax.swing.JTextArea;
019import javax.swing.JTextField;
020import javax.swing.JToggleButton;
021import jmri.util.FileUtil;
022import jmri.util.JmriJFrame;
023import jmri.util.swing.TextAreaFIFO;
024
025import javax.annotation.OverridingMethodsMustInvokeSuper;
026
027/**
028 * Abstract base class for Frames displaying communications monitor information.
029 *
030 * @author Bob Jacobsen Copyright (C) 2001, 2003, 2014
031 */
032public abstract class AbstractMonFrame extends JmriJFrame {
033
034    // template functions to fill in
035    protected abstract String title();    // provide the title for the frame
036
037    /**
038     * Initialize the data source.
039     * <p>
040     * This is invoked at the end of the GUI initialization phase. Subclass
041     * implementations should connect to their data source here.
042     */
043    protected abstract void init();
044
045    // the subclass also needs a dispose() method to close any specific communications; call super.dispose()
046    @OverridingMethodsMustInvokeSuper
047    @Override
048    public void dispose() {
049        if (userPrefs!=null) {
050           userPrefs.setSimplePreferenceState(timeStampCheck, timeCheckBox.isSelected());
051           userPrefs.setSimplePreferenceState(rawDataCheck, rawCheckBox.isSelected());
052           userPrefs.setSimplePreferenceState(alwaysOnTopCheck, alwaysOnTopCheckBox.isSelected());
053           userPrefs.setSimplePreferenceState(autoScrollCheck, !autoScrollCheckBox.isSelected());
054        }
055        stopLogButtonActionPerformed(null);
056        monTextPane.dispose();
057        super.dispose();
058    }
059    // you'll also have to add the message(Foo) members to handle info to be logged.
060    // these should call nextLine(String line, String raw) with their updates
061
062    // member declarations
063    protected JButton clearButton = new JButton(Bundle.getMessage("ButtonClearScreen"));
064    protected JToggleButton freezeButton = new JToggleButton(Bundle.getMessage("ButtonFreezeScreen"));
065    protected JScrollPane jScrollPane1 = new JScrollPane();
066    protected TextAreaFIFO monTextPane = new TextAreaFIFO(MAX_LINES);
067    protected JButton startLogButton = new JButton(Bundle.getMessage("ButtonStartLogging"));
068    protected JButton stopLogButton = new JButton(Bundle.getMessage("ButtonStopLogging"));
069
070    protected JCheckBox rawCheckBox = new JCheckBox(Bundle.getMessage("ButtonShowRaw"));
071    protected JCheckBox timeCheckBox = new JCheckBox(Bundle.getMessage("ButtonShowTimestamps"));
072    protected JCheckBox alwaysOnTopCheckBox = new JCheckBox(Bundle.getMessage("ButtonWindowOnTop"));
073    protected JCheckBox autoScrollCheckBox = new JCheckBox(Bundle.getMessage("ButtonAutoScroll"));
074    protected JButton openFileChooserButton = new JButton(Bundle.getMessage("ButtonChooseLogFile"));
075    protected JTextField entryField = new JTextField();
076    protected JButton enterButton = new JButton(Bundle.getMessage("ButtonAddMessage"));
077    private final String rawDataCheck = this.getClass().getName() + ".RawData"; // NOI18N
078    private final String timeStampCheck = this.getClass().getName() + ".TimeStamp"; // NOI18N
079    private final String alwaysOnTopCheck = this.getClass().getName() + ".alwaysOnTop"; // NOI18N
080    private final String autoScrollCheck = this.getClass().getName() + ".AutoScroll"; // NOI18N
081    public jmri.UserPreferencesManager userPrefs;
082
083    // for locking
084    final AbstractMonFrame self;
085
086    // to find and remember the log file
087    public final javax.swing.JFileChooser logFileChooser = new jmri.util.swing.JmriJFileChooser(FileUtil.getUserFilesPath());
088
089    public AbstractMonFrame() {
090        super();
091        self = this;
092    }
093
094    /**
095     * {@inheritDoc}
096     */
097    @Override
098    public void initComponents() {
099
100        userPrefs = jmri.InstanceManager.getDefault(jmri.UserPreferencesManager.class);
101        // the following code sets the frame's initial state
102
103        monTextPane.setVisible(true);
104        monTextPane.setToolTipText(Bundle.getMessage("TooltipMonTextPane")); // NOI18N
105        monTextPane.setEditable(false);
106
107        // fix a width for current character set
108        JTextField t = new JTextField(200);
109        int x = jScrollPane1.getPreferredSize().width + t.getPreferredSize().width;
110        int y = jScrollPane1.getPreferredSize().height + 10 * t.getPreferredSize().height;
111
112        jScrollPane1.getViewport().add(monTextPane);
113        jScrollPane1.setPreferredSize(new Dimension(x, y));
114        jScrollPane1.setVisible(true);
115
116        setTitle(title());
117        getContentPane().setLayout(new BoxLayout(getContentPane(), BoxLayout.Y_AXIS));
118
119        // add items to GUI
120        getContentPane().add(jScrollPane1);
121
122        JPanel paneA = new JPanel();
123        paneA.setLayout(new BoxLayout(paneA, BoxLayout.Y_AXIS));
124
125        JPanel topActions = new JPanel();
126        topActions.add(getActionButtonsPanel());
127        topActions.add(getCheckBoxPanel());
128
129        paneA.add(topActions);
130        paneA.add(getLogToFilePanel());
131
132        JPanel pane3 = new JPanel();
133        pane3.setLayout(new BoxLayout(pane3, BoxLayout.X_AXIS));
134        enterButton.setVisible(true);
135        enterButton.setToolTipText(Bundle.getMessage("TooltipAddMessage")); // NOI18N
136        enterButton.addActionListener(this::enterButtonActionPerformed);
137        entryField.setToolTipText(Bundle.getMessage("TooltipEntryPane", Bundle.getMessage("ButtonAddMessage"))); // NOI18N
138        pane3.add(enterButton);
139        pane3.add(entryField);
140        paneA.add(pane3);
141
142        getContentPane().add(paneA);
143
144        // connect to data source
145        init();
146
147        // add help menu to window
148        setHelp();
149
150        // prevent button areas from expanding
151        pack();
152        paneA.setMaximumSize(paneA.getSize());
153        pack();
154    }
155
156    protected JPanel getCheckBoxPanel() {
157        JPanel pane1 = new JPanel();
158        pane1.setLayout(new BoxLayout(pane1, BoxLayout.X_AXIS));
159
160        rawCheckBox.setVisible(true);
161        rawCheckBox.setToolTipText(Bundle.getMessage("TooltipShowRaw")); // NOI18N
162        rawCheckBox.setSelected(userPrefs.getSimplePreferenceState(rawDataCheck));
163
164        timeCheckBox.setVisible(true);
165        timeCheckBox.setToolTipText(Bundle.getMessage("TooltipShowTimestamps")); // NOI18N
166        timeCheckBox.setSelected(userPrefs.getSimplePreferenceState(timeStampCheck));
167
168        alwaysOnTopCheckBox.setVisible(true);
169        alwaysOnTopCheckBox.setToolTipText(Bundle.getMessage("TooltipWindowOnTop")); // NOI18N
170        alwaysOnTopCheckBox.setSelected(userPrefs.getSimplePreferenceState(alwaysOnTopCheck));
171        setAlwaysOnTop(alwaysOnTopCheckBox.isSelected());
172
173        alwaysOnTopCheckBox.addActionListener((ActionEvent e) -> {
174            setAlwaysOnTop(alwaysOnTopCheckBox.isSelected());
175        });
176
177        autoScrollCheckBox.setVisible(true);
178        autoScrollCheckBox.setToolTipText(Bundle.getMessage("TooltipAutoScroll")); // NOI18N
179        autoScrollCheckBox.setSelected(!userPrefs.getSimplePreferenceState(autoScrollCheck));
180
181        autoScrollCheckBox.addActionListener((ActionEvent e) -> {
182            monTextPane.setAutoScroll(autoScrollCheckBox.isSelected());
183        });
184
185        pane1.add(rawCheckBox);
186        pane1.add(timeCheckBox);
187        pane1.add(alwaysOnTopCheckBox);
188        pane1.add(autoScrollCheckBox);
189        return pane1;
190    }
191
192    protected JPanel getActionButtonsPanel() {
193
194        JPanel pane1 = new JPanel();
195        pane1.setLayout(new BoxLayout(pane1, BoxLayout.X_AXIS));
196
197        clearButton.setVisible(true);
198        clearButton.setToolTipText(Bundle.getMessage("TooltipClearMonHistory")); // NOI18N
199        clearButton.addActionListener(this::clearButtonActionPerformed);
200
201        freezeButton.setVisible(true);
202        freezeButton.setToolTipText(Bundle.getMessage("TooltipStopScroll")); // NOI18N
203
204        pane1.add(clearButton);
205        pane1.add(freezeButton);
206        return pane1;
207    }
208
209    protected JPanel getLogToFilePanel() {
210        JPanel pane1 = new JPanel();
211        pane1.setLayout(new BoxLayout(pane1, BoxLayout.X_AXIS));
212
213        startLogButton.setVisible(true);
214        startLogButton.setToolTipText(Bundle.getMessage("TooltipStartLogging"));
215
216        stopLogButton.setVisible(false);
217        stopLogButton.setToolTipText(Bundle.getMessage("TooltipStopLogging"));
218
219        openFileChooserButton.setVisible(true);
220        openFileChooserButton.setToolTipText(Bundle.getMessage("TooltipChooseLogFile"));
221
222        startLogButton.addActionListener(this::startLogButtonActionPerformed);
223        stopLogButton.addActionListener(this::stopLogButtonActionPerformed);
224
225        // set file chooser to a default
226        logFileChooser.setSelectedFile(new File("monitorLog.txt"));
227        openFileChooserButton.addActionListener(this::openFileChooserButtonActionPerformed);
228
229        pane1.add(openFileChooserButton);
230        pane1.add(startLogButton);
231        pane1.add(stopLogButton);
232        return pane1;
233    }
234
235    /**
236     * Define help menu for this window.
237     * <p>
238     * By default, provides a generic help page that covers general features.
239     * Specific implementations can override this to show their own help page if
240     * desired.
241     */
242    protected void setHelp() {
243        addHelpMenu("package.jmri.jmrix.AbstractMonFrame", true); // NOI18N
244    }
245
246    /**
247     * Handle display of traffic.
248     * @param line is the traffic in 'normal form'. Should end with \n
249     * @param raw is the "raw form" , should NOT end with \n
250     */
251    public void nextLine(String line, String raw) {
252        StringBuilder sb = new StringBuilder(120);
253
254        // display the timestamp if requested
255        if (timeCheckBox.isSelected()) {
256            sb.append(df.format(new Date())).append(": "); // NOI18N
257        }
258
259        // display the raw data if requested
260        if (rawCheckBox.isSelected()) {
261            sb.append('[').append(raw).append("]  "); // NOI18N
262        }
263
264        // display decoded data
265        sb.append(line);
266        synchronized (self) {
267            linesBuffer.append(sb.toString());
268        }
269
270        // if not frozen, display it in the Swing thread
271        if (!freezeButton.isSelected()) {
272            Runnable r = () -> {
273                synchronized (self) {
274                    monTextPane.append(linesBuffer.toString());
275                    linesBuffer.setLength(0);
276                }
277            };
278            javax.swing.SwingUtilities.invokeLater(r);
279        }
280
281        // if requested, log to a file.
282        if (logStream != null) {
283            synchronized (logStream) {
284                String logLine = sb.toString();
285                if (!newline.equals("\n")) {
286                    // have to massage the line-ends
287                    int lim = sb.length();
288                    StringBuilder out = new StringBuilder(sb.length() + 10);  // arbitrary guess at space
289                    for (int i = 0; i < lim; i++) {
290                        if (sb.charAt(i) == '\n') {
291                            out.append(newline);
292                        } else {
293                            out.append(sb.charAt(i));
294                        }
295                    }
296                    logLine = out.toString();
297                }
298                logStream.print(logLine);
299            }
300        }
301    }
302
303    String newline = System.getProperty("line.separator"); // NOI18N
304
305    public synchronized void clearButtonActionPerformed(java.awt.event.ActionEvent e) {
306        // clear the monitoring history
307        synchronized (linesBuffer) {
308            linesBuffer.setLength(0);
309            monTextPane.setText("");
310        }
311    }
312
313    public synchronized void startLogButtonActionPerformed(java.awt.event.ActionEvent e) {
314        // start logging by creating the stream
315        if (logStream == null) {  // successive clicks don't restart the file
316            // start logging
317            try {
318                logStream = new PrintStream(new FileOutputStream(logFileChooser.getSelectedFile()));
319            } catch (java.io.FileNotFoundException ex) {
320                log.error("exception", ex);
321            }
322        }
323        updateLoggingButtons();
324    }
325
326    public synchronized void stopLogButtonActionPerformed(java.awt.event.ActionEvent e) {
327        // stop logging by removing the stream
328        if (logStream != null) {
329            logStream.flush();
330            logStream.close();
331            logStream = null;
332        }
333        updateLoggingButtons();
334    }
335
336    private void updateLoggingButtons(){
337        this.startLogButton.setVisible(logStream == null);
338        this.stopLogButton.setVisible(logStream != null);
339    }
340
341    public void openFileChooserButtonActionPerformed(java.awt.event.ActionEvent e) {
342        // start at current file, show dialog
343        int retVal = logFileChooser.showSaveDialog(this);
344
345        // handle selection or cancel
346        if (retVal == JFileChooser.APPROVE_OPTION) {
347            boolean loggingNow = (logStream != null);
348            stopLogButtonActionPerformed(e);  // stop before changing file
349            //File file = logFileChooser.getSelectedFile();
350            // if we were currently logging, start the new file
351            if (loggingNow) {
352                startLogButtonActionPerformed(e);
353            }
354        }
355    }
356
357    public void enterButtonActionPerformed(java.awt.event.ActionEvent e) {
358        nextLine(entryField.getText() + "\n", ""); // NOI18N
359    }
360
361    /**
362     * Get access to the main text area.
363     * This is intended for use in e.g. scripting
364     * to extend the behaviour of the window.
365     * @return the text area.
366     */
367    public final synchronized JTextArea getTextArea() {
368        return monTextPane;
369    }
370
371    private volatile PrintStream logStream = null;
372
373    // to get a time string
374    DateFormat df = new SimpleDateFormat("HH:mm:ss.SSS");
375
376    StringBuffer linesBuffer = new StringBuffer();
377    static private int MAX_LINES = 500;
378
379    private static final org.slf4j.Logger log = org.slf4j.LoggerFactory.getLogger(AbstractMonFrame.class);
380
381}