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
078    /** Bracket characters wrapping the raw data in the displayed line; subclasses may override. */
079    protected String rawOpenBracket = "[";
080    protected String rawCloseBracket = "]";
081
082    private final String rawDataCheck = this.getClass().getName() + ".RawData"; // NOI18N
083    private final String timeStampCheck = this.getClass().getName() + ".TimeStamp"; // NOI18N
084    private final String alwaysOnTopCheck = this.getClass().getName() + ".alwaysOnTop"; // NOI18N
085    private final String autoScrollCheck = this.getClass().getName() + ".AutoScroll"; // NOI18N
086    public jmri.UserPreferencesManager userPrefs;
087
088    // for locking
089    final AbstractMonFrame self;
090
091    // to find and remember the log file
092    public final javax.swing.JFileChooser logFileChooser = new jmri.util.swing.JmriJFileChooser(FileUtil.getUserFilesPath());
093
094    public AbstractMonFrame() {
095        super();
096        self = this;
097    }
098
099    /**
100     * {@inheritDoc}
101     */
102    @Override
103    public void initComponents() {
104
105        userPrefs = jmri.InstanceManager.getDefault(jmri.UserPreferencesManager.class);
106        // the following code sets the frame's initial state
107
108        monTextPane.setVisible(true);
109        monTextPane.setToolTipText(Bundle.getMessage("TooltipMonTextPane")); // NOI18N
110        monTextPane.setEditable(false);
111
112        // fix a width for current character set
113        JTextField t = new JTextField(200);
114        int x = jScrollPane1.getPreferredSize().width + t.getPreferredSize().width;
115        int y = jScrollPane1.getPreferredSize().height + 10 * t.getPreferredSize().height;
116
117        jScrollPane1.getViewport().add(monTextPane);
118        jScrollPane1.setPreferredSize(new Dimension(x, y));
119        jScrollPane1.setVisible(true);
120
121        setTitle(title());
122        getContentPane().setLayout(new BoxLayout(getContentPane(), BoxLayout.Y_AXIS));
123
124        // add items to GUI
125        getContentPane().add(jScrollPane1);
126
127        JPanel paneA;
128        if (useStackedControlsLayout()) {
129            // Track preferred height so WrapLayout-based control rows can grow when
130            // the window narrows; unconstrained width lets the area stretch.
131            paneA = new JPanel() {
132                @Override
133                public Dimension getMaximumSize() {
134                    return new Dimension(Short.MAX_VALUE, getPreferredSize().height);
135                }
136            };
137        } else {
138            paneA = new JPanel();
139        }
140        paneA.setLayout(new BoxLayout(paneA, BoxLayout.Y_AXIS));
141
142        if (useStackedControlsLayout()) {
143            paneA.add(getActionButtonsPanel());
144            paneA.add(getCheckBoxPanel());
145            paneA.add(getLogToFilePanel());
146        } else {
147            JPanel topActions = new JPanel();
148            topActions.add(getActionButtonsPanel());
149            topActions.add(getCheckBoxPanel());
150            paneA.add(topActions);
151            paneA.add(getLogToFilePanel());
152        }
153
154        JPanel pane3 = new JPanel();
155        pane3.setLayout(new BoxLayout(pane3, BoxLayout.X_AXIS));
156        enterButton.setVisible(true);
157        enterButton.setToolTipText(Bundle.getMessage("TooltipAddMessage")); // NOI18N
158        enterButton.addActionListener(this::enterButtonActionPerformed);
159        entryField.setToolTipText(Bundle.getMessage("TooltipEntryPane", Bundle.getMessage("ButtonAddMessage"))); // NOI18N
160        pane3.add(enterButton);
161        pane3.add(entryField);
162        paneA.add(pane3);
163
164        getContentPane().add(paneA);
165
166        // connect to data source
167        init();
168
169        // add help menu to window
170        setHelp();
171
172        pack();
173        if (!useStackedControlsLayout()) {
174            // Legacy path pins the controls area at its packed size; the stacked
175            // path handles this via paneA's getMaximumSize override above.
176            paneA.setMaximumSize(paneA.getSize());
177            pack();
178        }
179    }
180
181    /**
182     * Whether to lay out the action, checkbox, and log button panels as separate
183     * stacked rows directly inside paneA (with paneA's height tracking its
184     * preferred so {@code WrapLayout}-based rows can grow when the window is
185     * narrowed). Defaults to {@code false} — legacy side-by-side layout with a
186     * FlowLayout wrapper and a static max-size pin. Subclasses opt in.
187     *
188     * @return true for stacked rows, false for the legacy layout
189     */
190    protected boolean useStackedControlsLayout() {
191        return false;
192    }
193
194    protected JPanel getCheckBoxPanel() {
195        JPanel pane1 = new JPanel();
196        pane1.setLayout(new BoxLayout(pane1, BoxLayout.X_AXIS));
197
198        rawCheckBox.setVisible(true);
199        rawCheckBox.setToolTipText(Bundle.getMessage("TooltipShowRaw")); // NOI18N
200        rawCheckBox.setSelected(userPrefs.getSimplePreferenceState(rawDataCheck));
201
202        timeCheckBox.setVisible(true);
203        timeCheckBox.setToolTipText(Bundle.getMessage("TooltipShowTimestamps")); // NOI18N
204        timeCheckBox.setSelected(userPrefs.getSimplePreferenceState(timeStampCheck));
205
206        alwaysOnTopCheckBox.setVisible(true);
207        alwaysOnTopCheckBox.setToolTipText(Bundle.getMessage("TooltipWindowOnTop")); // NOI18N
208        alwaysOnTopCheckBox.setSelected(userPrefs.getSimplePreferenceState(alwaysOnTopCheck));
209        setAlwaysOnTop(alwaysOnTopCheckBox.isSelected());
210
211        alwaysOnTopCheckBox.addActionListener((ActionEvent e) -> {
212            setAlwaysOnTop(alwaysOnTopCheckBox.isSelected());
213        });
214
215        autoScrollCheckBox.setVisible(true);
216        autoScrollCheckBox.setToolTipText(Bundle.getMessage("TooltipAutoScroll")); // NOI18N
217        autoScrollCheckBox.setSelected(!userPrefs.getSimplePreferenceState(autoScrollCheck));
218
219        autoScrollCheckBox.addActionListener((ActionEvent e) -> {
220            monTextPane.setAutoScroll(autoScrollCheckBox.isSelected());
221        });
222
223        pane1.add(rawCheckBox);
224        pane1.add(timeCheckBox);
225        pane1.add(alwaysOnTopCheckBox);
226        pane1.add(autoScrollCheckBox);
227        return pane1;
228    }
229
230    protected JPanel getActionButtonsPanel() {
231
232        JPanel pane1 = new JPanel();
233        pane1.setLayout(new BoxLayout(pane1, BoxLayout.X_AXIS));
234
235        clearButton.setVisible(true);
236        clearButton.setToolTipText(Bundle.getMessage("TooltipClearMonHistory")); // NOI18N
237        clearButton.addActionListener(this::clearButtonActionPerformed);
238
239        freezeButton.setVisible(true);
240        freezeButton.setToolTipText(Bundle.getMessage("TooltipStopScroll")); // NOI18N
241
242        pane1.add(clearButton);
243        pane1.add(freezeButton);
244        return pane1;
245    }
246
247    protected JPanel getLogToFilePanel() {
248        JPanel pane1 = new JPanel();
249        pane1.setLayout(new BoxLayout(pane1, BoxLayout.X_AXIS));
250
251        startLogButton.setVisible(true);
252        startLogButton.setToolTipText(Bundle.getMessage("TooltipStartLogging"));
253
254        stopLogButton.setVisible(false);
255        stopLogButton.setToolTipText(Bundle.getMessage("TooltipStopLogging"));
256
257        openFileChooserButton.setVisible(true);
258        openFileChooserButton.setToolTipText(Bundle.getMessage("TooltipChooseLogFile"));
259
260        startLogButton.addActionListener(this::startLogButtonActionPerformed);
261        stopLogButton.addActionListener(this::stopLogButtonActionPerformed);
262
263        // set file chooser to a default
264        logFileChooser.setSelectedFile(new File("monitorLog.txt"));
265        openFileChooserButton.addActionListener(this::openFileChooserButtonActionPerformed);
266
267        pane1.add(openFileChooserButton);
268        pane1.add(startLogButton);
269        pane1.add(stopLogButton);
270        return pane1;
271    }
272
273    /**
274     * Define help menu for this window.
275     * <p>
276     * By default, provides a generic help page that covers general features.
277     * Specific implementations can override this to show their own help page if
278     * desired.
279     */
280    protected void setHelp() {
281        addHelpMenu("package.jmri.jmrix.AbstractMonFrame", true); // NOI18N
282    }
283
284    /**
285     * Handle display of traffic.
286     * @param line is the traffic in 'normal form'. Should end with \n
287     * @param raw is the "raw form" , should NOT end with \n
288     */
289    public void nextLine(String line, String raw) {
290        nextLine(line, raw, null);
291    }
292
293    /**
294     * Handle display of traffic with an explicit direction marker (e.g. "TX:" or "RX:")
295     * that is shown regardless of which display checkboxes are selected.
296     * @param line is the traffic in 'normal form'. Should end with \n
297     * @param raw is the "raw form", should NOT end with \n
298     * @param direction direction marker to display, or null/empty to omit
299     */
300    public void nextLine(String line, String raw, String direction) {
301        StringBuilder sb = new StringBuilder(120);
302
303        // display the timestamp if requested
304        if (timeCheckBox.isSelected()) {
305            sb.append(df.format(new Date())).append(": "); // NOI18N
306        }
307
308        if (direction != null && !direction.isEmpty()) {
309            sb.append(direction).append(' ');
310        }
311
312        // display the raw data if requested
313        if (rawCheckBox.isSelected()) {
314            sb.append(rawOpenBracket).append(raw).append(rawCloseBracket).append("  "); // NOI18N
315        }
316
317        // display decoded data
318        sb.append(line);
319        synchronized (self) {
320            linesBuffer.append(sb.toString());
321        }
322
323        // if not frozen, display it in the Swing thread
324        if (!freezeButton.isSelected()) {
325            Runnable r = () -> {
326                synchronized (self) {
327                    monTextPane.append(linesBuffer.toString());
328                    linesBuffer.setLength(0);
329                }
330            };
331            javax.swing.SwingUtilities.invokeLater(r);
332        }
333
334        // if requested, log to a file.
335        if (logStream != null) {
336            synchronized (logStream) {
337                String logLine = sb.toString();
338                if (!newline.equals("\n")) {
339                    // have to massage the line-ends
340                    int lim = sb.length();
341                    StringBuilder out = new StringBuilder(sb.length() + 10);  // arbitrary guess at space
342                    for (int i = 0; i < lim; i++) {
343                        if (sb.charAt(i) == '\n') {
344                            out.append(newline);
345                        } else {
346                            out.append(sb.charAt(i));
347                        }
348                    }
349                    logLine = out.toString();
350                }
351                logStream.print(logLine);
352            }
353        }
354    }
355
356    String newline = System.getProperty("line.separator"); // NOI18N
357
358    public synchronized void clearButtonActionPerformed(java.awt.event.ActionEvent e) {
359        // clear the monitoring history
360        synchronized (linesBuffer) {
361            linesBuffer.setLength(0);
362            monTextPane.setText("");
363        }
364    }
365
366    public synchronized void startLogButtonActionPerformed(java.awt.event.ActionEvent e) {
367        // start logging by creating the stream
368        if (logStream == null) {  // successive clicks don't restart the file
369            // start logging
370            try {
371                logStream = new PrintStream(new FileOutputStream(logFileChooser.getSelectedFile()));
372            } catch (java.io.FileNotFoundException ex) {
373                log.error("exception", ex);
374            }
375        }
376        updateLoggingButtons();
377    }
378
379    public synchronized void stopLogButtonActionPerformed(java.awt.event.ActionEvent e) {
380        // stop logging by removing the stream
381        if (logStream != null) {
382            logStream.flush();
383            logStream.close();
384            logStream = null;
385        }
386        updateLoggingButtons();
387    }
388
389    private void updateLoggingButtons(){
390        this.startLogButton.setVisible(logStream == null);
391        this.stopLogButton.setVisible(logStream != null);
392    }
393
394    public void openFileChooserButtonActionPerformed(java.awt.event.ActionEvent e) {
395        // start at current file, show dialog
396        int retVal = logFileChooser.showSaveDialog(this);
397
398        // handle selection or cancel
399        if (retVal == JFileChooser.APPROVE_OPTION) {
400            boolean loggingNow = (logStream != null);
401            stopLogButtonActionPerformed(e);  // stop before changing file
402            //File file = logFileChooser.getSelectedFile();
403            // if we were currently logging, start the new file
404            if (loggingNow) {
405                startLogButtonActionPerformed(e);
406            }
407        }
408    }
409
410    public void enterButtonActionPerformed(java.awt.event.ActionEvent e) {
411        nextLine(entryField.getText() + "\n", ""); // NOI18N
412    }
413
414    /**
415     * Get access to the main text area.
416     * This is intended for use in e.g. scripting
417     * to extend the behaviour of the window.
418     * @return the text area.
419     */
420    public final synchronized JTextArea getTextArea() {
421        return monTextPane;
422    }
423
424    private volatile PrintStream logStream = null;
425
426    // to get a time string
427    DateFormat df = new SimpleDateFormat("HH:mm:ss.SSS");
428
429    StringBuffer linesBuffer = new StringBuffer();
430    private static int MAX_LINES = 500;
431
432    private static final org.slf4j.Logger log = org.slf4j.LoggerFactory.getLogger(AbstractMonFrame.class);
433
434}