]> git.gag.com Git - debian/openrocket/blob - src/net/sf/openrocket/gui/dialogs/DebugLogDialog.java
927cc54838f867b9235cb40514881ec8da8333eb
[debian/openrocket] / src / net / sf / openrocket / gui / dialogs / DebugLogDialog.java
1 package net.sf.openrocket.gui.dialogs;
2
3 import java.awt.Color;
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;
21
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;
40
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;
55
56 public class DebugLogDialog extends JDialog {
57         private static final LogHelper log = Application.getLogger();
58         
59         private static final int POLL_TIME = 250;
60         private static final String STACK_TRACE_MARK = "\uFF01";
61         
62         private static final EnumMap<LogLevel, Color> backgroundColors = new EnumMap<LogLevel, Color>(LogLevel.class);
63         static {
64                 for (LogLevel l : LogLevel.values()) {
65                         // Just to ensure every level has a bg color
66                         backgroundColors.put(l, Color.ORANGE);
67                 }
68                 final int hi = 255;
69                 final int lo = 150;
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));
76         }
77         
78         /** Buffer containing the log lines displayed */
79         private final List<LogLine> buffer = new ArrayList<LogLine>();
80         
81         /** Queue of log lines to be added to the displayed buffer */
82         private final Queue<LogLine> queue = new ConcurrentLinkedQueue<LogLine>();
83         
84         private final DelegatorLogger delegator;
85         private final LogListener logListener;
86         
87         private final EnumMap<LogLevel, JCheckBox> filterButtons = new EnumMap<LogLevel, JCheckBox>(LogLevel.class);
88         private final JCheckBox followBox;
89         private final Timer timer;
90         
91
92         private final JTable table;
93         private final ColumnTableModel model;
94         private final TableRowSorter<TableModel> sorter;
95         
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;
102         
103         public DebugLogDialog(Window parent) {
104                 super(parent, "OpenRocket debug log");
105                 
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);
113                 } else {
114                         log.warn("Application log is not a DelegatorLogger");
115                         delegator = null;
116                         logListener = null;
117                 }
118                 
119                 // Fetch old log lines
120                 LogLevelBufferLogger bufferLogger = Application.getLogBuffer();
121                 if (bufferLogger != null) {
122                         buffer.addAll(bufferLogger.getLogs());
123                 } else {
124                         log.warn("Application does not have a log buffer");
125                 }
126                 
127
128                 // Create the UI
129                 JPanel mainPanel = new JPanel(new MigLayout("fill"));
130                 this.add(mainPanel);
131                 
132
133                 JSplitPane split = new JSplitPane(JSplitPane.VERTICAL_SPLIT);
134                 split.setDividerLocation(0.7);
135                 mainPanel.add(split, "grow");
136                 
137                 // Top panel
138                 JPanel panel = new JPanel(new MigLayout("fill"));
139                 split.add(panel);
140                 
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() {
147                                 @Override
148                                 public void actionPerformed(ActionEvent e) {
149                                         sorter.setRowFilter(new LogFilter());
150                                 }
151                         });
152                         panel.add(box, "gapright unrel");
153                         filterButtons.put(l, box);
154                 }
155                 
156                 followBox = new JCheckBox("Follow");
157                 followBox.setSelected(true);
158                 panel.add(followBox, "skip, gapright para, right");
159                 
160                 JButton clear = new JButton("Clear");
161                 clear.addActionListener(new ActionListener() {
162                         @Override
163                         public void actionPerformed(ActionEvent e) {
164                                 log.user("Clearing log buffer");
165                                 buffer.clear();
166                                 queue.clear();
167                                 model.fireTableDataChanged();
168                         }
169                 });
170                 panel.add(clear, "right, wrap");
171                 
172
173
174                 // Create the table model
175                 model = new ColumnTableModel(
176
177                 new Column("#") {
178                         @Override
179                         public Object getValueAt(int row) {
180                                 return buffer.get(row).getLogCount();
181                         }
182                         
183                         @Override
184                         public int getDefaultWidth() {
185                                 return 60;
186                         }
187                 },
188                                 new Column("Time") {
189                                         @Override
190                                         public Object getValueAt(int row) {
191                                                 return String.format("%.3f", buffer.get(row).getTimestamp() / 1000.0);
192                                         }
193                                         
194                                         @Override
195                                         public int getDefaultWidth() {
196                                                 return 60;
197                                         }
198                                 },
199                                 new Column("Level") {
200                                         @Override
201                                         public Object getValueAt(int row) {
202                                                 return buffer.get(row).getLevel();
203                                         }
204                                         
205                                         @Override
206                                         public int getDefaultWidth() {
207                                                 return 60;
208                                         }
209                                 },
210                                 new Column("") {
211                                         @Override
212                                         public Object getValueAt(int row) {
213                                                 if (buffer.get(row).getCause() != null) {
214                                                         return STACK_TRACE_MARK;
215                                                 } else {
216                                                         return "";
217                                                 }
218                                         }
219                                         
220                                         @Override
221                                         public int getExactWidth() {
222                                                 return 16;
223                                         }
224                                 },
225                                 new Column("Location") {
226                                         @Override
227                                         public Object getValueAt(int row) {
228                                                 TraceException e = buffer.get(row).getTrace();
229                                                 if (e != null) {
230                                                         return e.getMessage();
231                                                 } else {
232                                                         return "";
233                                                 }
234                                         }
235                                         
236                                         @Override
237                                         public int getDefaultWidth() {
238                                                 return 200;
239                                         }
240                                 },
241                                 new Column("Message") {
242                                         @Override
243                                         public Object getValueAt(int row) {
244                                                 return buffer.get(row).getMessage();
245                                         }
246                                         
247                                         @Override
248                                         public int getDefaultWidth() {
249                                                 return 580;
250                                         }
251                                 }
252
253                 ) {
254                         @Override
255                         public int getRowCount() {
256                                 return buffer.size();
257                         }
258                 };
259                 
260                 table = new JTable(model);
261                 table.setSelectionMode(ListSelectionModel.SINGLE_SELECTION);
262                 table.setSelectionBackground(Color.LIGHT_GRAY);
263                 table.setSelectionForeground(Color.BLACK);
264                 model.setColumnWidths(table.getColumnModel());
265                 table.setDefaultRenderer(Object.class, new Renderer());
266                 
267                 table.getSelectionModel().addListSelectionListener(new ListSelectionListener() {
268                         @Override
269                         public void valueChanged(ListSelectionEvent e) {
270                                 int row = table.getSelectedRow();
271                                 if (row >= 0) {
272                                         row = sorter.convertRowIndexToModel(row);
273                                 }
274                                 updateSelected(row);
275                         }
276                 });
277                 
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());
284                 
285
286                 panel.add(new JScrollPane(table), "span, grow, width " +
287                                 (Toolkit.getDefaultToolkit().getScreenSize().width * 8 / 10) +
288                                 "px, height 400px");
289                 
290
291                 panel = new JPanel(new MigLayout("fill"));
292                 split.add(panel);
293                 
294                 panel.add(new JLabel("Log line number:"), "split, gapright rel");
295                 numberLabel = new SelectableLabel();
296                 panel.add(numberLabel, "width 70lp, gapright para");
297                 
298                 panel.add(new JLabel("Time:"), "split, gapright rel");
299                 timeLabel = new SelectableLabel();
300                 panel.add(timeLabel, "width 70lp, gapright para");
301                 
302                 panel.add(new JLabel("Level:"), "split, gapright rel");
303                 levelLabel = new SelectableLabel();
304                 panel.add(levelLabel, "width 70lp, gapright para");
305                 
306                 panel.add(new JLabel("Location:"), "split, gapright rel");
307                 locationLabel = new SelectableLabel();
308                 panel.add(locationLabel, "growx, wrap unrel");
309                 
310                 panel.add(new JLabel("Log message:"), "split, gapright rel");
311                 messageLabel = new SelectableLabel();
312                 panel.add(messageLabel, "growx, wrap para");
313                 
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");
319                 
320
321                 JButton close = new JButton("Close");
322                 close.addActionListener(new ActionListener() {
323                         @Override
324                         public void actionPerformed(ActionEvent e) {
325                                 DebugLogDialog.this.dispose();
326                         }
327                 });
328                 mainPanel.add(close, "newline para, right, tag ok");
329                 
330
331                 // Use timer to purge the queue so as not to overwhelm the EDT with events
332                 timer = new Timer(POLL_TIME, new ActionListener() {
333                         @Override
334                         public void actionPerformed(ActionEvent e) {
335                                 purgeQueue();
336                         }
337                 });
338                 timer.setRepeats(true);
339                 timer.start();
340                 
341                 this.addWindowListener(new WindowAdapter() {
342                         @Override
343                         public void windowClosed(WindowEvent e) {
344                                 log.user("Closing debug log dialog");
345                                 timer.stop();
346                                 if (delegator != null) {
347                                         log.info("Removing log listener");
348                                         delegator.removeLogger(logListener);
349                                 }
350                         }
351                 });
352                 
353                 GUIUtil.setDisposableDialogOptions(this, close);
354                 followBox.requestFocus();
355         }
356         
357         
358
359         private void updateSelected(int row) {
360                 if (row < 0) {
361                         
362                         numberLabel.setText("");
363                         timeLabel.setText("");
364                         levelLabel.setText("");
365                         locationLabel.setText("");
366                         messageLabel.setText("");
367                         stackTraceLabel.setText("");
368                         
369                 } else {
370                         
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();
376                         if (e != null) {
377                                 locationLabel.setText(e.getMessage());
378                         } else {
379                                 locationLabel.setText("-");
380                         }
381                         messageLabel.setText(line.getMessage());
382                         Throwable t = line.getCause();
383                         if (t != null) {
384                                 StackTraceWriter stw = new StackTraceWriter();
385                                 PrintWriter pw = new PrintWriter(stw);
386                                 t.printStackTrace(pw);
387                                 pw.flush();
388                                 stackTraceLabel.setText(stw.toString());
389                                 stackTraceLabel.setCaretPosition(0);
390                         } else {
391                                 stackTraceLabel.setText("");
392                         }
393                         
394                 }
395         }
396         
397         
398         /**
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 "---".
401          */
402         private boolean isExcludedRow(int row) {
403                 LogLine line = buffer.get(row);
404                 return (line.getTimestamp() == 0) && (line.getMessage().startsWith("---"));
405         }
406         
407         
408         /**
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.
411          */
412         private void purgeQueue() {
413                 int start = buffer.size();
414                 
415                 LogLine line;
416                 while ((line = queue.poll()) != null) {
417                         buffer.add(line);
418                 }
419                 
420                 int end = buffer.size() - 1;
421                 if (end >= start) {
422                         model.fireTableRowsInserted(start, end);
423                         if (followBox.isSelected()) {
424                                 SwingUtilities.invokeLater(new Runnable() {
425                                         @Override
426                                         public void run() {
427                                                 Rectangle rect = table.getCellRect(1000000000, 1, true);
428                                                 table.scrollRectToVisible(rect);
429                                         }
430                                 });
431                         }
432                 }
433         }
434         
435         
436         /**
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.
439          */
440         private class LogListener extends LogHelper {
441                 @Override
442                 public void log(LogLine line) {
443                         queue.add(line);
444                 }
445         }
446         
447         private class LogFilter extends RowFilter<TableModel, Integer> {
448                 
449                 @Override
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();
454                 }
455                 
456         }
457         
458         
459         private class Renderer extends JLabel implements TableCellRenderer {
460                 @Override
461                 public Component getTableCellRendererComponent(JTable table1, Object value, boolean isSelected, boolean hasFocus,
462                                 int row, int column) {
463                         Color fg, bg;
464                         
465                         row = sorter.convertRowIndexToModel(row);
466                         
467                         if (STACK_TRACE_MARK.equals(value)) {
468                                 fg = Color.RED;
469                         } else {
470                                 fg = table1.getForeground();
471                         }
472                         bg = backgroundColors.get(buffer.get(row).getLevel());
473                         
474                         if (isSelected) {
475                                 bg = bg.darker();
476                         } else if (isExcludedRow(row)) {
477                                 bg = bg.brighter();
478                         }
479                         
480                         this.setForeground(fg);
481                         this.setBackground(bg);
482                         
483                         this.setOpaque(true);
484                         this.setText(value.toString());
485                         
486                         return this;
487                 }
488         }
489         
490         
491         private class LocationComparator implements Comparator<Object> {
492                 private final Pattern splitPattern = Pattern.compile("^\\(([^:]*+):([0-9]++).*\\)$");
493                 
494                 @Override
495                 public int compare(Object o1, Object o2) {
496                         String s1 = o1.toString();
497                         String s2 = o2.toString();
498                         
499                         Matcher m1 = splitPattern.matcher(s1);
500                         Matcher m2 = splitPattern.matcher(s2);
501                         
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);
507                                 
508                                 if (class1.equals(class2)) {
509                                         return NumericComparator.INSTANCE.compare(pos1, pos2);
510                                 } else {
511                                         return class1.compareTo(class2);
512                                 }
513                         }
514                         
515                         return s1.compareTo(s2);
516                 }
517                 
518         }
519         
520 }