package net.sf.openrocket.gui.dialogs; import java.awt.Color; import java.awt.Component; import java.awt.Rectangle; import java.awt.Toolkit; import java.awt.Window; import java.awt.event.ActionEvent; import java.awt.event.ActionListener; import java.awt.event.WindowAdapter; import java.awt.event.WindowEvent; import java.io.PrintWriter; import java.util.ArrayList; import java.util.Comparator; import java.util.EnumMap; import java.util.List; import java.util.Queue; import java.util.concurrent.ConcurrentLinkedQueue; import java.util.regex.Matcher; import java.util.regex.Pattern; import javax.swing.JButton; import javax.swing.JCheckBox; import javax.swing.JDialog; import javax.swing.JLabel; import javax.swing.JPanel; import javax.swing.JScrollPane; import javax.swing.JSplitPane; import javax.swing.JTable; import javax.swing.JTextArea; import javax.swing.ListSelectionModel; import javax.swing.RowFilter; import javax.swing.SwingUtilities; import javax.swing.Timer; import javax.swing.event.ListSelectionEvent; import javax.swing.event.ListSelectionListener; import javax.swing.table.TableCellRenderer; import javax.swing.table.TableModel; import javax.swing.table.TableRowSorter; import net.miginfocom.swing.MigLayout; import net.sf.openrocket.gui.adaptors.Column; import net.sf.openrocket.gui.adaptors.ColumnTable; import net.sf.openrocket.gui.adaptors.ColumnTableModel; import net.sf.openrocket.gui.components.SelectableLabel; import net.sf.openrocket.gui.util.GUIUtil; import net.sf.openrocket.l10n.Translator; import net.sf.openrocket.logging.DelegatorLogger; import net.sf.openrocket.logging.LogHelper; import net.sf.openrocket.logging.LogLevel; import net.sf.openrocket.logging.LogLevelBufferLogger; import net.sf.openrocket.logging.LogLine; import net.sf.openrocket.logging.LoggingSystemSetup; import net.sf.openrocket.logging.Markers; import net.sf.openrocket.logging.StackTraceWriter; import net.sf.openrocket.startup.Application; import net.sf.openrocket.util.NumericComparator; import org.slf4j.Logger; import org.slf4j.LoggerFactory; public class DebugLogDialog extends JDialog { private static final Logger log = LoggerFactory.getLogger(DebugLogDialog.class); private static final int POLL_TIME = 250; private static final String STACK_TRACE_MARK = "\uFF01"; private static final Translator trans = Application.getTranslator(); private static final EnumMap<LogLevel, Color> backgroundColors = new EnumMap<LogLevel, Color>(LogLevel.class); static { for (LogLevel l : LogLevel.values()) { // Just to ensure every level has a bg color backgroundColors.put(l, Color.ORANGE); } final int hi = 255; final int lo = 150; backgroundColors.put(LogLevel.ERROR, new Color(hi, lo, lo)); backgroundColors.put(LogLevel.WARN, new Color(hi, (hi + lo) / 2, lo)); backgroundColors.put(LogLevel.USER, new Color(lo, lo, hi)); backgroundColors.put(LogLevel.INFO, new Color(hi, hi, lo)); backgroundColors.put(LogLevel.DEBUG, new Color(lo, hi, lo)); backgroundColors.put(LogLevel.VBOSE, new Color(lo, hi, (hi + lo) / 2)); } /** Buffer containing the log lines displayed */ private final List<LogLine> buffer = new ArrayList<LogLine>(); /** Queue of log lines to be added to the displayed buffer */ private final Queue<LogLine> queue = new ConcurrentLinkedQueue<LogLine>(); private final DelegatorLogger delegator; private final LogListener logListener; private final EnumMap<LogLevel, JCheckBox> filterButtons = new EnumMap<LogLevel, JCheckBox>(LogLevel.class); private final JCheckBox followBox; private final Timer timer; private final JTable table; private final ColumnTableModel model; private final TableRowSorter<TableModel> sorter; private final SelectableLabel numberLabel; private final SelectableLabel timeLabel; private final SelectableLabel levelLabel; private final SelectableLabel locationLabel; private final SelectableLabel messageLabel; private final JTextArea stackTraceLabel; public DebugLogDialog(Window parent) { //// OpenRocket debug log super(parent, trans.get("debuglogdlg.OpenRocketdebuglog")); LogHelper applicationLog = LoggingSystemSetup.getInstance(); if (applicationLog instanceof DelegatorLogger) { log.info("Adding log listener"); delegator = (DelegatorLogger) applicationLog; logListener = new LogListener(); delegator.addLogger(logListener); } else { log.warn("Application log is not a DelegatorLogger"); delegator = null; logListener = null; } // Fetch old log lines LogLevelBufferLogger bufferLogger = LoggingSystemSetup.getBufferLogger(); if (bufferLogger != null) { buffer.addAll(bufferLogger.getLogs()); } else { log.warn("Application does not have a log buffer"); } // Create the UI JPanel mainPanel = new JPanel(new MigLayout("fill")); this.add(mainPanel); JSplitPane split = new JSplitPane(JSplitPane.VERTICAL_SPLIT); split.setDividerLocation(0.7); mainPanel.add(split, "grow"); // Top panel JPanel panel = new JPanel(new MigLayout("fill")); split.add(panel); //// Display log lines: panel.add(new JLabel(trans.get("debuglogdlg.Displayloglines")), "gapright para, split"); for (LogLevel l : LogLevel.values()) { JCheckBox box = new JCheckBox(l.toString()); // By default display DEBUG and above box.setSelected(l.atLeast(LogLevel.DEBUG)); box.addActionListener(new ActionListener() { @Override public void actionPerformed(ActionEvent e) { sorter.setRowFilter(new LogFilter()); } }); panel.add(box, "gapright unrel"); filterButtons.put(l, box); } //// Follow followBox = new JCheckBox(trans.get("debuglogdlg.Follow")); followBox.setSelected(true); panel.add(followBox, "skip, gapright para, right"); //// Clear button JButton clear = new JButton(trans.get("debuglogdlg.but.clear")); clear.addActionListener(new ActionListener() { @Override public void actionPerformed(ActionEvent e) { log.info(Markers.USER_MARKER, "Clearing log buffer"); buffer.clear(); queue.clear(); model.fireTableDataChanged(); } }); panel.add(clear, "right, wrap"); // Create the table model model = new ColumnTableModel( new Column("#") { @Override public Object getValueAt(int row) { return buffer.get(row).getLogCount(); } @Override public int getDefaultWidth() { return 60; } }, //// Time new Column(trans.get("debuglogdlg.col.Time")) { @Override public Object getValueAt(int row) { return String.format("%.3f", buffer.get(row).getTimestamp() / 1000.0); } @Override public int getDefaultWidth() { return 60; } }, //// Level new Column(trans.get("debuglogdlg.col.Level")) { @Override public Object getValueAt(int row) { return buffer.get(row).getLevel(); } @Override public int getDefaultWidth() { return 60; } }, new Column("") { @Override public Object getValueAt(int row) { if (buffer.get(row).getCause() != null) { return STACK_TRACE_MARK; } else { return ""; } } @Override public int getExactWidth() { return 16; } }, //// Location new Column(trans.get("debuglogdlg.col.Location")) { @Override public Object getValueAt(int row) { String e = buffer.get(row).getLocation(); return e; } @Override public int getDefaultWidth() { return 200; } }, //// Message new Column(trans.get("debuglogdlg.col.Message")) { @Override public Object getValueAt(int row) { return buffer.get(row).getMessage(); } @Override public int getDefaultWidth() { return 580; } } ) { @Override public int getRowCount() { return buffer.size(); } }; table = new ColumnTable(model); table.setSelectionMode(ListSelectionModel.MULTIPLE_INTERVAL_SELECTION); table.setSelectionBackground(Color.LIGHT_GRAY); table.setSelectionForeground(Color.BLACK); model.setColumnWidths(table.getColumnModel()); table.setDefaultRenderer(Object.class, new Renderer()); table.getSelectionModel().addListSelectionListener(new ListSelectionListener() { @Override public void valueChanged(ListSelectionEvent e) { int row = table.getSelectedRow(); if (row >= 0) { row = sorter.convertRowIndexToModel(row); } updateSelected(row); } }); sorter = new TableRowSorter<TableModel>(model); sorter.setComparator(0, NumericComparator.INSTANCE); sorter.setComparator(1, NumericComparator.INSTANCE); sorter.setComparator(4, new LocationComparator()); table.setRowSorter(sorter); sorter.setRowFilter(new LogFilter()); panel.add(new JScrollPane(table), "span, grow, width " + (Toolkit.getDefaultToolkit().getScreenSize().width * 8 / 10) + "px, height 400px"); panel = new JPanel(new MigLayout("fill")); split.add(panel); //// Log line number: panel.add(new JLabel(trans.get("debuglogdlg.lbl.Loglinenbr")), "split, gapright rel"); numberLabel = new SelectableLabel(); panel.add(numberLabel, "width 70lp, gapright para"); //// Time: panel.add(new JLabel(trans.get("debuglogdlg.lbl.Time")), "split, gapright rel"); timeLabel = new SelectableLabel(); panel.add(timeLabel, "width 70lp, gapright para"); //// Level: panel.add(new JLabel(trans.get("debuglogdlg.lbl.Level")), "split, gapright rel"); levelLabel = new SelectableLabel(); panel.add(levelLabel, "width 70lp, gapright para"); //// Location: panel.add(new JLabel(trans.get("debuglogdlg.lbl.Location")), "split, gapright rel"); locationLabel = new SelectableLabel(); panel.add(locationLabel, "growx, wrap unrel"); //// Log message: panel.add(new JLabel(trans.get("debuglogdlg.lbl.Logmessage")), "split, gapright rel"); messageLabel = new SelectableLabel(); panel.add(messageLabel, "growx, wrap para"); //// Stack trace: panel.add(new JLabel(trans.get("debuglogdlg.lbl.Stacktrace")), "wrap rel"); stackTraceLabel = new JTextArea(8, 80); stackTraceLabel.setEditable(false); GUIUtil.changeFontSize(stackTraceLabel, -2); panel.add(new JScrollPane(stackTraceLabel), "grow"); //Close button JButton close = new JButton(trans.get("dlg.but.close")); close.addActionListener(new ActionListener() { @Override public void actionPerformed(ActionEvent e) { DebugLogDialog.this.dispose(); } }); mainPanel.add(close, "newline para, right, tag ok"); // Use timer to purge the queue so as not to overwhelm the EDT with events timer = new Timer(POLL_TIME, new ActionListener() { @Override public void actionPerformed(ActionEvent e) { purgeQueue(); } }); timer.setRepeats(true); timer.start(); this.addWindowListener(new WindowAdapter() { @Override public void windowClosed(WindowEvent e) { log.info(Markers.USER_MARKER, "Closing debug log dialog"); timer.stop(); if (delegator != null) { log.info("Removing log listener"); delegator.removeLogger(logListener); } } }); GUIUtil.setDisposableDialogOptions(this, close); followBox.requestFocus(); } private void updateSelected(int row) { if (row < 0) { numberLabel.setText(""); timeLabel.setText(""); levelLabel.setText(""); locationLabel.setText(""); messageLabel.setText(""); stackTraceLabel.setText(""); } else { LogLine line = buffer.get(row); numberLabel.setText("" + line.getLogCount()); timeLabel.setText(String.format("%.3f s", line.getTimestamp() / 1000.0)); levelLabel.setText(line.getLevel().toString()); String e = line.getLocation(); locationLabel.setText(e); messageLabel.setText(line.getMessage()); Throwable t = line.getCause(); if (t != null) { StackTraceWriter stw = new StackTraceWriter(); PrintWriter pw = new PrintWriter(stw); t.printStackTrace(pw); pw.flush(); stackTraceLabel.setText(stw.toString()); stackTraceLabel.setCaretPosition(0); } else { stackTraceLabel.setText(""); } } } /** * Check whether a row signifies a number of missing rows. This check is "heuristic" * and checks whether the timestamp is zero and the message starts with "---". */ private boolean isExcludedRow(int row) { LogLine line = buffer.get(row); return (line.getTimestamp() == 0) && (line.getMessage().startsWith("---")); } /** * Purge the queue of incoming log lines. This is called periodically from the EDT, and * it adds any lines in the queue to the buffer, and fires a table event. */ private void purgeQueue() { int start = buffer.size(); LogLine line; while ((line = queue.poll()) != null) { buffer.add(line); } int end = buffer.size() - 1; if (end >= start) { model.fireTableRowsInserted(start, end); if (followBox.isSelected()) { SwingUtilities.invokeLater(new Runnable() { @Override public void run() { Rectangle rect = table.getCellRect(1000000000, 1, true); table.scrollRectToVisible(rect); } }); } } } /** * A logger that adds log lines to the queue. This method may be called from any * thread, and therefore must be thread-safe. */ private class LogListener extends LogHelper { @Override public void log(LogLine line) { queue.add(line); } } private class LogFilter extends RowFilter<TableModel, Integer> { @Override public boolean include(RowFilter.Entry<? extends TableModel, ? extends Integer> entry) { int index = entry.getIdentifier(); LogLine line = buffer.get(index); return filterButtons.get(line.getLevel()).isSelected(); } } private class Renderer extends JLabel implements TableCellRenderer { @Override public Component getTableCellRendererComponent(JTable table1, Object value, boolean isSelected, boolean hasFocus, int row, int column) { Color fg, bg; row = sorter.convertRowIndexToModel(row); if (STACK_TRACE_MARK.equals(value)) { fg = Color.RED; } else { fg = table1.getForeground(); } bg = backgroundColors.get(buffer.get(row).getLevel()); if (isSelected) { bg = bg.darker(); } else if (isExcludedRow(row)) { bg = bg.brighter(); } this.setForeground(fg); this.setBackground(bg); this.setOpaque(true); this.setText(String.valueOf(value)); return this; } } private class LocationComparator implements Comparator<Object> { private final Pattern splitPattern = Pattern.compile("^\\(([^:]*+):([0-9]++).*\\)$"); @Override public int compare(Object o1, Object o2) { String s1 = o1.toString(); String s2 = o2.toString(); Matcher m1 = splitPattern.matcher(s1); Matcher m2 = splitPattern.matcher(s2); if (m1.matches() && m2.matches()) { String class1 = m1.group(1); String pos1 = m1.group(2); String class2 = m2.group(1); String pos2 = m2.group(2); if (class1.equals(class2)) { return NumericComparator.INSTANCE.compare(pos1, pos2); } else { return class1.compareTo(class2); } } return s1.compareTo(s2); } } }