Undo/redo system enhancements, DnD for component tree, bug fixes
[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                         box.setSelected(true);
145                         box.addActionListener(new ActionListener() {
146                                 @Override
147                                 public void actionPerformed(ActionEvent e) {
148                                         sorter.setRowFilter(new LogFilter());
149                                 }
150                         });
151                         panel.add(box, "gapright unrel");
152                         filterButtons.put(l, box);
153                 }
154                 
155                 followBox = new JCheckBox("Follow");
156                 followBox.setSelected(true);
157                 panel.add(followBox, "skip, gapright para, right");
158                 
159                 JButton clear = new JButton("Clear");
160                 clear.addActionListener(new ActionListener() {
161                         @Override
162                         public void actionPerformed(ActionEvent e) {
163                                 log.user("Clearing log buffer");
164                                 buffer.clear();
165                                 queue.clear();
166                                 model.fireTableDataChanged();
167                         }
168                 });
169                 panel.add(clear, "right, wrap");
170                 
171
172
173                 // Create the table model
174                 model = new ColumnTableModel(
175
176                 new Column("#") {
177                         @Override
178                         public Object getValueAt(int row) {
179                                 return buffer.get(row).getLogCount();
180                         }
181                         
182                         @Override
183                         public int getDefaultWidth() {
184                                 return 60;
185                         }
186                 },
187                                 new Column("Time") {
188                                         @Override
189                                         public Object getValueAt(int row) {
190                                                 return String.format("%.3f", buffer.get(row).getTimestamp() / 1000.0);
191                                         }
192                                         
193                                         @Override
194                                         public int getDefaultWidth() {
195                                                 return 60;
196                                         }
197                                 },
198                                 new Column("Level") {
199                                         @Override
200                                         public Object getValueAt(int row) {
201                                                 return buffer.get(row).getLevel();
202                                         }
203                                         
204                                         @Override
205                                         public int getDefaultWidth() {
206                                                 return 60;
207                                         }
208                                 },
209                                 new Column("") {
210                                         @Override
211                                         public Object getValueAt(int row) {
212                                                 if (buffer.get(row).getCause() != null) {
213                                                         return STACK_TRACE_MARK;
214                                                 } else {
215                                                         return "";
216                                                 }
217                                         }
218                                         
219                                         @Override
220                                         public int getExactWidth() {
221                                                 return 16;
222                                         }
223                                 },
224                                 new Column("Location") {
225                                         @Override
226                                         public Object getValueAt(int row) {
227                                                 TraceException e = buffer.get(row).getTrace();
228                                                 if (e != null) {
229                                                         return e.getMessage();
230                                                 } else {
231                                                         return "";
232                                                 }
233                                         }
234                                         
235                                         @Override
236                                         public int getDefaultWidth() {
237                                                 return 200;
238                                         }
239                                 },
240                                 new Column("Message") {
241                                         @Override
242                                         public Object getValueAt(int row) {
243                                                 return buffer.get(row).getMessage();
244                                         }
245                                         
246                                         @Override
247                                         public int getDefaultWidth() {
248                                                 return 580;
249                                         }
250                                 }
251
252                 ) {
253                         @Override
254                         public int getRowCount() {
255                                 return buffer.size();
256                         }
257                 };
258                 
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());
265                 
266                 table.getSelectionModel().addListSelectionListener(new ListSelectionListener() {
267                         @Override
268                         public void valueChanged(ListSelectionEvent e) {
269                                 int row = table.getSelectedRow();
270                                 if (row >= 0) {
271                                         row = sorter.convertRowIndexToModel(row);
272                                 }
273                                 updateSelected(row);
274                         }
275                 });
276                 
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);
282                 
283
284                 panel.add(new JScrollPane(table), "span, grow, width " +
285                                 (Toolkit.getDefaultToolkit().getScreenSize().width * 8 / 10) +
286                                 "px, height 400px");
287                 
288
289                 panel = new JPanel(new MigLayout("fill"));
290                 split.add(panel);
291                 
292                 panel.add(new JLabel("Log line number:"), "split, gapright rel");
293                 numberLabel = new SelectableLabel();
294                 panel.add(numberLabel, "width 70lp, gapright para");
295                 
296                 panel.add(new JLabel("Time:"), "split, gapright rel");
297                 timeLabel = new SelectableLabel();
298                 panel.add(timeLabel, "width 70lp, gapright para");
299                 
300                 panel.add(new JLabel("Level:"), "split, gapright rel");
301                 levelLabel = new SelectableLabel();
302                 panel.add(levelLabel, "width 70lp, gapright para");
303                 
304                 panel.add(new JLabel("Location:"), "split, gapright rel");
305                 locationLabel = new SelectableLabel();
306                 panel.add(locationLabel, "growx, wrap unrel");
307                 
308                 panel.add(new JLabel("Log message:"), "split, gapright rel");
309                 messageLabel = new SelectableLabel();
310                 panel.add(messageLabel, "growx, wrap para");
311                 
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");
317                 
318
319                 JButton close = new JButton("Close");
320                 close.addActionListener(new ActionListener() {
321                         @Override
322                         public void actionPerformed(ActionEvent e) {
323                                 DebugLogDialog.this.dispose();
324                         }
325                 });
326                 mainPanel.add(close, "newline para, right, tag ok");
327                 
328
329                 // Use timer to purge the queue so as not to overwhelm the EDT with events
330                 timer = new Timer(POLL_TIME, new ActionListener() {
331                         @Override
332                         public void actionPerformed(ActionEvent e) {
333                                 purgeQueue();
334                         }
335                 });
336                 timer.setRepeats(true);
337                 timer.start();
338                 
339                 this.addWindowListener(new WindowAdapter() {
340                         @Override
341                         public void windowClosed(WindowEvent e) {
342                                 log.user("Closing debug log dialog");
343                                 timer.stop();
344                                 if (delegator != null) {
345                                         log.info("Removing log listener");
346                                         delegator.removeLogger(logListener);
347                                 }
348                         }
349                 });
350                 
351                 GUIUtil.setDisposableDialogOptions(this, close);
352                 followBox.requestFocus();
353         }
354         
355         
356
357         private void updateSelected(int row) {
358                 if (row < 0) {
359                         
360                         numberLabel.setText("");
361                         timeLabel.setText("");
362                         levelLabel.setText("");
363                         locationLabel.setText("");
364                         messageLabel.setText("");
365                         stackTraceLabel.setText("");
366                         
367                 } else {
368                         
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();
374                         if (e != null) {
375                                 locationLabel.setText(e.getMessage());
376                         } else {
377                                 locationLabel.setText("-");
378                         }
379                         messageLabel.setText(line.getMessage());
380                         Throwable t = line.getCause();
381                         if (t != null) {
382                                 StackTraceWriter stw = new StackTraceWriter();
383                                 PrintWriter pw = new PrintWriter(stw);
384                                 t.printStackTrace(pw);
385                                 pw.flush();
386                                 stackTraceLabel.setText(stw.toString());
387                                 stackTraceLabel.setCaretPosition(0);
388                         } else {
389                                 stackTraceLabel.setText("");
390                         }
391                         
392                 }
393         }
394         
395         
396         /**
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 "---".
399          */
400         private boolean isExcludedRow(int row) {
401                 LogLine line = buffer.get(row);
402                 return (line.getTimestamp() == 0) && (line.getMessage().startsWith("---"));
403         }
404         
405         
406         /**
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.
409          */
410         private void purgeQueue() {
411                 int start = buffer.size();
412                 
413                 LogLine line;
414                 while ((line = queue.poll()) != null) {
415                         buffer.add(line);
416                 }
417                 
418                 int end = buffer.size() - 1;
419                 if (end >= start) {
420                         model.fireTableRowsInserted(start, end);
421                         if (followBox.isSelected()) {
422                                 SwingUtilities.invokeLater(new Runnable() {
423                                         @Override
424                                         public void run() {
425                                                 Rectangle rect = table.getCellRect(1000000000, 1, true);
426                                                 table.scrollRectToVisible(rect);
427                                         }
428                                 });
429                         }
430                 }
431         }
432         
433         
434         /**
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.
437          */
438         private class LogListener extends LogHelper {
439                 @Override
440                 public void log(LogLine line) {
441                         queue.add(line);
442                 }
443         }
444         
445         private class LogFilter extends RowFilter<TableModel, Integer> {
446                 
447                 @Override
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();
452                 }
453                 
454         }
455         
456         
457         private class Renderer extends JLabel implements TableCellRenderer {
458                 @Override
459                 public Component getTableCellRendererComponent(JTable table1, Object value, boolean isSelected, boolean hasFocus,
460                                 int row, int column) {
461                         Color fg, bg;
462                         
463                         row = sorter.convertRowIndexToModel(row);
464                         
465                         if (STACK_TRACE_MARK.equals(value)) {
466                                 fg = Color.RED;
467                         } else {
468                                 fg = table1.getForeground();
469                         }
470                         bg = backgroundColors.get(buffer.get(row).getLevel());
471                         
472                         if (isSelected) {
473                                 bg = bg.darker();
474                         } else if (isExcludedRow(row)) {
475                                 bg = bg.brighter();
476                         }
477                         
478                         this.setForeground(fg);
479                         this.setBackground(bg);
480                         
481                         this.setOpaque(true);
482                         this.setText(value.toString());
483                         
484                         return this;
485                 }
486         }
487         
488         
489         private class LocationComparator implements Comparator<Object> {
490                 private final Pattern splitPattern = Pattern.compile("^\\(([^:]*+):([0-9]++).*\\)$");
491                 
492                 @Override
493                 public int compare(Object o1, Object o2) {
494                         String s1 = o1.toString();
495                         String s2 = o2.toString();
496                         
497                         Matcher m1 = splitPattern.matcher(s1);
498                         Matcher m2 = splitPattern.matcher(s2);
499                         
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);
505                                 
506                                 if (class1.equals(class2)) {
507                                         return NumericComparator.INSTANCE.compare(pos1, pos2);
508                                 } else {
509                                         return class1.compareTo(class2);
510                                 }
511                         }
512                         
513                         return s1.compareTo(s2);
514                 }
515                 
516         }
517         
518 }