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}