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 box.setSelected(true);
145 box.addActionListener(new ActionListener() {
147 public void actionPerformed(ActionEvent e) {
148 sorter.setRowFilter(new LogFilter());
151 panel.add(box, "gapright unrel");
152 filterButtons.put(l, box);
155 followBox = new JCheckBox("Follow");
156 followBox.setSelected(true);
157 panel.add(followBox, "skip, gapright para, right");
159 JButton clear = new JButton("Clear");
160 clear.addActionListener(new ActionListener() {
162 public void actionPerformed(ActionEvent e) {
163 log.user("Clearing log buffer");
166 model.fireTableDataChanged();
169 panel.add(clear, "right, wrap");
173 // Create the table model
174 model = new ColumnTableModel(
178 public Object getValueAt(int row) {
179 return buffer.get(row).getLogCount();
183 public int getDefaultWidth() {
189 public Object getValueAt(int row) {
190 return String.format("%.3f", buffer.get(row).getTimestamp() / 1000.0);
194 public int getDefaultWidth() {
198 new Column("Level") {
200 public Object getValueAt(int row) {
201 return buffer.get(row).getLevel();
205 public int getDefaultWidth() {
211 public Object getValueAt(int row) {
212 if (buffer.get(row).getCause() != null) {
213 return STACK_TRACE_MARK;
220 public int getExactWidth() {
224 new Column("Location") {
226 public Object getValueAt(int row) {
227 TraceException e = buffer.get(row).getTrace();
229 return e.getMessage();
236 public int getDefaultWidth() {
240 new Column("Message") {
242 public Object getValueAt(int row) {
243 return buffer.get(row).getMessage();
247 public int getDefaultWidth() {
254 public int getRowCount() {
255 return buffer.size();
259 table = new JTable(model);
260 table.setSelectionMode(ListSelectionModel.SINGLE_SELECTION);
261 table.setSelectionBackground(Color.LIGHT_GRAY);
262 table.setSelectionForeground(Color.BLACK);
263 model.setColumnWidths(table.getColumnModel());
264 table.setDefaultRenderer(Object.class, new Renderer());
266 table.getSelectionModel().addListSelectionListener(new ListSelectionListener() {
268 public void valueChanged(ListSelectionEvent e) {
269 int row = table.getSelectedRow();
271 row = sorter.convertRowIndexToModel(row);
277 sorter = new TableRowSorter<TableModel>(model);
278 sorter.setComparator(0, NumericComparator.INSTANCE);
279 sorter.setComparator(1, NumericComparator.INSTANCE);
280 sorter.setComparator(4, new LocationComparator());
281 table.setRowSorter(sorter);
284 panel.add(new JScrollPane(table), "span, grow, width " +
285 (Toolkit.getDefaultToolkit().getScreenSize().width * 8 / 10) +
289 panel = new JPanel(new MigLayout("fill"));
292 panel.add(new JLabel("Log line number:"), "split, gapright rel");
293 numberLabel = new SelectableLabel();
294 panel.add(numberLabel, "width 70lp, gapright para");
296 panel.add(new JLabel("Time:"), "split, gapright rel");
297 timeLabel = new SelectableLabel();
298 panel.add(timeLabel, "width 70lp, gapright para");
300 panel.add(new JLabel("Level:"), "split, gapright rel");
301 levelLabel = new SelectableLabel();
302 panel.add(levelLabel, "width 70lp, gapright para");
304 panel.add(new JLabel("Location:"), "split, gapright rel");
305 locationLabel = new SelectableLabel();
306 panel.add(locationLabel, "growx, wrap unrel");
308 panel.add(new JLabel("Log message:"), "split, gapright rel");
309 messageLabel = new SelectableLabel();
310 panel.add(messageLabel, "growx, wrap para");
312 panel.add(new JLabel("Stack trace:"), "wrap rel");
313 stackTraceLabel = new JTextArea(8, 80);
314 stackTraceLabel.setEditable(false);
315 GUIUtil.changeFontSize(stackTraceLabel, -2);
316 panel.add(new JScrollPane(stackTraceLabel), "grow");
319 JButton close = new JButton("Close");
320 close.addActionListener(new ActionListener() {
322 public void actionPerformed(ActionEvent e) {
323 DebugLogDialog.this.dispose();
326 mainPanel.add(close, "newline para, right, tag ok");
329 // Use timer to purge the queue so as not to overwhelm the EDT with events
330 timer = new Timer(POLL_TIME, new ActionListener() {
332 public void actionPerformed(ActionEvent e) {
336 timer.setRepeats(true);
339 this.addWindowListener(new WindowAdapter() {
341 public void windowClosed(WindowEvent e) {
342 log.user("Closing debug log dialog");
344 if (delegator != null) {
345 log.info("Removing log listener");
346 delegator.removeLogger(logListener);
351 GUIUtil.setDisposableDialogOptions(this, close);
352 followBox.requestFocus();
357 private void updateSelected(int row) {
360 numberLabel.setText("");
361 timeLabel.setText("");
362 levelLabel.setText("");
363 locationLabel.setText("");
364 messageLabel.setText("");
365 stackTraceLabel.setText("");
369 LogLine line = buffer.get(row);
370 numberLabel.setText("" + line.getLogCount());
371 timeLabel.setText(String.format("%.3f s", line.getTimestamp() / 1000.0));
372 levelLabel.setText(line.getLevel().toString());
373 TraceException e = line.getTrace();
375 locationLabel.setText(e.getMessage());
377 locationLabel.setText("-");
379 messageLabel.setText(line.getMessage());
380 Throwable t = line.getCause();
382 StackTraceWriter stw = new StackTraceWriter();
383 PrintWriter pw = new PrintWriter(stw);
384 t.printStackTrace(pw);
386 stackTraceLabel.setText(stw.toString());
387 stackTraceLabel.setCaretPosition(0);
389 stackTraceLabel.setText("");
397 * Check whether a row signifies a number of missing rows. This check is "heuristic"
398 * and checks whether the timestamp is zero and the message starts with "---".
400 private boolean isExcludedRow(int row) {
401 LogLine line = buffer.get(row);
402 return (line.getTimestamp() == 0) && (line.getMessage().startsWith("---"));
407 * Purge the queue of incoming log lines. This is called periodically from the EDT, and
408 * it adds any lines in the queue to the buffer, and fires a table event.
410 private void purgeQueue() {
411 int start = buffer.size();
414 while ((line = queue.poll()) != null) {
418 int end = buffer.size() - 1;
420 model.fireTableRowsInserted(start, end);
421 if (followBox.isSelected()) {
422 SwingUtilities.invokeLater(new Runnable() {
425 Rectangle rect = table.getCellRect(1000000000, 1, true);
426 table.scrollRectToVisible(rect);
435 * A logger that adds log lines to the queue. This method may be called from any
436 * thread, and therefore must be thread-safe.
438 private class LogListener extends LogHelper {
440 public void log(LogLine line) {
445 private class LogFilter extends RowFilter<TableModel, Integer> {
448 public boolean include(RowFilter.Entry<? extends TableModel, ? extends Integer> entry) {
449 int index = entry.getIdentifier();
450 LogLine line = buffer.get(index);
451 return filterButtons.get(line.getLevel()).isSelected();
457 private class Renderer extends JLabel implements TableCellRenderer {
459 public Component getTableCellRendererComponent(JTable table1, Object value, boolean isSelected, boolean hasFocus,
460 int row, int column) {
463 row = sorter.convertRowIndexToModel(row);
465 if (STACK_TRACE_MARK.equals(value)) {
468 fg = table1.getForeground();
470 bg = backgroundColors.get(buffer.get(row).getLevel());
474 } else if (isExcludedRow(row)) {
478 this.setForeground(fg);
479 this.setBackground(bg);
481 this.setOpaque(true);
482 this.setText(value.toString());
489 private class LocationComparator implements Comparator<Object> {
490 private final Pattern splitPattern = Pattern.compile("^\\(([^:]*+):([0-9]++).*\\)$");
493 public int compare(Object o1, Object o2) {
494 String s1 = o1.toString();
495 String s2 = o2.toString();
497 Matcher m1 = splitPattern.matcher(s1);
498 Matcher m2 = splitPattern.matcher(s2);
500 if (m1.matches() && m2.matches()) {
501 String class1 = m1.group(1);
502 String pos1 = m1.group(2);
503 String class2 = m2.group(1);
504 String pos2 = m2.group(2);
506 if (class1.equals(class2)) {
507 return NumericComparator.INSTANCE.compare(pos1, pos2);
509 return class1.compareTo(class2);
513 return s1.compareTo(s2);