001package apps; 002 003import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; 004 005import java.awt.*; 006import java.awt.datatransfer.Clipboard; 007import java.awt.datatransfer.StringSelection; 008import java.awt.event.*; 009import java.io.*; 010import java.lang.reflect.InvocationTargetException; 011import java.util.*; 012 013import javax.swing.*; 014 015import jmri.UserPreferencesManager; 016import jmri.util.JmriJFrame; 017import jmri.util.swing.TextAreaFIFO; 018 019/** 020 * Class to direct standard output and standard error to a ( JTextArea ) TextAreaFIFO . 021 * This allows for easier clipboard operations etc. 022 * <hr> 023 * This file is part of JMRI. 024 * <p> 025 * JMRI is free software; you can redistribute it and/or modify it under the 026 * terms of version 2 of the GNU General Public License as published by the Free 027 * Software Foundation. See the "COPYING" file for a copy of this license. 028 * <p> 029 * JMRI is distributed in the hope that it will be useful, but WITHOUT ANY 030 * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR 031 * A PARTICULAR PURPOSE. See the GNU General Public License for more details. 032 * 033 * @author Matthew Harris copyright (c) 2010, 2011, 2012 034 */ 035public final class SystemConsole { 036 037 /** 038 * Get current SystemConsole instance. 039 * If one doesn't yet exist, create it. 040 * @return current SystemConsole instance 041 */ 042 public static SystemConsole getInstance() { 043 return InstanceHolder.INSTANCE; 044 } 045 046 private static class InstanceHolder { 047 private static final SystemConsole INSTANCE; 048 049 static { 050 SystemConsole instance = null; 051 try { 052 instance = new SystemConsole(); 053 } catch (RuntimeException ex) { 054 log.error("failed to complete Console redirection", ex); 055 } 056 INSTANCE = instance; 057 } 058 } 059 060 static final ResourceBundle rbc = ResourceBundle.getBundle("apps.AppsConfigBundle"); // NOI18N 061 062 private static final int STD_ERR = 1; 063 private static final int STD_OUT = 2; 064 065 private final TextAreaFIFO console; 066 067 private final PrintStream originalOut; 068 private final PrintStream originalErr; 069 070 private final PrintStream outputStream; 071 private final PrintStream errorStream; 072 073 private JmriJFrame frame = null; 074 075 private final JPopupMenu popup = new JPopupMenu(); 076 077 private JMenuItem copySelection = null; 078 079 private JMenu wrapMenu = null; 080 private ButtonGroup wrapGroup = null; 081 082 private JMenu schemeMenu = null; 083 private ButtonGroup schemeGroup = null; 084 085 private ArrayList<Scheme> schemes; 086 087 private int scheme = 0; // Green on Black 088 089 private int fontSize = 12; 090 091 private int fontStyle = Font.PLAIN; 092 093 private static final String FONT_FAMILY = "Monospaced"; 094 095 public static final int WRAP_STYLE_NONE = 0x00; 096 public static final int WRAP_STYLE_LINE = 0x01; 097 public static final int WRAP_STYLE_WORD = 0x02; 098 099 private int wrapStyle = WRAP_STYLE_WORD; 100 101 private final String alwaysScrollCheck = this.getClass().getName() + ".alwaysScroll"; // NOI18N 102 private final String alwaysOnTopCheck = this.getClass().getName() + ".alwaysOnTop"; // NOI18N 103 104 public int MAX_CONSOLE_LINES = 5000; // public, not static so can be modified via a script 105 106 /** 107 * Initialise the system console ensuring both System.out and System.err 108 * streams are re-directed to the consoles JTextArea 109 */ 110 111 @SuppressFBWarnings(value = "DM_DEFAULT_ENCODING", 112 justification = "Can only be called from the same instance so default encoding OK") 113 private SystemConsole() { 114 // Record current System.out and System.err 115 // so that we can still send to them 116 originalOut = System.out; 117 originalErr = System.err; 118 119 // Create the console text area 120 console = new TextAreaFIFO(MAX_CONSOLE_LINES); 121 122 // Setup the console text area 123 console.setRows(20); 124 console.setColumns(120); 125 console.setFont(new Font(FONT_FAMILY, fontStyle, fontSize)); 126 console.setEditable(false); 127 setScheme(scheme); 128 setWrapStyle(wrapStyle); 129 130 this.outputStream = new PrintStream(outStream(STD_OUT), true); 131 this.errorStream = new PrintStream(outStream(STD_ERR), true); 132 133 // Then redirect to it 134 redirectSystemStreams(outputStream, errorStream); 135 } 136 137 /** 138 * Return the JFrame containing the console 139 * 140 * @return console JFrame 141 */ 142 public static JFrame getConsole() { 143 return SystemConsole.getInstance().getFrame(); 144 } 145 146 public JFrame getFrame() { 147 148 // Check if we've created the frame and do so if not 149 if (frame == null) { 150 log.debug("Creating frame for console"); 151 // To avoid possible locks, frame layout should be 152 // performed on the Swing thread 153 if (SwingUtilities.isEventDispatchThread()) { 154 createFrame(); 155 } else { 156 try { 157 // Use invokeAndWait method as we don't want to 158 // return until the frame layout is completed 159 SwingUtilities.invokeAndWait(this::createFrame); 160 } catch (InvocationTargetException ex) { 161 log.error("Invocation Exception creating system console frame", ex); 162 } catch (InterruptedException ex) { 163 log.error("Interrupt Exception creating system console frame", ex); 164 Thread.currentThread().interrupt(); 165 } 166 } 167 log.debug("Frame created"); 168 } 169 170 return frame; 171 } 172 173 /** 174 * Layout the console frame 175 */ 176 private void createFrame() { 177 // Use a JmriJFrame to ensure that we fit on the screen 178 frame = new JmriJFrame(Bundle.getMessage("TitleConsole")); 179 180 UserPreferencesManager pref = jmri.InstanceManager.getDefault(UserPreferencesManager.class); 181 182 // Add Help menu (Windows menu automaitically added) 183 frame.addHelpMenu("package.apps.SystemConsole", true); // NOI18N 184 185 // Grab a reference to the system clipboard 186 final Clipboard clipboard = frame.getToolkit().getSystemClipboard(); 187 188 // Setup the scroll pane 189 JScrollPane scroll = new JScrollPane(console); 190 frame.add(scroll, BorderLayout.CENTER); 191 192 193 JPanel p = new JPanel(); 194 195 // Add button to clear display 196 JButton clear = new JButton(Bundle.getMessage("ButtonClear")); 197 clear.addActionListener( e -> console.setText("")); 198 clear.setToolTipText(Bundle.getMessage("ButtonClearTip")); 199 p.add(clear); 200 201 // Add button to allow copy to clipboard 202 JButton copy = new JButton(Bundle.getMessage("ButtonCopyClip")); 203 copy.addActionListener( e -> { 204 StringSelection text = new StringSelection(console.getText()); 205 clipboard.setContents(text, text); 206 }); 207 p.add(copy); 208 209 // Add button to allow console window to be closed 210 JButton close = new JButton(Bundle.getMessage("ButtonClose")); 211 close.addActionListener( e -> { 212 frame.setVisible(false); 213 console.dispose(); 214 frame.dispose(); 215 }); 216 p.add(close); 217 218 JButton stackTrace = new JButton(Bundle.getMessage("ButtonStackTrace")); 219 stackTrace.addActionListener( e -> performStackTrace()); 220 p.add(stackTrace); 221 222 // Add checkbox to enable/disable auto-scrolling 223 // Use the inverted SimplePreferenceState to default as enabled 224 JCheckBox autoScroll = new JCheckBox(Bundle.getMessage("CheckBoxAutoScroll")); 225 p.add( autoScroll, !pref.getSimplePreferenceState(alwaysScrollCheck)); 226 console.setAutoScroll(autoScroll.isSelected()); 227 autoScroll.addActionListener((ActionEvent event) -> { 228 console.setAutoScroll(autoScroll.isSelected()); 229 pref.setSimplePreferenceState(alwaysScrollCheck, !autoScroll.isSelected()); 230 }); 231 232 // Add checkbox to enable/disable always on top 233 JCheckBox alwaysOnTop = new JCheckBox(Bundle.getMessage("CheckBoxOnTop")); 234 p.add( alwaysOnTop, pref.getSimplePreferenceState(alwaysOnTopCheck)); 235 alwaysOnTop.setVisible(true); 236 alwaysOnTop.setToolTipText(Bundle.getMessage("ToolTipOnTop")); 237 alwaysOnTop.addActionListener((ActionEvent event) -> { 238 frame.setAlwaysOnTop(alwaysOnTop.isSelected()); 239 pref.setSimplePreferenceState(alwaysOnTopCheck, alwaysOnTop.isSelected()); 240 }); 241 242 frame.setAlwaysOnTop(alwaysOnTop.isSelected()); 243 244 // Define the pop-up menu 245 copySelection = new JMenuItem(Bundle.getMessage("MenuItemCopy")); 246 copySelection.addActionListener((ActionEvent event) -> { 247 StringSelection text = new StringSelection(console.getSelectedText()); 248 clipboard.setContents(text, text); 249 }); 250 popup.add(copySelection); 251 252 JMenuItem menuItem = new JMenuItem(Bundle.getMessage("ButtonCopyClip")); 253 menuItem.addActionListener((ActionEvent event) -> { 254 StringSelection text = new StringSelection(console.getText()); 255 clipboard.setContents(text, text); 256 }); 257 popup.add(menuItem); 258 259 popup.add(new JSeparator()); 260 261 JRadioButtonMenuItem rbMenuItem; 262 263 // Define the colour scheme sub-menu 264 schemeMenu = new JMenu(rbc.getString("ConsoleSchemeMenu")); 265 schemeGroup = new ButtonGroup(); 266 for (final Scheme s : schemes) { 267 rbMenuItem = new JRadioButtonMenuItem(s.description); 268 rbMenuItem.addActionListener( e -> setScheme(schemes.indexOf(s))); 269 rbMenuItem.setSelected(getScheme() == schemes.indexOf(s)); 270 schemeMenu.add(rbMenuItem); 271 schemeGroup.add(rbMenuItem); 272 } 273 popup.add(schemeMenu); 274 275 // Define the wrap style sub-menu 276 wrapMenu = new JMenu(rbc.getString("ConsoleWrapStyleMenu")); 277 wrapGroup = new ButtonGroup(); 278 rbMenuItem = new JRadioButtonMenuItem(rbc.getString("ConsoleWrapStyleNone")); 279 rbMenuItem.addActionListener( e -> setWrapStyle(WRAP_STYLE_NONE)); 280 rbMenuItem.setSelected(getWrapStyle() == WRAP_STYLE_NONE); 281 wrapMenu.add(rbMenuItem); 282 wrapGroup.add(rbMenuItem); 283 284 rbMenuItem = new JRadioButtonMenuItem(rbc.getString("ConsoleWrapStyleLine")); 285 rbMenuItem.addActionListener( e -> setWrapStyle(WRAP_STYLE_LINE)); 286 rbMenuItem.setSelected(getWrapStyle() == WRAP_STYLE_LINE); 287 wrapMenu.add(rbMenuItem); 288 wrapGroup.add(rbMenuItem); 289 290 rbMenuItem = new JRadioButtonMenuItem(rbc.getString("ConsoleWrapStyleWord")); 291 rbMenuItem.addActionListener( e -> setWrapStyle(WRAP_STYLE_WORD)); 292 rbMenuItem.setSelected(getWrapStyle() == WRAP_STYLE_WORD); 293 wrapMenu.add(rbMenuItem); 294 wrapGroup.add(rbMenuItem); 295 296 popup.add(wrapMenu); 297 298 // Bind pop-up to objects 299 MouseListener popupListener = new PopupListener(); 300 console.addMouseListener(popupListener); 301 frame.addMouseListener(popupListener); 302 303 // Add the button panel to the frame & then arrange everything 304 frame.add(p, BorderLayout.SOUTH); 305 frame.pack(); 306 } 307 308 /** 309 * Add text to the console 310 * 311 * @param text the text to add 312 * @param which the stream that this text is for 313 */ 314 private void updateTextArea(final String text, final int which) { 315 // Append message to the original System.out / System.err streams 316 if (which == STD_OUT) { 317 originalOut.append(text); 318 } else if (which == STD_ERR) { 319 originalErr.append(text); 320 } 321 322 // Now append to the JTextArea 323 SwingUtilities.invokeLater(() -> { 324 synchronized (SystemConsole.this) { 325 console.append(text); } 326 }); 327 328 } 329 330 /** 331 * Creates a new OutputStream for the specified stream 332 * 333 * @param which the stream, either STD_OUT or STD_ERR 334 * @return the new OutputStream 335 */ 336 private OutputStream outStream(final int which) { 337 return new OutputStream() { 338 @Override 339 public void write(int b) throws IOException { 340 updateTextArea(String.valueOf((char) b), which); 341 } 342 343 @Override 344 @SuppressFBWarnings(value = "DM_DEFAULT_ENCODING", 345 justification = "Can only be called from the same instance so default encoding OK") 346 public void write(byte[] b, int off, int len) throws IOException { 347 updateTextArea(new String(b, off, len), which); 348 } 349 350 @Override 351 public void write(byte[] b) throws IOException { 352 write(b, 0, b.length); 353 } 354 }; 355 } 356 357 /** 358 * Method to redirect the system streams to the console 359 */ 360 private void redirectSystemStreams(PrintStream out, PrintStream err) { 361 System.setOut(out); 362 System.setErr(err); 363 } 364 365 /** 366 * Set the console wrapping style to one of the following: 367 * 368 * @param style one of the defined style attributes - one of 369 * <ul> 370 * <li>{@link #WRAP_STYLE_NONE} No wrapping 371 * <li>{@link #WRAP_STYLE_LINE} Wrap at end of line 372 * <li>{@link #WRAP_STYLE_WORD} Wrap by word boundaries 373 * </ul> 374 */ 375 public void setWrapStyle(int style) { 376 wrapStyle = style; 377 console.setLineWrap(style != WRAP_STYLE_NONE); 378 console.setWrapStyleWord(style == WRAP_STYLE_WORD); 379 380 if (wrapGroup != null) { 381 wrapGroup.setSelected(wrapMenu.getItem(style).getModel(), true); 382 } 383 } 384 385 /** 386 * Retrieve the current console wrapping style 387 * 388 * @return current wrapping style - one of 389 * <ul> 390 * <li>{@link #WRAP_STYLE_NONE} No wrapping 391 * <li>{@link #WRAP_STYLE_LINE} Wrap at end of line 392 * <li>{@link #WRAP_STYLE_WORD} Wrap by word boundaries (default) 393 * </ul> 394 */ 395 public int getWrapStyle() { 396 return wrapStyle; 397 } 398 399 /** 400 * Set the console font size 401 * 402 * @param size point size of font between 6 and 28 point 403 */ 404 public void setFontSize(int size) { 405 updateFont(FONT_FAMILY, fontStyle, (fontSize = size < 6 ? 6 : size > 28 ? 28 : size)); 406 } 407 408 /** 409 * Retrieve the current console font size (default 12 point) 410 * 411 * @return selected font size in points 412 */ 413 public int getFontSize() { 414 return fontSize; 415 } 416 417 /** 418 * Set the console font style 419 * 420 * @param style one of 421 * {@link Font#BOLD}, {@link Font#ITALIC}, {@link Font#PLAIN} 422 * (default) 423 */ 424 public void setFontStyle(int style) { 425 426 if (style == Font.BOLD || style == Font.ITALIC || style == Font.PLAIN || style == (Font.BOLD | Font.ITALIC)) { 427 fontStyle = style; 428 } else { 429 fontStyle = Font.PLAIN; 430 } 431 updateFont(FONT_FAMILY, fontStyle, fontSize); 432 } 433 434 /** 435 * Retrieve the current console font style 436 * 437 * @return selected font style - one of 438 * {@link Font#BOLD}, {@link Font#ITALIC}, {@link Font#PLAIN} 439 * (default) 440 */ 441 public int getFontStyle() { 442 return fontStyle; 443 } 444 445 /** 446 * Update the system console font with the specified parameters 447 * 448 * @param style font style 449 * @param size font size 450 */ 451 private void updateFont(String family, int style, int size) { 452 console.setFont(new Font(family, style, size)); 453 } 454 455 /** 456 * Method to define console colour schemes 457 */ 458 private void defineSchemes() { 459 schemes = new ArrayList<>(); 460 schemes.add(new Scheme(rbc.getString("ConsoleSchemeGreenOnBlack"), Color.GREEN, Color.BLACK)); 461 schemes.add(new Scheme(rbc.getString("ConsoleSchemeOrangeOnBlack"), Color.ORANGE, Color.BLACK)); 462 schemes.add(new Scheme(rbc.getString("ConsoleSchemeWhiteOnBlack"), Color.WHITE, Color.BLACK)); 463 schemes.add(new Scheme(rbc.getString("ConsoleSchemeBlackOnWhite"), Color.BLACK, Color.WHITE)); 464 schemes.add(new Scheme(rbc.getString("ConsoleSchemeWhiteOnBlue"), Color.WHITE, Color.BLUE)); 465 schemes.add(new Scheme(rbc.getString("ConsoleSchemeBlackOnLightGray"), Color.BLACK, Color.LIGHT_GRAY)); 466 schemes.add(new Scheme(rbc.getString("ConsoleSchemeBlackOnGray"), Color.BLACK, Color.GRAY)); 467 schemes.add(new Scheme(rbc.getString("ConsoleSchemeWhiteOnGray"), Color.WHITE, Color.GRAY)); 468 schemes.add(new Scheme(rbc.getString("ConsoleSchemeWhiteOnDarkGray"), Color.WHITE, Color.DARK_GRAY)); 469 schemes.add(new Scheme(rbc.getString("ConsoleSchemeGreenOnDarkGray"), Color.GREEN, Color.DARK_GRAY)); 470 schemes.add(new Scheme(rbc.getString("ConsoleSchemeOrangeOnDarkGray"), Color.ORANGE, Color.DARK_GRAY)); 471 } 472 473 @SuppressWarnings("deprecation") // The method getId() from the type Thread is deprecated since version 19 474 // The replacement Thread.threadId() isn't available before version 19 475 private void performStackTrace() { 476 System.out.println("----------- Begin Stack Trace -----------"); //NO18N 477 System.out.println("-----------------------------------------"); //NO18N 478 Map<Thread, StackTraceElement[]> traces = new HashMap<>(Thread.getAllStackTraces()); 479 for (Thread thread : traces.keySet()) { 480 System.out.println("[" + thread.getId() + "] " + thread.getName()); 481 for (StackTraceElement el : thread.getStackTrace()) { 482 System.out.println(" " + el); 483 } 484 System.out.println("-----------------------------------------"); //NO18N 485 } 486 System.out.println("----------- End Stack Trace -----------"); //NO18N 487 } 488 489 /** 490 * Set the console colour scheme 491 * 492 * @param which the scheme to use 493 */ 494 public void setScheme(int which) { 495 scheme = which; 496 497 if (schemes == null) { 498 defineSchemes(); 499 } 500 501 Scheme s; 502 503 try { 504 s = schemes.get(which); 505 } catch (IndexOutOfBoundsException ex) { 506 s = schemes.get(0); 507 scheme = 0; 508 } 509 510 console.setForeground(s.foreground); 511 console.setBackground(s.background); 512 513 if (schemeGroup != null) { 514 schemeGroup.setSelected(schemeMenu.getItem(scheme).getModel(), true); 515 } 516 } 517 518 public PrintStream getOutputStream() { 519 return this.outputStream; 520 } 521 522 public PrintStream getErrorStream() { 523 return this.errorStream; 524 } 525 526 /** 527 * Stop logging System output and error streams to the console. 528 */ 529 public void close() { 530 redirectSystemStreams(originalOut, originalErr); 531 } 532 533 /** 534 * Start logging System output and error streams to the console. 535 */ 536 public void open() { 537 redirectSystemStreams(getOutputStream(), getErrorStream()); 538 } 539 540 /** 541 * Retrieve the current console colour scheme 542 * 543 * @return selected colour scheme 544 */ 545 public int getScheme() { 546 return scheme; 547 } 548 549 public Scheme[] getSchemes() { 550 return this.schemes.toArray(new Scheme[this.schemes.size()]); 551 // return this.schemes.toArray(Scheme[]::new); 552 // It should be possible to use the line above, however causes eclipse compilation error 553 // Annotation type 'org.eclipse.jdt.annotation.NonNull' cannot be found on the build path, 554 // which is implicitly needed for null analysis. 555 } 556 557 /** 558 * Class holding details of each scheme 559 */ 560 public static final class Scheme { 561 562 public Color foreground; 563 public Color background; 564 public String description; 565 566 Scheme(String description, Color foreground, Color background) { 567 this.foreground = foreground; 568 this.background = background; 569 this.description = description; 570 } 571 } 572 573 /** 574 * Class to deal with handling popup menu 575 */ 576 public final class PopupListener extends MouseAdapter { 577 578 @Override 579 public void mousePressed(MouseEvent e) { 580 maybeShowPopup(e); 581 } 582 583 @Override 584 public void mouseReleased(MouseEvent e) { 585 maybeShowPopup(e); 586 } 587 588 private void maybeShowPopup(MouseEvent e) { 589 if (e.isPopupTrigger()) { 590 copySelection.setEnabled(console.getSelectionStart() != console.getSelectionEnd()); 591 popup.show(e.getComponent(), e.getX(), e.getY()); 592 } 593 } 594 } 595 596 private static final org.slf4j.Logger log = org.slf4j.LoggerFactory.getLogger(SystemConsole.class); 597 598}