001: /*
002: * GeoTools - OpenSource mapping toolkit
003: * http://geotools.org
004: * (C) 2003-2006, Geotools Project Managment Committee (PMC)
005: * (C) 2002, Institut de Recherche pour le Développement
006: *
007: * This library is free software; you can redistribute it and/or
008: * modify it under the terms of the GNU Lesser General Public
009: * License as published by the Free Software Foundation; either
010: * version 2.1 of the License, or (at your option) any later version.
011: *
012: * This library is distributed in the hope that it will be useful,
013: * but WITHOUT ANY WARRANTY; without even the implied warranty of
014: * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
015: * Lesser General Public License for more details.
016: */
017: package org.geotools.gui.swing;
018:
019: // Swing dependencies
020: import javax.swing.JTable;
021: import javax.swing.JPanel;
022: import javax.swing.JFrame;
023: import javax.swing.JDialog;
024: import javax.swing.JComponent;
025: import javax.swing.JScrollPane;
026: import javax.swing.JDesktopPane;
027: import javax.swing.JInternalFrame;
028: import javax.swing.table.TableModel;
029: import javax.swing.table.TableColumn;
030: import javax.swing.table.TableColumnModel;
031: import javax.swing.table.DefaultTableCellRenderer;
032: import javax.swing.event.TableColumnModelListener;
033: import javax.swing.event.TableColumnModelEvent;
034: import javax.swing.event.ListSelectionEvent;
035: import javax.swing.event.ChangeEvent;
036:
037: // AWT
038: import java.awt.Color;
039: import java.awt.Frame;
040: import java.awt.Dialog;
041: import java.awt.Component;
042: import java.awt.BorderLayout;
043: import java.awt.event.WindowEvent;
044: import java.awt.event.WindowAdapter;
045:
046: // Logging
047: import java.util.logging.Level;
048: import java.util.logging.Logger;
049: import java.util.logging.Handler;
050: import java.util.logging.LogRecord;
051:
052: // Collections
053: import java.util.List;
054: import java.util.Arrays;
055: import java.util.ArrayList;
056:
057: // Resources
058: import org.geotools.util.logging.Logging;
059: import org.geotools.resources.XArray;
060: import org.geotools.resources.i18n.Vocabulary;
061: import org.geotools.resources.i18n.VocabularyKeys;
062: import org.geotools.resources.SwingUtilities;
063:
064: /**
065: * A panel displaying logging messages. The windows displaying Geotools's logging messages
066: * can be constructed with the following code:
067: *
068: * <blockquote><pre>
069: * new LoggingPanel("org.geotools").{@link #show(Component) show}(null);
070: * </pre></blockquote>
071: *
072: * This panel is initially set to listen to messages of level {@link Level#CONFIG} or higher.
073: * This level can be changed with <code>{@link #getHandler}.setLevel(aLevel)</code>.
074: *
075: * @since 2.0
076: * @source $URL: http://svn.geotools.org/geotools/tags/2.4.1/modules/extension/widgets-swing/src/main/java/org/geotools/gui/swing/LoggingPanel.java $
077: * @version $Id: LoggingPanel.java 27862 2007-11-12 19:51:19Z desruisseaux $
078: * @author Martin Desruisseaux
079: */
080: public class LoggingPanel extends JPanel {
081: /**
082: * Enumeration class for columns to be shown in a {@link LoggingPanel}.
083: * Valid columns include {@link #LOGGER LOGGER}, {@link #CLASS CLASS},
084: * {@link #METHOD METHOD}, {@link #TIME_OF_DAY TIME_OF_DAY}, {@link #LEVEL LEVEL}
085: * and {@link #MESSAGE MESSAGE}.
086: *
087: * @todo Use the enum keyword once J2SE 1.5 will be available.
088: */
089: public static final class Column {
090: final int index;
091:
092: Column(final int index) {
093: this .index = index;
094: }
095: }
096:
097: /*
098: * NOTE: Values for the following contants MUST match
099: * index in the LoggingTableModel.COLUMN_NAMES array.
100: */
101: /** Constant for {@link #setColumnVisible}. */
102: public static final Column LOGGER = new Column(0);
103: /** Constant for {@link #setColumnVisible}. */
104: public static final Column CLASS = new Column(1);
105: /** Constant for {@link #setColumnVisible}. */
106: public static final Column METHOD = new Column(2);
107: /** Constant for {@link #setColumnVisible}. */
108: public static final Column TIME_OF_DAY = new Column(3);
109: /** Constant for {@link #setColumnVisible}. */
110: public static final Column LEVEL = new Column(4);
111: /** Constant for {@link #setColumnVisible}. */
112: public static final Column MESSAGE = new Column(5);
113:
114: /**
115: * The background color for the columns prior to the logging message.
116: */
117: private static final Color INFO_BACKGROUND = new Color(240, 240,
118: 240);
119:
120: /**
121: * The model for this component.
122: */
123: private final LoggingTableModel model = new LoggingTableModel();
124:
125: /**
126: * The table for displaying logging messages.
127: */
128: private final JTable table = new JTable(model);
129:
130: /**
131: * The levels for colors enumerated in {@code levelColors}. This array
132: * <strong>must</strong> be in increasing order. Logging messages of level
133: * {@code levelValues[i]} or higher will be displayed with foreground
134: * color <code>levelColors[i*2]</code> and background color <code>levelColors[i*2+1]</code>.
135: *
136: * @see Level#intValue
137: * @see #getForeground(LogRecord)
138: * @see #getBackground(LogRecord)
139: */
140: private int[] levelValues = new int[0];
141:
142: /**
143: * Pairs of foreground and background colors to use for displaying logging messages.
144: * Logging messages of level {@code levelValues[i]} or higher will be displayed
145: * with foreground color <code>levelColors[i*2]</code> and background color
146: * <code>levelColors[i*2+1]</code>.
147: *
148: * @see #getForeground(LogRecord)
149: * @see #getBackground(LogRecord)
150: */
151: private final List levelColors = new ArrayList();
152:
153: /**
154: * The logger specified at construction time, or {@code null} if none.
155: */
156: private Logger logger;
157:
158: /**
159: * Constructs a new logging panel. This panel is not registered to any logger.
160: * Registration can be done with the following code:
161: *
162: * <blockquote><pre>
163: * logger.{@link Logger#addHandler addHandler}({@link #getHandler});
164: * </pre></blockquote>
165: */
166: public LoggingPanel() {
167: super (new BorderLayout());
168: table.setShowGrid(false);
169: table.setCellSelectionEnabled(false);
170: table.setGridColor(Color.LIGHT_GRAY);
171: table.setAutoResizeMode(JTable.AUTO_RESIZE_OFF);
172: table.setDefaultRenderer(Object.class, new CellRenderer());
173:
174: if (true) {
175: int width = 300;
176: final TableColumnModel columns = table.getColumnModel();
177: for (int i = model.getColumnCount(); --i >= 0;) {
178: columns.getColumn(i).setPreferredWidth(width);
179: width = 80;
180: }
181: }
182:
183: final JScrollPane scroll = new JScrollPane(table);
184: new AutoScroll(scroll.getVerticalScrollBar().getModel());
185: add(scroll, BorderLayout.CENTER);
186:
187: setLevelColor(Level.ALL, Color.GRAY, null);
188: setLevelColor(Level.CONFIG, null, null);
189: setLevelColor(Level.WARNING, Color.RED, null);
190: setLevelColor(Level.SEVERE, Color.WHITE, Color.RED);
191: }
192:
193: /**
194: * Constructs a new logging panel and register it to the specified logger.
195: *
196: * @param logger The logger to listen to, or {@code null} for the root logger.
197: */
198: public LoggingPanel(Logger logger) {
199: this ();
200: if (logger == null) {
201: logger = Logging.getLogger("");
202: }
203: logger.addHandler(getHandler());
204: this .logger = logger;
205: }
206:
207: /**
208: * Construct a logging panel and register it to the specified logger.
209: *
210: * @param logger The logger name to listen to, or {@code null} for the root logger.
211: */
212: public LoggingPanel(final String logger) {
213: this (org.geotools.util.logging.Logging
214: .getLogger(logger != null ? logger : ""));
215: }
216:
217: /**
218: * Returns the logging handler.
219: */
220: public Handler getHandler() {
221: return model;
222: }
223:
224: /**
225: * Returns {@code true} if the given column is visible.
226: *
227: * @param column The column to show or hide. May be one of {@link #LOGGER}, {@link #CLASS},
228: * {@link #METHOD}, {@link #TIME_OF_DAY}, {@link #LEVEL} or {@link #MESSAGE}.
229: */
230: public boolean isColumnVisible(final Column column) {
231: return model.isColumnVisible(column.index);
232: }
233:
234: /**
235: * Show or hide the given column.
236: *
237: * @param column The column to show or hide. May be one of {@link #LOGGER}, {@link #CLASS},
238: * {@link #METHOD}, {@link #TIME_OF_DAY}, {@link #LEVEL} or {@link #MESSAGE}.
239: * @param visible The visible state for the specified column.
240: */
241: public void setColumnVisible(final Column column,
242: final boolean visible) {
243: model.setColumnVisible(column.index, visible);
244: }
245:
246: /**
247: * Returns the capacity. This is the maximum number of {@link LogRecord}s the handler
248: * can memorize. If more messages are logged, then the earliest messages will be discarted.
249: */
250: public int getCapacity() {
251: return model.getCapacity();
252: }
253:
254: /**
255: * Set the capacity. This is the maximum number of {@link LogRecord}s the handler can
256: * memorize. If more messages are logged, then the earliest messages will be discarted.
257: */
258: public void setCapacity(final int capacity) {
259: model.setCapacity(capacity);
260: }
261:
262: /**
263: * Returns the foreground color for the specified log record. This method is invoked at
264: * rendering time for every cell in the table's "message" column. The default implementation
265: * returns a color based on the record's level, using colors set with {@link #setLevelColor}.
266: *
267: * @param record The record to get the foreground color.
268: * @return The foreground color for the specified record,
269: * or {@code null} for the default color.
270: */
271: public Color getForeground(final LogRecord record) {
272: return getColor(record, 0);
273: }
274:
275: /**
276: * Returns the background color for the specified log record. This method is invoked at
277: * rendering time for every cell in the table's "message" column. The default implementation
278: * returns a color based on the record's level, using colors set with {@link #setLevelColor}.
279: *
280: * @param record The record to get the background color.
281: * @return The background color for the specified record,
282: * or {@code null} for the default color.
283: */
284: public Color getBackground(final LogRecord record) {
285: return getColor(record, 1);
286: }
287:
288: /**
289: * Returns the foreground or background color for the specified record.
290: *
291: * @param record The record to get the color.
292: * @param offset 0 for the foreground color, or 1 for the background color.
293: * @return The color for the specified record, or {@code null} for the default color.
294: */
295: private Color getColor(final LogRecord record, final int offset) {
296: int i = Arrays.binarySearch(levelValues, record.getLevel()
297: .intValue());
298: if (i < 0) {
299: i = ~i - 1; // "~" is the tild symbol, not minus.
300: if (i < 0) {
301: return null;
302: }
303: }
304: return (Color) levelColors.get(i * 2 + offset);
305: }
306:
307: /**
308: * Set the foreground and background colors for messages of the specified level.
309: * The specified colors will apply on any messages of level {@code level} or
310: * greater, up to the next level set with an other call to {@code setLevelColor(...)}.
311: *
312: * @param level The minimal level to set color for.
313: * @param foreground The foreground color, or {@code null} for the default color.
314: * @param background The background color, or {@code null} for the default color.
315: */
316: public void setLevelColor(final Level level,
317: final Color foreground, final Color background) {
318: final int value = level.intValue();
319: int i = Arrays.binarySearch(levelValues, value);
320: if (i >= 0) {
321: i *= 2;
322: levelColors.set(i + 0, foreground);
323: levelColors.set(i + 1, background);
324: } else {
325: i = ~i;
326: levelValues = XArray.insert(levelValues, i, 1);
327: levelValues[i] = value;
328: i *= 2;
329: levelColors.add(i + 0, foreground);
330: levelColors.add(i + 1, background);
331: }
332: assert XArray.isSorted(levelValues);
333: assert levelValues.length * 2 == levelColors.size();
334: }
335:
336: /**
337: * Layout this component. This method give all the remaining space, if any,
338: * to the last table's column. This column is usually the one with logging
339: * messages.
340: */
341: public void doLayout() {
342: final TableColumnModel model = table.getColumnModel();
343: final int messageColumn = model.getColumnCount() - 1;
344: Component parent = table.getParent();
345: int delta = parent.getWidth();
346: if ((parent = parent.getParent()) instanceof JScrollPane) {
347: delta -= ((JScrollPane) parent).getVerticalScrollBar()
348: .getPreferredSize().width;
349: }
350: for (int i = 0; i < messageColumn; i++) {
351: delta -= model.getColumn(i).getWidth();
352: }
353: final TableColumn column = model.getColumn(messageColumn);
354: if (delta > Math.max(column.getWidth(), column
355: .getPreferredWidth())) {
356: column.setPreferredWidth(delta);
357: }
358: super .doLayout();
359: }
360:
361: /**
362: * Convenience method showing this logging panel into a frame.
363: * Different kinds of frame can be constructed according {@code owner} class:
364: *
365: * <ul>
366: * <li>If {@code owner} or one of its parent is a {@link JDesktopPane},
367: * then {@code panel} is added into a {@link JInternalFrame}.</li>
368: * <li>If {@code owner} or one of its parent is a {@link Frame} or a {@link Dialog},
369: * then {@code panel} is added into a {@link JDialog}.</li>
370: * <li>Otherwise, {@code panel} is added into a {@link JFrame}.</li>
371: * </ul>
372: *
373: * @param owner The owner, or {@code null} to show
374: * this logging panel in a top-level window.
375: * @return The frame. May be a {@link JInternalFrame},
376: * a {@link JDialog} or a {@link JFrame}.
377: */
378: public Component show(final Component owner) {
379: final Component frame = SwingUtilities.toFrame(owner, this ,
380: Vocabulary.format(VocabularyKeys.EVENT_LOGGER),
381: new WindowAdapter() {
382: public void windowClosed(WindowEvent event) {
383: dispose();
384: }
385: });
386: frame.setSize(750, 300);
387: frame.setVisible(true);
388: doLayout();
389: return frame;
390: }
391:
392: /**
393: * Free any resources used by this {@code LoggingPanel}. If a {@link Logger} was
394: * specified at construction time, then this method unregister the {@code LoggingPanel}'s
395: * handler from the specified logger. Next, {@link Handler#close} is invoked.
396: * <br><br>
397: * This method is invoked automatically when the user close the windows created
398: * with {@link #show(Component)}. If this {@code LoggingPanel} is displayed
399: * by some other ways (for example if it has been added into a {@link JPanel}),
400: * then this {@code dispose()} should be invoked explicitely when the container
401: * is being discarted.
402: */
403: public void dispose() {
404: final Handler handler = getHandler();
405: while (logger != null) {
406: logger.removeHandler(handler);
407: logger = logger.getParent();
408: }
409: handler.close();
410: }
411:
412: /**
413: * Display cell contents. This class is used for changing
414: * the cell's color according the log record level.
415: */
416: private final class CellRenderer extends DefaultTableCellRenderer
417: implements TableColumnModelListener {
418: /**
419: * Default color for the foreground.
420: */
421: private Color foreground;
422:
423: /**
424: * Default color for the background.
425: */
426: private Color background;
427:
428: /**
429: * The index of messages column.
430: */
431: private int messageColumn;
432:
433: /**
434: * The last row for which the side has been computed.
435: */
436: private int lastRow;
437:
438: /**
439: * Construct a new cell renderer.
440: */
441: public CellRenderer() {
442: foreground = super .getForeground();
443: background = super .getBackground();
444: table.getColumnModel().addColumnModelListener(this );
445: }
446:
447: /**
448: * Set the foreground color.
449: */
450: public void setForeground(final Color foreground) {
451: super .setForeground(this .foreground = foreground);
452: }
453:
454: /**
455: * Set the background colior
456: */
457: public void setBackground(final Color background) {
458: super .setBackground(this .background = background);
459: }
460:
461: /**
462: * Returns the component to use for painting the cell.
463: */
464: public Component getTableCellRendererComponent(
465: final JTable table, final Object value,
466: final boolean isSelected, final boolean hasFocus,
467: final int rowIndex, final int columnIndex) {
468: Color foreground = this .foreground;
469: Color background = this .background;
470: final boolean isMessage = (columnIndex == messageColumn);
471: if (!isMessage) {
472: background = INFO_BACKGROUND;
473: }
474: if (rowIndex >= 0) {
475: final TableModel candidate = table.getModel();
476: if (candidate instanceof LoggingTableModel) {
477: final LoggingTableModel model = (LoggingTableModel) candidate;
478: final LogRecord record = model
479: .getLogRecord(rowIndex);
480: Color color;
481: color = LoggingPanel.this .getForeground(record);
482: if (color != null)
483: foreground = color;
484: color = LoggingPanel.this .getBackground(record);
485: if (color != null)
486: background = color;
487: }
488: }
489: super .setBackground(background);
490: super .setForeground(foreground);
491: final Component component = super
492: .getTableCellRendererComponent(table, value,
493: isSelected, hasFocus, rowIndex, columnIndex);
494: /*
495: * If a new record is being painted and this new record is wider
496: * than previous ones, then make the message column width larger.
497: */
498: if (isMessage) {
499: if (rowIndex > lastRow) {
500: final int width = component.getPreferredSize().width + 15;
501: final TableColumn column = table.getColumnModel()
502: .getColumn(columnIndex);
503: if (width > column.getPreferredWidth()) {
504: column.setPreferredWidth(width);
505: }
506: if (rowIndex == lastRow + 1) {
507: lastRow = rowIndex;
508: }
509: }
510: }
511: return component;
512: }
513:
514: /**
515: * Invoked when the message column may have moved. This method update the
516: * {@link #messageColumn} field, so that the message column will continue
517: * to be paint with special colors.
518: */
519: private final void update() {
520: messageColumn = table.convertColumnIndexToView(model
521: .getColumnCount() - 1);
522: }
523:
524: public void columnAdded(TableColumnModelEvent e) {
525: update();
526: }
527:
528: public void columnMarginChanged(ChangeEvent e) {
529: update();
530: }
531:
532: public void columnMoved(TableColumnModelEvent e) {
533: update();
534: }
535:
536: public void columnRemoved(TableColumnModelEvent e) {
537: update();
538: }
539:
540: public void columnSelectionChanged(ListSelectionEvent e) {
541: update();
542: }
543: }
544: }
|