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