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