Source code: org/geotools/gui/swing/LoggingPanel.java
1 /*
2 * -*- mode: java; c-basic-indent: 4; indent-tabs-mode: nil -*-
3 * :indentSize=4:noTabs=true:tabSize=4:indentOnTab=true:indentOnEnter=true:mode=java:
4 * ex: set tabstop=4 expandtab:
5 *
6 * MrPostman - webmail <-> email gateway
7 * Copyright (C) 2002-2003 MrPostman Development Group
8 * Projectpage: http://mrbook.org/mrpostman/
9 *
10 *
11 * This program is free software; you can redistribute it and/or modify
12 * it under the terms of the GNU General Public License as published by
13 * the Free Software Foundation; either version 2 of the License, or
14 * (at your option) any later version.
15 *
16 * This program is distributed in the hope that it will be useful,
17 * but WITHOUT ANY WARRANTY; without even the implied warranty of
18 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
19 * GNU General Public License for more details.
20 * In particular, this implies that users are responsible for
21 * using MrPostman after reading the terms and conditions given
22 * by their web-mail provider.
23 *
24 * You should have received a copy of the GNU General Public License
25 * Named LICENSE in the base directory of this distribution,
26 * if not, write to the Free Software
27 * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
28 */
29
30 /*
31 * Geotools - OpenSource mapping toolkit
32 * (C) 2002, Centre for Computational Geography
33 * (C) 2002, Institut de Recherche pour le Développement
34 *
35 * This library is free software; you can redistribute it and/or
36 * modify it under the terms of the GNU Lesser General Public
37 * License as published by the Free Software Foundation; either
38 * version 2.1 of the License, or (at your option) any later version.
39 *
40 * This library is distributed in the hope that it will be useful,
41 * but WITHOUT ANY WARRANTY; without even the implied warranty of
42 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
43 * Lesser General Public License for more details.
44 *
45 * You should have received a copy of the GNU Lesser General Public
46 * License along with this library; if not, write to the Free Software
47 * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
48 *
49 *
50 * Contacts:
51 * UNITED KINGDOM: James Macgill
52 * mailto:j.macgill@geog.leeds.ac.uk
53 *
54 * FRANCE: Surveillance de l'Environnement Assistée par Satellite
55 * Institut de Recherche pour le Développement / US-Espace
56 * mailto:seasnet@teledetection.fr
57 *
58 * CANADA: Observatoire du Saint-Laurent
59 * Institut Maurice-Lamontagne
60 * mailto:osl@osl.gc.ca
61 */
62 package org.geotools.gui.swing;
63
64 import org.geotools.resources.SwingUtilities;
65 // Resources
66 import org.geotools.resources.XArray;
67 import org.geotools.resources.gui.ResourceKeys;
68 import org.geotools.resources.gui.Resources;
69
70 import java.awt.BorderLayout;
71 // AWT
72 import java.awt.Color;
73 import java.awt.Component;
74 import java.awt.event.WindowAdapter;
75 import java.awt.event.WindowEvent;
76
77 import java.util.ArrayList;
78 import java.util.Arrays;
79 // Collections
80 import java.util.List;
81 import java.util.logging.Handler;
82 // Logging
83 import java.util.logging.Level;
84 import java.util.logging.LogRecord;
85 import java.util.logging.Logger;
86
87 import javax.swing.JPanel;
88 import javax.swing.JScrollPane;
89 // Swing dependencies
90 import javax.swing.JTable;
91 import javax.swing.event.ChangeEvent;
92 import javax.swing.event.ListSelectionEvent;
93 import javax.swing.event.TableColumnModelEvent;
94 import javax.swing.event.TableColumnModelListener;
95 import javax.swing.table.DefaultTableCellRenderer;
96 import javax.swing.table.TableColumn;
97 import javax.swing.table.TableColumnModel;
98 import javax.swing.table.TableModel;
99
100
101 /**
102 * A panel displaying logging messages. The windows displaying Geotools's logging messages
103 * can be constructed with the following code:
104 *
105 * <blockquote><pre>
106 * new LoggingPanel("org.geotools").{@link #show(Component) show}(null);
107 * </pre></blockquote>
108 *
109 * This panel is initially set to listen to messages of level {@link Level#CONFIG} or higher.
110 * This level can be changed with <code>{@link #getHandler}.setLevel(aLevel)</code>.
111 *
112 * @version $Id: LoggingPanel.java,v 1.5 2003/02/09 23:38:11 lbruand Exp $
113 * @author Martin Desruisseaux
114 */
115 public class LoggingPanel extends JPanel {
116 public static final String CVSID = "$Id: LoggingPanel.java,v 1.5 2003/02/09 23:38:11 lbruand Exp $";
117
118 /**
119 * The model for this component.
120 */
121 private final LoggingTableModel model = new LoggingTableModel();
122
123 /**
124 * The table for displaying logging messages.
125 */
126 private final JTable table = new JTable(model);
127
128 /**
129 * The levels for colors enumerated in <code>levelColors</code>. This array
130 * <strong>must</strong> be in increasing order. Logging messages of level
131 * <code>levelValues[i]</code> or higher will be displayed with foreground
132 * color <code>levelColors[i*2]</code> and background color <code>levelColors[i*2+1]</code>.
133 *
134 * @see Level#intValue
135 * @see #getForeground(LogRecord)
136 * @see #getBackground(LogRecord)
137 */
138 private int[] levelValues = new int[0];
139
140 /**
141 * Pairs of foreground and background colors to use for displaying logging messages.
142 * Logging messages of level <code>levelValues[i]</code> or higher will be displayed
143 * with foreground color <code>levelColors[i*2]</code> and background color
144 * <code>levelColors[i*2+1]</code>.
145 *
146 * @see #getForeground(LogRecord)
147 * @see #getBackground(LogRecord)
148 */
149 private final List levelColors = new ArrayList();
150
151 /**
152 * The logger specified at construction time, or <code>null</code> if none.
153 */
154 private Logger logger;
155
156 /**
157 * Constructs a new logging panel. This panel is not registered to any logger.
158 * Registration can be done with the following code:
159 *
160 * <blockquote><pre>
161 * logger.{@link Logger#addHandler addHandler}({@link #getHandler});
162 * </pre></blockquote>
163 */
164 public LoggingPanel() {
165 super(new BorderLayout());
166 table.setShowGrid(false);
167 table.setCellSelectionEnabled(false);
168 table.setGridColor(Color.LIGHT_GRAY);
169 table.setAutoResizeMode(JTable.AUTO_RESIZE_OFF);
170 table.setDefaultRenderer(Object.class, new CellRenderer());
171
172 if (true) {
173 int width = 300;
174 final TableColumnModel columns = table.getColumnModel();
175
176 for (int i = model.getColumnCount(); --i >= 0;) {
177 columns.getColumn(i).setPreferredWidth(width);
178 width = 80;
179 }
180 }
181
182 final JScrollPane scroll = new JScrollPane(table);
183 new AutoScroll(scroll.getVerticalScrollBar().getModel());
184 add(scroll, BorderLayout.CENTER);
185
186 setLevelColor(Level.CONFIG, new Color(0, 128, 0), null);
187 setLevelColor(Level.WARNING, Color.RED, null);
188 setLevelColor(Level.SEVERE, Color.WHITE, Color.RED);
189 }
190
191 /**
192 * Constructs a new logging panel and register it to the specified logger.
193 *
194 * @param logger The logger to listen to, or <code>null</code> for the root logger.
195 */
196 public LoggingPanel(Logger logger) {
197 this();
198
199 if (logger == null) {
200 logger = Logger.getLogger("");
201 }
202 logger.addHandler(getHandler());
203 this.logger = logger;
204 }
205
206 /**
207 * Construct a logging panel and register it to the specified logger.
208 *
209 * @param logger The logger name to listen to, or <code>null</code> for the root logger.
210 */
211 public LoggingPanel(final String logger) {
212 this(Logger.getLogger((logger != null) ? logger : ""));
213 }
214
215 /**
216 * Returns the logging handler.
217 */
218 public Handler getHandler() {
219 return model;
220 }
221
222 /**
223 * Returns the capacity. This is the maximum number of {@link LogRecord}s the handler
224 * can memorize. If more messages are logged, then the earliest messages will be discarted.
225 */
226 public int getCapacity() {
227 return model.getCapacity();
228 }
229
230 /**
231 * Set the capacity. This is the maximum number of {@link LogRecord}s the handler can
232 * memorize. If more messages are logged, then the earliest messages will be discarted.
233 */
234 public void setCapacity(final int capacity) {
235 model.setCapacity(capacity);
236 }
237
238 /**
239 * Returns the foreground color for the specified log record. This method is invoked at
240 * rendering time for every cell in the table's "message" column. The default implementation
241 * returns a color based on the record's level, using colors set with {@link #setLevelColor}.
242 *
243 * @param record The record to get the foreground color.
244 * @return The foreground color for the specified record,
245 * or <code>null</code> for the default color.
246 */
247 public Color getForeground(final LogRecord record) {
248 return getColor(record, 0);
249 }
250
251 /**
252 * Returns the background color for the specified log record. This method is invoked at
253 * rendering time for every cell in the table's "message" column. The default implementation
254 * returns a color based on the record's level, using colors set with {@link #setLevelColor}.
255 *
256 * @param record The record to get the background color.
257 * @return The background color for the specified record,
258 * or <code>null</code> for the default color.
259 */
260 public Color getBackground(final LogRecord record) {
261 return getColor(record, 1);
262 }
263
264 /**
265 * Returns the foreground or background color for the specified record.
266 *
267 * @param record The record to get the color.
268 * @param offset 0 for the foreground color, or 1 for the background color.
269 * @return The color for the specified record, or <code>null</code> for the default color.
270 */
271 private Color getColor(final LogRecord record, final int offset) {
272 int i = Arrays.binarySearch(levelValues, record.getLevel().intValue());
273
274 if (i < 0) {
275 i = ~i - 1; // "~" is the tild symbol, not minus.
276
277 if (i < 0) {
278 return null;
279 }
280 }
281 return (Color) levelColors.get((i * 2) + offset);
282 }
283
284 /**
285 * Set the foreground and background colors for messages of the specified level.
286 * The specified colors will apply on any messages of level <code>level</code> or
287 * greater, up to the next level set with an other call to <code>setLevelColor(...)</code>.
288 *
289 * @param level The minimal level to set color for.
290 * @param foreground The foreground color, or <code>null</code> for the default color.
291 * @param background The background color, or <code>null</code> for the default color.
292 */
293 public void setLevelColor(final Level level, final Color foreground, final Color background) {
294 final int value = level.intValue();
295 int i = Arrays.binarySearch(levelValues, value);
296
297 if (i >= 0) {
298 i *= 2;
299 levelColors.set(i + 0, foreground);
300 levelColors.set(i + 1, background);
301 } else {
302 i = ~i;
303 levelValues = XArray.insert(levelValues, i, 1);
304 levelValues[i] = value;
305 i *= 2;
306 levelColors.add(i + 0, foreground);
307 levelColors.add(i + 1, background);
308 }
309 /*assert XArray.isSorted(levelValues);
310 assert levelValues.length*2 == levelColors.size();*/
311 }
312
313 /**
314 * Layout this component. This method give all the remaining space, if any,
315 * to the last table's column. This column is usually the one with logging
316 * messages.
317 */
318 public void doLayout() {
319 final TableColumnModel model = table.getColumnModel();
320 final int messageColumn = model.getColumnCount() - 1;
321 Component parent = table.getParent();
322 int delta = parent.getWidth();
323
324 if ((parent = parent.getParent()) instanceof JScrollPane) {
325 delta -= ((JScrollPane) parent).getVerticalScrollBar().getPreferredSize().width;
326 }
327
328 for (int i = 0; i < messageColumn; i++) {
329 delta -= model.getColumn(i).getWidth();
330 }
331 final TableColumn column = model.getColumn(messageColumn);
332
333 if (delta > Math.max(column.getWidth(), column.getPreferredWidth())) {
334 column.setPreferredWidth(delta);
335 }
336 super.doLayout();
337 }
338
339 /**
340 * Convenience method showing this logging panel into a frame.
341 * Different kinds of frame can be constructed according <code>owner</code> class:
342 *
343 * <ul>
344 * <li>If <code>owner</code> or one of its parent is a {@link JDesktopPane},
345 * then <code>panel</code> is added into a {@link JInternalFrame}.</li>
346 * <li>If <code>owner</code> or one of its parent is a {@link Frame} or a {@link Dialog},
347 * then <code>panel</code> is added into a {@link JDialog}.</li>
348 * <li>Otherwise, <code>panel</code> is added into a {@link JFrame}.</li>
349 * </ul>
350 *
351 * @param owner The owner, or <code>null</code> to show
352 * this logging panel in a top-level window.
353 * @return The frame. May be a {@link JInternalFrame},
354 * a {@link JDialog} or a {@link JFrame}.
355 */
356 public Component show(final Component owner) {
357 final Component frame = SwingUtilities.toFrame(owner, this, Resources.format(ResourceKeys.EVENT_LOGGER),
358 new WindowAdapter() {
359 public void windowClosed(WindowEvent event) {
360 dispose();
361 }
362 });
363 frame.setSize(750, 300);
364 frame.setVisible(true);
365 doLayout();
366 return frame;
367 }
368
369 /**
370 * Free any resources used by this <code>LoggingPanel</code>. If a {@link Logger} was
371 * specified at construction time, then this method unregister the <code>LoggingPanel</code>'s
372 * handler from the specified logger. Next, {@link Handler#close} is invoked.
373 * <br><br>
374 * This method is invoked automatically when the user close the windows created
375 * with {@link #show(Component)}. If this <code>LoggingPanel</code> is displayed
376 * by some other ways (for example if it has been added into a {@link JPanel}),
377 * then this <code>dispose()</code> should be invoked explicitely when the container
378 * is being discarted.
379 */
380 public void dispose() {
381 final Handler handler = getHandler();
382
383 while (logger != null) {
384 logger.removeHandler(handler);
385 logger = logger.getParent();
386 }
387 handler.close();
388 }
389
390 /**
391 * Display cell contents. This class is used for changing
392 * the cell's color according the log record level.
393 */
394 private final class CellRenderer extends DefaultTableCellRenderer implements TableColumnModelListener {
395 /**
396 * Default color for the foreground.
397 */
398 private Color foreground;
399
400 /**
401 * Default color for the background.
402 */
403 private Color background;
404
405 /**
406 * The index of messages column.
407 */
408 private int messageColumn;
409
410 /**
411 * The last row for which the side has been computed.
412 */
413 private int lastRow;
414
415 /**
416 * Construct a new cell renderer.
417 */
418 public CellRenderer() {
419 foreground = super.getForeground();
420 background = super.getBackground();
421 table.getColumnModel().addColumnModelListener(this);
422 }
423
424 /**
425 * Set the foreground color.
426 */
427 public void setForeground(final Color foreground) {
428 super.setForeground(this.foreground = foreground);
429 }
430
431 /**
432 * Set the background colior
433 */
434 public void setBackground(final Color background) {
435 super.setBackground(this.background = background);
436 }
437
438 /**
439 * Returns the component to use for painting the cell.
440 */
441 public Component getTableCellRendererComponent(final JTable table, final Object value,
442 final boolean isSelected, final boolean hasFocus, final int rowIndex, final int columnIndex) {
443 Color foreground = this.foreground;
444 Color background = this.background;
445 final boolean isMessage = (columnIndex == messageColumn);
446
447 if (!isMessage) {
448 foreground = Color.GRAY;
449 } else if (rowIndex >= 0) {
450 final TableModel candidate = table.getModel();
451
452 if (candidate instanceof LoggingTableModel) {
453 final LoggingTableModel model = (LoggingTableModel) candidate;
454 final LogRecord record = model.getLogRecord(rowIndex);
455 Color color;
456 color = LoggingPanel.this.getForeground(record);
457
458 if (color != null) {
459 foreground = color;
460 }
461 color = LoggingPanel.this.getBackground(record);
462
463 if (color != null) {
464 background = color;
465 }
466 }
467 }
468 super.setBackground(background);
469 super.setForeground(foreground);
470 final Component component = super.getTableCellRendererComponent(table, value, isSelected, hasFocus,
471 rowIndex, columnIndex);
472
473 /*
474 * If a new record is being painted and this new record is wider
475 * than previous ones, then make the message column width larger.
476 */
477 if (isMessage) {
478 if (rowIndex > lastRow) {
479 final int width = component.getPreferredSize().width + 15;
480 final TableColumn column = table.getColumnModel().getColumn(columnIndex);
481
482 if (width > column.getPreferredWidth()) {
483 column.setPreferredWidth(width);
484 }
485
486 if (rowIndex == (lastRow + 1)) {
487 lastRow = rowIndex;
488 }
489 }
490 }
491 return component;
492 }
493
494 /**
495 * Invoked when the message column may have moved. This method update the
496 * {@link #messageColumn} field, so that the message column will continue
497 * to be paint with special colors.
498 */
499 private final void update() {
500 messageColumn = table.convertColumnIndexToView(model.getColumnCount() - 1);
501 }
502
503 public void columnAdded(TableColumnModelEvent e) {
504 update();
505 }
506
507 public void columnMarginChanged(ChangeEvent e) {
508 update();
509 }
510
511 public void columnMoved(TableColumnModelEvent e) {
512 update();
513 }
514
515 public void columnRemoved(TableColumnModelEvent e) {
516 update();
517 }
518
519 public void columnSelectionChanged(ListSelectionEvent e) {
520 update();
521 }
522 }
523 }