1 package net.sf.openrocket.gui.dialogs;
4 import java.awt.Component;
5 import java.awt.Rectangle;
6 import java.awt.Toolkit;
7 import java.awt.Window;
8 import java.awt.event.ActionEvent;
9 import java.awt.event.ActionListener;
10 import java.awt.event.WindowAdapter;
11 import java.awt.event.WindowEvent;
12 import java.io.PrintWriter;
13 import java.util.ArrayList;
14 import java.util.Comparator;
15 import java.util.EnumMap;
16 import java.util.List;
17 import java.util.Queue;
18 import java.util.concurrent.ConcurrentLinkedQueue;
19 import java.util.regex.Matcher;
20 import java.util.regex.Pattern;
22 import javax.swing.JButton;
23 import javax.swing.JCheckBox;
24 import javax.swing.JDialog;
25 import javax.swing.JLabel;
26 import javax.swing.JPanel;
27 import javax.swing.JScrollPane;
28 import javax.swing.JSplitPane;
29 import javax.swing.JTable;
30 import javax.swing.JTextArea;
31 import javax.swing.ListSelectionModel;
32 import javax.swing.RowFilter;
33 import javax.swing.SwingUtilities;
34 import javax.swing.Timer;
35 import javax.swing.event.ListSelectionEvent;
36 import javax.swing.event.ListSelectionListener;
37 import javax.swing.table.TableCellRenderer;
38 import javax.swing.table.TableModel;
39 import javax.swing.table.TableRowSorter;
41 import net.miginfocom.swing.MigLayout;
42 import net.sf.openrocket.gui.adaptors.Column;
43 import net.sf.openrocket.gui.adaptors.ColumnTableModel;
44 import net.sf.openrocket.gui.components.SelectableLabel;
45 import net.sf.openrocket.logging.DelegatorLogger;
46 import net.sf.openrocket.logging.LogHelper;
47 import net.sf.openrocket.logging.LogLevel;
48 import net.sf.openrocket.logging.LogLevelBufferLogger;
49 import net.sf.openrocket.logging.LogLine;
50 import net.sf.openrocket.logging.StackTraceWriter;
51 import net.sf.openrocket.logging.TraceException;
52 import net.sf.openrocket.startup.Application;
53 import net.sf.openrocket.util.GUIUtil;
54 import net.sf.openrocket.util.NumericComparator;
56 public class DebugLogDialog extends JDialog {
57 private static final LogHelper log = Application.getLogger();
59 private static final int POLL_TIME = 250;
60 private static final String STACK_TRACE_MARK = "\uFF01";
62 private static final EnumMap<LogLevel, Color> backgroundColors = new EnumMap<LogLevel, Color>(LogLevel.class);
64 for (LogLevel l : LogLevel.values()) {
65 // Just to ensure every level has a bg color
66 backgroundColors.put(l, Color.ORANGE);
70 backgroundColors.put(LogLevel.ERROR, new Color(hi, lo, lo));
71 backgroundColors.put(LogLevel.WARN, new Color(hi, (hi + lo) / 2, lo));
72 backgroundColors.put(LogLevel.USER, new Color(lo, lo, hi));
73 backgroundColors.put(LogLevel.INFO, new Color(hi, hi, lo));
74 backgroundColors.put(LogLevel.DEBUG, new Color(lo, hi, lo));
75 backgroundColors.put(LogLevel.VBOSE, new Color(lo, hi, (hi + lo) / 2));
78 /** Buffer containing the log lines displayed */
79 private final List<LogLine> buffer = new ArrayList<LogLine>();
81 /** Queue of log lines to be added to the displayed buffer */
82 private final Queue<LogLine> queue = new ConcurrentLinkedQueue<LogLine>();
84 private final DelegatorLogger delegator;
85 private final LogListener logListener;
87 private final EnumMap<LogLevel, JCheckBox> filterButtons = new EnumMap<LogLevel, JCheckBox>(LogLevel.class);
88 private final JCheckBox followBox;
89 private final Timer timer;
92 private final JTable table;
93 private final ColumnTableModel model;
94 private final TableRowSorter<TableModel> sorter;
96 private final SelectableLabel numberLabel;
97 private final SelectableLabel timeLabel;
98 private final SelectableLabel levelLabel;
99 private final SelectableLabel locationLabel;
100 private final SelectableLabel messageLabel;
101 private final JTextArea stackTraceLabel;
103 public DebugLogDialog(Window parent) {
104 super(parent, "OpenRocket debug log");
106 // Start listening to log lines
107 LogHelper applicationLog = Application.getLogger();
108 if (applicationLog instanceof DelegatorLogger) {
109 log.info("Adding log listener");
110 delegator = (DelegatorLogger) applicationLog;
111 logListener = new LogListener();
112 delegator.addLogger(logListener);
114 log.warn("Application log is not a DelegatorLogger");
119 // Fetch old log lines
120 LogLevelBufferLogger bufferLogger = Application.getLogBuffer();
121 if (bufferLogger != null) {
122 buffer.addAll(bufferLogger.getLogs());
124 log.warn("Application does not have a log buffer");
129 JPanel mainPanel = new JPanel(new MigLayout("fill"));
133 JSplitPane split = new JSplitPane(JSplitPane.VERTICAL_SPLIT);
134 split.setDividerLocation(0.7);
135 mainPanel.add(split, "grow");
138 JPanel panel = new JPanel(new MigLayout("fill"));
141 panel.add(new JLabel("Display log lines:"), "gapright para, split");
142 for (LogLevel l : LogLevel.values()) {
143 JCheckBox box = new JCheckBox(l.toString());
144 // By default display DEBUG and above
145 box.setSelected(l.atLeast(LogLevel.DEBUG));
146 box.addActionListener(new ActionListener() {
148 public void actionPerformed(ActionEvent e) {
149 sorter.setRowFilter(new LogFilter());
152 panel.add(box, "gapright unrel");
153 filterButtons.put(l, box);
156 followBox = new JCheckBox("Follow");
157 followBox.setSelected(true);
158 panel.add(followBox, "skip, gapright para, right");
160 JButton clear = new JButton("Clear");
161 clear.addActionListener(new ActionListener() {
163 public void actionPerformed(ActionEvent e) {
164 log.user("Clearing log buffer");
167 model.fireTableDataChanged();
170 panel.add(clear, "right, wrap");
174 // Create the table model
175 model = new ColumnTableModel(
179 public Object getValueAt(int row) {
180 return buffer.get(row).getLogCount();
184 public int getDefaultWidth() {
190 public Object getValueAt(int row) {
191 return String.format("%.3f", buffer.get(row).getTimestamp() / 1000.0);
195 public int getDefaultWidth() {
199 new Column("Level") {
201 public Object getValueAt(int row) {
202 return buffer.get(row).getLevel();
206 public int getDefaultWidth() {
212 public Object getValueAt(int row) {
213 if (buffer.get(row).getCause() != null) {
214 return STACK_TRACE_MARK;
221 public int getExactWidth() {
225 new Column("Location") {
227 public Object getValueAt(int row) {
228 TraceException e = buffer.get(row).getTrace();
230 return e.getMessage();
237 public int getDefaultWidth() {
241 new Column("Message") {
243 public Object getValueAt(int row) {
244 return buffer.get(row).getMessage();
248 public int getDefaultWidth() {
255 public int getRowCount() {
256 return buffer.size();
260 table = new JTable(model);
261 table.setSelectionMode(ListSelectionModel.MULTIPLE_INTERVAL_SELECTION);
262 table.setSelectionBackground(Color.LIGHT_GRAY);
263 table.setSelectionForeground(Color.BLACK);
264 model.setColumnWidths(table.getColumnModel());
265 table.setDefaultRenderer(Object.class, new Renderer());
267 table.getSelectionModel().addListSelectionListener(new ListSelectionListener() {
269 public void valueChanged(ListSelectionEvent e) {
270 int row = table.getSelectedRow();
272 row = sorter.convertRowIndexToModel(row);
278 sorter = new TableRowSorter<TableModel>(model);
279 sorter.setComparator(0, NumericComparator.INSTANCE);
280 sorter.setComparator(1, NumericComparator.INSTANCE);
281 sorter.setComparator(4, new LocationComparator());
282 table.setRowSorter(sorter);
283 sorter.setRowFilter(new LogFilter());
286 panel.add(new JScrollPane(table), "span, grow, width " +
287 (Toolkit.getDefaultToolkit().getScreenSize().width * 8 / 10) +
291 panel = new JPanel(new MigLayout("fill"));
294 panel.add(new JLabel("Log line number:"), "split, gapright rel");
295 numberLabel = new SelectableLabel();
296 panel.add(numberLabel, "width 70lp, gapright para");
298 panel.add(new JLabel("Time:"), "split, gapright rel");
299 timeLabel = new SelectableLabel();
300 panel.add(timeLabel, "width 70lp, gapright para");
302 panel.add(new JLabel("Level:"), "split, gapright rel");
303 levelLabel = new SelectableLabel();
304 panel.add(levelLabel, "width 70lp, gapright para");
306 panel.add(new JLabel("Location:"), "split, gapright rel");
307 locationLabel = new SelectableLabel();
308 panel.add(locationLabel, "growx, wrap unrel");
310 panel.add(new JLabel("Log message:"), "split, gapright rel");
311 messageLabel = new SelectableLabel();
312 panel.add(messageLabel, "growx, wrap para");
314 panel.add(new JLabel("Stack trace:"), "wrap rel");
315 stackTraceLabel = new JTextArea(8, 80);
316 stackTraceLabel.setEditable(false);
317 GUIUtil.changeFontSize(stackTraceLabel, -2);
318 panel.add(new JScrollPane(stackTraceLabel), "grow");
321 JButton close = new JButton("Close");
322 close.addActionListener(new ActionListener() {
324 public void actionPerformed(ActionEvent e) {
325 DebugLogDialog.this.dispose();
328 mainPanel.add(close, "newline para, right, tag ok");
331 // Use timer to purge the queue so as not to overwhelm the EDT with events
332 timer = new Timer(POLL_TIME, new ActionListener() {
334 public void actionPerformed(ActionEvent e) {
338 timer.setRepeats(true);
341 this.addWindowListener(new WindowAdapter() {
343 public void windowClosed(WindowEvent e) {
344 log.user("Closing debug log dialog");
346 if (delegator != null) {
347 log.info("Removing log listener");
348 delegator.removeLogger(logListener);
353 GUIUtil.setDisposableDialogOptions(this, close);
354 followBox.requestFocus();
359 private void updateSelected(int row) {
362 numberLabel.setText("");
363 timeLabel.setText("");
364 levelLabel.setText("");
365 locationLabel.setText("");
366 messageLabel.setText("");
367 stackTraceLabel.setText("");
371 LogLine line = buffer.get(row);
372 numberLabel.setText("" + line.getLogCount());
373 timeLabel.setText(String.format("%.3f s", line.getTimestamp() / 1000.0));
374 levelLabel.setText(line.getLevel().toString());
375 TraceException e = line.getTrace();
377 locationLabel.setText(e.getMessage());
379 locationLabel.setText("-");
381 messageLabel.setText(line.getMessage());
382 Throwable t = line.getCause();
384 StackTraceWriter stw = new StackTraceWriter();
385 PrintWriter pw = new PrintWriter(stw);
386 t.printStackTrace(pw);
388 stackTraceLabel.setText(stw.toString());
389 stackTraceLabel.setCaretPosition(0);
391 stackTraceLabel.setText("");
399 * Check whether a row signifies a number of missing rows. This check is "heuristic"
400 * and checks whether the timestamp is zero and the message starts with "---".
402 private boolean isExcludedRow(int row) {
403 LogLine line = buffer.get(row);
404 return (line.getTimestamp() == 0) && (line.getMessage().startsWith("---"));
409 * Purge the queue of incoming log lines. This is called periodically from the EDT, and
410 * it adds any lines in the queue to the buffer, and fires a table event.
412 private void purgeQueue() {
413 int start = buffer.size();
416 while ((line = queue.poll()) != null) {
420 int end = buffer.size() - 1;
422 model.fireTableRowsInserted(start, end);
423 if (followBox.isSelected()) {
424 SwingUtilities.invokeLater(new Runnable() {
427 Rectangle rect = table.getCellRect(1000000000, 1, true);
428 table.scrollRectToVisible(rect);
437 * A logger that adds log lines to the queue. This method may be called from any
438 * thread, and therefore must be thread-safe.
440 private class LogListener extends LogHelper {
442 public void log(LogLine line) {
447 private class LogFilter extends RowFilter<TableModel, Integer> {
450 public boolean include(RowFilter.Entry<? extends TableModel, ? extends Integer> entry) {
451 int index = entry.getIdentifier();
452 LogLine line = buffer.get(index);
453 return filterButtons.get(line.getLevel()).isSelected();
459 private class Renderer extends JLabel implements TableCellRenderer {
461 public Component getTableCellRendererComponent(JTable table1, Object value, boolean isSelected, boolean hasFocus,
462 int row, int column) {
465 row = sorter.convertRowIndexToModel(row);
467 if (STACK_TRACE_MARK.equals(value)) {
470 fg = table1.getForeground();
472 bg = backgroundColors.get(buffer.get(row).getLevel());
476 } else if (isExcludedRow(row)) {
480 this.setForeground(fg);
481 this.setBackground(bg);
483 this.setOpaque(true);
484 this.setText(value.toString());
491 private class LocationComparator implements Comparator<Object> {
492 private final Pattern splitPattern = Pattern.compile("^\\(([^:]*+):([0-9]++).*\\)$");
495 public int compare(Object o1, Object o2) {
496 String s1 = o1.toString();
497 String s2 = o2.toString();
499 Matcher m1 = splitPattern.matcher(s1);
500 Matcher m2 = splitPattern.matcher(s2);
502 if (m1.matches() && m2.matches()) {
503 String class1 = m1.group(1);
504 String pos1 = m1.group(2);
505 String class2 = m2.group(1);
506 String pos2 = m2.group(2);
508 if (class1.equals(class2)) {
509 return NumericComparator.INSTANCE.compare(pos1, pos2);
511 return class1.compareTo(class2);
515 return s1.compareTo(s2);