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}