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