/*
* GeoTools - The Open Source Java GIS Toolkit
* http://geotools.org
*
* (C) 2002-2008, Open Source Geospatial Foundation (OSGeo)
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation;
* version 2.1 of the License.
*
* This library is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*/
package org.geotools.gui.swing;
// Swing dependencies
import javax.swing.JTable;
import javax.swing.JPanel;
import javax.swing.JFrame;
import javax.swing.JDialog;
import javax.swing.JComponent;
import javax.swing.JScrollPane;
import javax.swing.JDesktopPane;
import javax.swing.JInternalFrame;
import javax.swing.table.TableModel;
import javax.swing.table.TableColumn;
import javax.swing.table.TableColumnModel;
import javax.swing.table.DefaultTableCellRenderer;
import javax.swing.event.TableColumnModelListener;
import javax.swing.event.TableColumnModelEvent;
import javax.swing.event.ListSelectionEvent;
import javax.swing.event.ChangeEvent;
// AWT
import java.awt.Color;
import java.awt.Frame;
import java.awt.Dialog;
import java.awt.Component;
import java.awt.BorderLayout;
import java.awt.event.WindowEvent;
import java.awt.event.WindowAdapter;
// Logging
import java.util.logging.Level;
import java.util.logging.Logger;
import java.util.logging.Handler;
import java.util.logging.LogRecord;
// Collections
import java.util.List;
import java.util.Arrays;
import java.util.ArrayList;
// Resources
import org.geotools.util.logging.Logging;
import org.geotools.resources.XArray;
import org.geotools.resources.i18n.Vocabulary;
import org.geotools.resources.i18n.VocabularyKeys;
import org.geotools.resources.SwingUtilities;
/**
* A panel displaying logging messages. The windows displaying Geotools's logging messages
* can be constructed with the following code:
*
* <blockquote><pre>
* new LoggingPanel("org.geotools").{@link #show(Component) show}(null);
* </pre></blockquote>
*
* This panel is initially set to listen to messages of level {@link Level#CONFIG} or higher.
* This level can be changed with <code>{@link #getHandler}.setLevel(aLevel)</code>.
*
* @since 2.0
* @source $URL$
* @version $Id$
* @author Martin Desruisseaux (IRD)
*/
public class LoggingPanel extends JPanel {
/**
* Enumeration class for columns to be shown in a {@link LoggingPanel}.
* Valid columns include {@link #LOGGER LOGGER}, {@link #CLASS CLASS},
* {@link #METHOD METHOD}, {@link #TIME_OF_DAY TIME_OF_DAY}, {@link #LEVEL LEVEL}
* and {@link #MESSAGE MESSAGE}.
*
* @todo Use the enum keyword once J2SE 1.5 will be available.
*/
public static final class Column {
final int index;
Column(final int index) {
this.index = index;
}
}
/*
* NOTE: Values for the following contants MUST match
* index in the LoggingTableModel.COLUMN_NAMES array.
*/
/** Constant for {@link #setColumnVisible}. */ public static final Column LOGGER = new Column(0);
/** Constant for {@link #setColumnVisible}. */ public static final Column CLASS = new Column(1);
/** Constant for {@link #setColumnVisible}. */ public static final Column METHOD = new Column(2);
/** Constant for {@link #setColumnVisible}. */ public static final Column TIME_OF_DAY = new Column(3);
/** Constant for {@link #setColumnVisible}. */ public static final Column LEVEL = new Column(4);
/** Constant for {@link #setColumnVisible}. */ public static final Column MESSAGE = new Column(5);
/**
* The background color for the columns prior to the logging message.
*/
private static final Color INFO_BACKGROUND = new Color(240,240,240);
/**
* The model for this component.
*/
private final LoggingTableModel model = new LoggingTableModel();
/**
* The table for displaying logging messages.
*/
private final JTable table = new JTable(model);
/**
* The levels for colors enumerated in {@code levelColors}. This array
* <strong>must</strong> be in increasing order. Logging messages of level
* {@code levelValues[i]} or higher will be displayed with foreground
* color <code>levelColors[i*2]</code> and background color <code>levelColors[i*2+1]</code>.
*
* @see Level#intValue
* @see #getForeground(LogRecord)
* @see #getBackground(LogRecord)
*/
private int[] levelValues = new int[0];
/**
* Pairs of foreground and background colors to use for displaying logging messages.
* Logging messages of level {@code levelValues[i]} or higher will be displayed
* with foreground color <code>levelColors[i*2]</code> and background color
* <code>levelColors[i*2+1]</code>.
*
* @see #getForeground(LogRecord)
* @see #getBackground(LogRecord)
*/
private final List levelColors = new ArrayList();
/**
* The logger specified at construction time, or {@code null} if none.
*/
private Logger logger;
/**
* Constructs a new logging panel. This panel is not registered to any logger.
* Registration can be done with the following code:
*
* <blockquote><pre>
* logger.{@link Logger#addHandler addHandler}({@link #getHandler});
* </pre></blockquote>
*/
public LoggingPanel() {
super(new BorderLayout());
table.setShowGrid(false);
table.setCellSelectionEnabled(false);
table.setGridColor(Color.LIGHT_GRAY);
table.setAutoResizeMode(JTable.AUTO_RESIZE_OFF);
table.setDefaultRenderer(Object.class, new CellRenderer());
if (true) {
int width = 300;
final TableColumnModel columns = table.getColumnModel();
for (int i=model.getColumnCount(); --i>=0;) {
columns.getColumn(i).setPreferredWidth(width);
width = 80;
}
}
final JScrollPane scroll = new JScrollPane(table);
new AutoScroll(scroll.getVerticalScrollBar().getModel());
add(scroll, BorderLayout.CENTER);
setLevelColor(Level.ALL, Color.GRAY, null);
setLevelColor(Level.CONFIG, null, null);
setLevelColor(Level.WARNING, Color.RED, null);
setLevelColor(Level.SEVERE, Color.WHITE, Color.RED);
}
/**
* Constructs a new logging panel and register it to the specified logger.
*
* @param logger The logger to listen to, or {@code null} for the root logger.
*/
public LoggingPanel(Logger logger) {
this();
if (logger == null) {
logger = Logging.getLogger("");
}
logger.addHandler(getHandler());
this.logger = logger;
}
/**
* Construct a logging panel and register it to the specified logger.
*
* @param logger The logger name to listen to, or {@code null} for the root logger.
*/
public LoggingPanel(final String logger) {
this(org.geotools.util.logging.Logging.getLogger(logger!=null ? logger : ""));
}
/**
* Returns the logging handler.
*/
public Handler getHandler() {
return model;
}
/**
* Returns {@code true} if the given column is visible.
*
* @param column The column to show or hide. May be one of {@link #LOGGER}, {@link #CLASS},
* {@link #METHOD}, {@link #TIME_OF_DAY}, {@link #LEVEL} or {@link #MESSAGE}.
*/
public boolean isColumnVisible(final Column column) {
return model.isColumnVisible(column.index);
}
/**
* Show or hide the given column.
*
* @param column The column to show or hide. May be one of {@link #LOGGER}, {@link #CLASS},
* {@link #METHOD}, {@link #TIME_OF_DAY}, {@link #LEVEL} or {@link #MESSAGE}.
* @param visible The visible state for the specified column.
*/
public void setColumnVisible(final Column column, final boolean visible) {
model.setColumnVisible(column.index, visible);
}
/**
* Returns the capacity. This is the maximum number of {@link LogRecord}s the handler
* can memorize. If more messages are logged, then the earliest messages will be discarted.
*/
public int getCapacity() {
return model.getCapacity();
}
/**
* Set the capacity. This is the maximum number of {@link LogRecord}s the handler can
* memorize. If more messages are logged, then the earliest messages will be discarted.
*/
public void setCapacity(final int capacity) {
model.setCapacity(capacity);
}
/**
* Returns the foreground color for the specified log record. This method is invoked at
* rendering time for every cell in the table's "message" column. The default implementation
* returns a color based on the record's level, using colors set with {@link #setLevelColor}.
*
* @param record The record to get the foreground color.
* @return The foreground color for the specified record,
* or {@code null} for the default color.
*/
public Color getForeground(final LogRecord record) {
return getColor(record, 0);
}
/**
* Returns the background color for the specified log record. This method is invoked at
* rendering time for every cell in the table's "message" column. The default implementation
* returns a color based on the record's level, using colors set with {@link #setLevelColor}.
*
* @param record The record to get the background color.
* @return The background color for the specified record,
* or {@code null} for the default color.
*/
public Color getBackground(final LogRecord record) {
return getColor(record, 1);
}
/**
* Returns the foreground or background color for the specified record.
*
* @param record The record to get the color.
* @param offset 0 for the foreground color, or 1 for the background color.
* @return The color for the specified record, or {@code null} for the default color.
*/
private Color getColor(final LogRecord record, final int offset) {
int i = Arrays.binarySearch(levelValues, record.getLevel().intValue());
if (i < 0) {
i = ~i - 1; // "~" is the tild symbol, not minus.
if (i < 0) {
return null;
}
}
return (Color) levelColors.get(i*2 + offset);
}
/**
* Set the foreground and background colors for messages of the specified level.
* The specified colors will apply on any messages of level {@code level} or
* greater, up to the next level set with an other call to {@code setLevelColor(...)}.
*
* @param level The minimal level to set color for.
* @param foreground The foreground color, or {@code null} for the default color.
* @param background The background color, or {@code null} for the default color.
*/
public void setLevelColor(final Level level, final Color foreground, final Color background) {
final int value = level.intValue();
int i = Arrays.binarySearch(levelValues, value);
if (i >= 0) {
i *= 2;
levelColors.set(i+0, foreground);
levelColors.set(i+1, background);
} else {
i = ~i;
levelValues = XArray.insert(levelValues, i, 1);
levelValues[i] = value;
i *= 2;
levelColors.add(i+0, foreground);
levelColors.add(i+1, background);
}
assert XArray.isSorted(levelValues);
assert levelValues.length*2 == levelColors.size();
}
/**
* Layout this component. This method give all the remaining space, if any,
* to the last table's column. This column is usually the one with logging
* messages.
*/
public void doLayout() {
final TableColumnModel model = table.getColumnModel();
final int messageColumn = model.getColumnCount()-1;
Component parent = table.getParent();
int delta = parent.getWidth();
if ((parent=parent.getParent()) instanceof JScrollPane) {
delta -= ((JScrollPane) parent).getVerticalScrollBar().getPreferredSize().width;
}
for (int i=0; i<messageColumn; i++) {
delta -= model.getColumn(i).getWidth();
}
final TableColumn column = model.getColumn(messageColumn);
if (delta > Math.max(column.getWidth(), column.getPreferredWidth())) {
column.setPreferredWidth(delta);
}
super.doLayout();
}
/**
* Convenience method showing this logging panel into a frame.
* Different kinds of frame can be constructed according {@code owner} class:
*
* <ul>
* <li>If {@code owner} or one of its parent is a {@link JDesktopPane},
* then {@code panel} is added into a {@link JInternalFrame}.</li>
* <li>If {@code owner} or one of its parent is a {@link Frame} or a {@link Dialog},
* then {@code panel} is added into a {@link JDialog}.</li>
* <li>Otherwise, {@code panel} is added into a {@link JFrame}.</li>
* </ul>
*
* @param owner The owner, or {@code null} to show
* this logging panel in a top-level window.
* @return The frame. May be a {@link JInternalFrame},
* a {@link JDialog} or a {@link JFrame}.
*/
public Component show(final Component owner) {
final Component frame = SwingUtilities.toFrame(owner, this,
Vocabulary.format(VocabularyKeys.EVENT_LOGGER),
new WindowAdapter()
{
public void windowClosed(WindowEvent event) {
dispose();
}
});
frame.setSize(750, 300);
frame.setVisible(true);
doLayout();
return frame;
}
/**
* Free any resources used by this {@code LoggingPanel}. If a {@link Logger} was
* specified at construction time, then this method unregister the {@code LoggingPanel}'s
* handler from the specified logger. Next, {@link Handler#close} is invoked.
* <br><br>
* This method is invoked automatically when the user close the windows created
* with {@link #show(Component)}. If this {@code LoggingPanel} is displayed
* by some other ways (for example if it has been added into a {@link JPanel}),
* then this {@code dispose()} should be invoked explicitely when the container
* is being discarted.
*/
public void dispose() {
final Handler handler = getHandler();
while (logger != null) {
logger.removeHandler(handler);
logger = logger.getParent();
}
handler.close();
}
/**
* Display cell contents. This class is used for changing
* the cell's color according the log record level.
*/
private final class CellRenderer extends DefaultTableCellRenderer
implements TableColumnModelListener
{
/**
* Default color for the foreground.
*/
private Color foreground;
/**
* Default color for the background.
*/
private Color background;
/**
* The index of messages column.
*/
private int messageColumn;
/**
* The last row for which the side has been computed.
*/
private int lastRow;
/**
* Construct a new cell renderer.
*/
public CellRenderer() {
foreground = super.getForeground();
background = super.getBackground();
table.getColumnModel().addColumnModelListener(this);
}
/**
* Set the foreground color.
*/
public void setForeground(final Color foreground) {
super.setForeground(this.foreground=foreground);
}
/**
* Set the background colior
*/
public void setBackground(final Color background) {
super.setBackground(this.background=background);
}
/**
* Returns the component to use for painting the cell.
*/
public Component getTableCellRendererComponent(final JTable table,
final Object value,
final boolean isSelected,
final boolean hasFocus,
final int rowIndex,
final int columnIndex)
{
Color foreground = this.foreground;
Color background = this.background;
final boolean isMessage = (columnIndex == messageColumn);
if (!isMessage) {
background = INFO_BACKGROUND;
}
if (rowIndex >= 0) {
final TableModel candidate = table.getModel();
if (candidate instanceof LoggingTableModel) {
final LoggingTableModel model = (LoggingTableModel) candidate;
final LogRecord record = model.getLogRecord(rowIndex);
Color color;
color=LoggingPanel.this.getForeground(record); if (color!=null) foreground=color;
color=LoggingPanel.this.getBackground(record); if (color!=null) background=color;
}
}
super.setBackground(background);
super.setForeground(foreground);
final Component component = super.getTableCellRendererComponent(table, value,
isSelected, hasFocus, rowIndex, columnIndex);
/*
* If a new record is being painted and this new record is wider
* than previous ones, then make the message column width larger.
*/
if (isMessage) {
if (rowIndex > lastRow) {
final int width = component.getPreferredSize().width + 15;
final TableColumn column = table.getColumnModel().getColumn(columnIndex);
if (width > column.getPreferredWidth()) {
column.setPreferredWidth(width);
}
if (rowIndex == lastRow+1) {
lastRow = rowIndex;
}
}
}
return component;
}
/**
* Invoked when the message column may have moved. This method update the
* {@link #messageColumn} field, so that the message column will continue
* to be paint with special colors.
*/
private final void update() {
messageColumn = table.convertColumnIndexToView(model.getColumnCount()-1);
}
public void columnAdded (TableColumnModelEvent e) {update();}
public void columnMarginChanged (ChangeEvent e) {update();}
public void columnMoved (TableColumnModelEvent e) {update();}
public void columnRemoved (TableColumnModelEvent e) {update();}
public void columnSelectionChanged(ListSelectionEvent e) {update();}
}
}