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