/*
* Geotoolkit.org - An Open Source Java GIS Toolkit
* http://www.geotoolkit.org
*
* (C) 2002-2012, Open Source Geospatial Foundation (OSGeo)
* (C) 2009-2012, Geomatys
*
* 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.geotoolkit.gui.swing;
import javax.swing.JTable;
import javax.swing.JPanel;
import javax.swing.JComponent;
import javax.swing.JScrollPane;
import javax.swing.table.TableColumn;
import javax.swing.table.TableColumnModel;
import javax.swing.ScrollPaneConstants;
import java.awt.Font;
import java.awt.Color;
import java.awt.Component;
import java.awt.BorderLayout;
import java.awt.event.WindowEvent;
import java.awt.event.WindowAdapter;
import java.util.Arrays;
import java.util.Comparator;
import java.util.logging.Level;
import java.util.logging.Logger;
import java.util.logging.Handler;
import org.jdesktop.swingx.JXTable;
import org.jdesktop.swingx.table.TableColumnExt;
import org.jdesktop.swingx.renderer.StringValue;
import org.jdesktop.swingx.renderer.DefaultTableRenderer;
import org.jdesktop.swingx.decorator.ComponentAdapter;
import org.jdesktop.swingx.decorator.ColorHighlighter;
import org.jdesktop.swingx.decorator.HighlightPredicate;
import org.apache.sis.util.ArraysExt;
import org.apache.sis.util.Disposable;
import org.apache.sis.util.logging.Logging;
import org.geotoolkit.resources.Vocabulary;
/**
* A panel displaying logging messages. The windows displaying Geotk's logging messages
* can be constructed with the following code:
*
* {@preformat java
* new LoggingPanel("org.geotoolkit").show(null);
* }
*
* This panel initially listens to all messages ({@link Level#ALL}). However the messages will
* still be filtered according the {@linkplain Logger#getLevel() logger level}. If all levels
* are really aimed to be reported, then a call to {@code Logger.setLevel(Level.ALL)} may be
* needed.
* <p>
* Note that a different level can be set specifically to this {@code LoggingPanel} with a call
* to <code>{@linkplain #getHandler}.setLevel(aLevel)</code>. However this is only for restricting
* the logger messages to a higher level than the logger level.
*
* @author Martin Desruisseaux (IRD, Geomatys)
* @version 3.12
*
* @since 2.0
* @module
*/
@SuppressWarnings("serial")
public class LoggingPanel extends JComponent implements Disposable {
/**
* Enumeration class for columns to be shown in a {@link LoggingPanel}.
*
* @author Martin Desruisseaux (IRD)
* @version 3.00
*
* @see LoggingPanel#setColumnVisible
*
* @since 2.0
* @module
*/
public static enum Column {
/*
* NOTE: Orinal values MUST match index in the LoggingTableModel.COLUMN_NAMES array.
*/
/** The column displaying the logger name. */ LOGGER (160),
/** The column displaying the originating class. */ CLASS (120),
/** The column displaying the originating method. */ METHOD (80),
/** The column displaying the log record time. */ TIME_OF_DAY(80),
/** The column displaying the log record level. */ LEVEL (80),
/** The column displaying the message. */ MESSAGE (300);
/** The preferred width. */
final int width;
/** Creates a new column with the given preferred width. */
private Column(final int width) {
this.width = width;
}
}
/**
* The model for this component.
*/
private final LoggingTableModel model;
/**
* The table for displaying logging messages.
*/
private final JXTable table;
/**
* Scroll down automatically when a new log record is added, provided that
* the scroll is already at the bottom.
*/
private final LoggingTableModel.Scroll scrollControl;
/**
* Foreground and background colors to use for displaying logging messages.
*
* @see #getForeground(Level)
* @see #getBackground(Level)
*/
private Highlighter[] levelColors = new Highlighter[0];
/**
* The logger specified at construction time, or {@code null} if none.
*/
private Logger logger;
/**
* The font to use for messages. We use by default a monospaced font
* because some messages are formatted for the console (e.g. as a table).
*/
private Font messageFont;
/**
* Constructs a new logging panel. This panel is not registered to any logger.
* Registration can be done with the following code:
*
* {@preformat java
* logger.addHandler(getHandler());
* }
*/
public LoggingPanel() {
setLayout(new BorderLayout());
messageFont = Font.decode("Monospaced");
model = new LoggingTableModel();
table = new JXTable(model);
/*
* Sets table properties, especially the row height which is set to the font size.
* This is needed in order to preserve the formatting of boxes, trees, etc. printed
* using the drawing character of monospaced font. The row height will be set again
* in the setMessageFont(...) method.
*/
table.setShowGrid(false);
table.setRolloverEnabled(false);
table.setColumnControlVisible(true);
table.setCellSelectionEnabled(false);
table.setRowHeight(messageFont.getSize());
table.setRowMargin(0);
table.setAutoResizeMode(JTable.AUTO_RESIZE_LAST_COLUMN);
final TableColumnModel columns = table.getColumnModel();
for (final Column c : Column.values()) {
final TableColumn column = columns.getColumn(c.ordinal());
column.setPreferredWidth(c.width);
column.setIdentifier(c);
}
table.setDefaultRenderer(Level.class, new DefaultTableRenderer(new StringValue() {
@Override public String getString(final Object value) {
return (value instanceof Level) ? ((Level) value).getLocalizedName() : null;
}
}));
final JScrollPane scroll = new JScrollPane(table);
scroll.setVerticalScrollBarPolicy(ScrollPaneConstants.VERTICAL_SCROLLBAR_ALWAYS);
scrollControl = new LoggingTableModel.Scroll(table, scroll.getVerticalScrollBar().getModel());
model.addTableModelListener(scrollControl);
add(scroll, BorderLayout.CENTER);
setBackground(Color.WHITE);
setForeground(Color.BLACK);
setLevelColor(Level.ALL, Color.GRAY, null);
setLevelColor(Level.CONFIG, null, null);
setLevelColor(Level.WARNING, null, Color.YELLOW);
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;
}
/**
* Constructs 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(Logging.getLogger(logger != null ? logger : ""));
}
/**
* Returns the logging handler. This is the handler to register to loggers in order to get
* logging message to appear in the widget. This registration had been done automatically
* if this widget has been created with any constructor except the no-argument one.
*
* @return 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.
* @return {@code true} if the given column is visible.
*/
public boolean isColumnVisible(final Column column) {
return ((TableColumnExt) table.getColumn(column)).isVisible();
}
/**
* Shows or hide the given column.
*
* @param column The column to show or hide.
* @param visible The visible state for the specified column.
*/
public void setColumnVisible(final Column column, final boolean visible) {
((TableColumnExt) table.getColumn(column)).setVisible(visible);
}
/**
* Returns the maximum number of {@link java.util.logging.LogRecord}s the handler can
* memorize. If more messages are logged, then the earliest messages will be discarded.
*
* @return The current maximum number of record.
*/
public int getCapacity() {
return model.getCapacity();
}
/**
* Sets the maximum number of {@link java.util.logging.LogRecord}s the handler can memorize.
* If more messages are logged, then the earliest messages will be discarded.
*
* @param capacity The new maximum number of record.
*/
public void setCapacity(final int capacity) {
model.setCapacity(capacity);
}
/**
* Returns the font to use for displaying the messages (last table column).
* The default is a monospaced font because some message are formatted for
* the console (for example they may use the drawing unicode characters).
*
* @return The font to use for displaying messages.
*
* @since 3.01
*/
public Font getMessageFont() {
return messageFont;
}
/**
* Sets the font to use for displaying the messages.
*
* @param font The new font for displaying messages.
*
* @since 3.01
*/
public void setMessageFont(final Font font) {
final Font old = messageFont;
messageFont = font;
table.setRowHeight(font.getSize());
firePropertyChange("messageFont", old, font);
repaint();
}
/**
* Returns the foreground color for the given level.
*
* @param level The level for which to get the foreground color.
* @return The foreground color for the given level, or {@code null} for the default color.
*
* @since 3.01
*/
public Color getForeground(final Level level) {
final ColorHighlighter hlr = getHighlighter(level);
return (hlr != null) ? hlr.getForeground() : null;
}
/**
* Returns the background color for the specified log record. This method returns a color based
* on the record level, using colors set with {@link #setLevelColor setLevelColor(...)}.
*
* @param level The level for which to get the background color.
* @return The background color for the given level, or {@code null} for the default color.
*
* @since 3.01
*/
public Color getBackground(final Level level) {
final ColorHighlighter hlr = getHighlighter(level);
return (hlr != null) ? hlr.getBackground() : null;
}
/**
* Returns the highlighter for the given level.
*
* @param level The level for which to get the highlighter.
* @return The highlighter for the given level, or {@code null} if none.
*/
private ColorHighlighter getHighlighter(final Level level) {
int i = Arrays.binarySearch(levelColors, level, COMPARATOR);
if (i < 0) {
/*
* No exact match for the given level.
* Looks for the level below the requested one.
*/
i = ~i - 1; // "~" is the tild symbol, not minus.
if (i < 0) {
return null;
}
}
return levelColors[i];
}
/**
* Sets the foreground and background colors for messages of the specified level.
* The specified colors will apply on any messages of level {@code level} or
* higher, 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 Highlighter hlr;
Highlighter[] levelColors = this.levelColors;
int i = Arrays.binarySearch(levelColors, level, COMPARATOR);
if (i >= 0) {
hlr = levelColors[i];
} else {
i = ~i;
hlr = new Highlighter(level.intValue());
this.levelColors = levelColors = ArraysExt.insert(levelColors, i, 1);
levelColors[i] = hlr;
if (i != 0) {
levelColors[i-1].upper = hlr.lower;
}
if (++i < levelColors.length) {
hlr.upper = levelColors[i].lower;
}
assert ArraysExt.isSorted(levelColors, COMPARATOR, true);
table.setHighlighters(levelColors);
}
hlr.setBackground(background);
hlr.setForeground(foreground);
}
/**
* Layout this component. This method gives all the remaining space, if any,
* to the last table's column. This column is usually the one with logging
* messages.
*/
@Override
public void doLayout() {
final TableColumnModel model = table.getColumnModel();
final int messageColumn = model.getColumnCount() - 1;
Component parent = table.getParent();
int delta = parent.getWidth();
parent = parent.getParent();
if (parent 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 the {@code owner} class:
* <p>
* <ul>
* <li>If {@code owner} or one of its parent is a {@link javax.swing.JDesktopPane},
* then {@code panel} is added into a {@link javax.swing.JInternalFrame}.</li>
* <li>If {@code owner} or one of its parent is a {@link java.awt.Frame} or a
* {@link java.awt.Dialog}, then {@code panel} is added into a
* {@link javax.swing.JDialog}.</li>
* <li>Otherwise, {@code panel} is added into a {@link javax.swing.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 javax.swing.JInternalFrame},
* a {@link javax.swing.JDialog} or a {@link javax.swing.JFrame}.
*/
public Component show(final Component owner) {
final String title = Vocabulary.format(Vocabulary.Keys.EventLogger);
final Window frame = WindowCreator.Handler.DEFAULT.createWindow(owner, this, title);
frame.addWindowListener(new WindowAdapter() {
@Override public void windowClosed(WindowEvent event) {
dispose();
}
});
frame.setSize(750, 300);
frame.setVisible(true);
doLayout();
return (Component) frame;
}
/**
* Frees 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.
* <p>
* This method is invoked automatically when the user closes 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()}
* method must be invoked explicitly when the container is being discarded.
*/
@Override
public void dispose() {
model.removeTableModelListener(scrollControl);
scrollControl.dispose();
final Handler handler = getHandler();
while (logger != null) {
logger.removeHandler(handler);
logger = logger.getParent();
}
handler.close();
}
/**
* Compares two levels for order. The level can be encapsulated in either
* {@link Level} or {@link Highlighter} object.
*/
private static final Comparator<Object> COMPARATOR = new Comparator<Object>() {
@Override public int compare(final Object o1, final Object o2) {
// Do not return (n1 - n2) - it doesn't work because of overflow.
final int n1 = level(o1);
final int n2 = level(o2);
if (n1 > n2) return +1;
if (n1 < n2) return -1;
return 0;
}
};
/**
* Returns the numeric value of the given level. The level can be encapsulated in either
* {@link Level} or {@link Highlighter} object.
*/
static int level(final Object o) {
if (o instanceof Level) {
return ((Level) o).intValue();
}
return ((Highlighter) o).lower;
}
/**
* Used for changing the cell's color according the log record level.
*
* @author Martin Desruisseaux (Geomatys)
* @version 3.01
*
* @since 3.01
* @module
*/
private final class Highlighter extends ColorHighlighter implements HighlightPredicate {
/**
* Uses this highlighter only for log level in the given range.
* The lower value is inclusive while the upper value is exclusive.
*/
final int lower;
/**
* The upper level value, exclusive. Must be updated by the caller
* when other highlighter are added or removed.
*/
int upper;
/**
* Creates a default highlighter.
*/
Highlighter(final int level) {
lower = level;
upper = Integer.MAX_VALUE;
setHighlightPredicate(this);
}
/**
* Returns {@code true} if this highlighter should be applied to the current row.
*/
@Override
public boolean isHighlighted(final Component renderer, final ComponentAdapter adapter) {
final Object value = adapter.getValue(Column.LEVEL.ordinal());
final int level;
if (value instanceof Level) {
// The normal case.
level = ((Level) value).intValue();
} else if (value instanceof Integer) {
// This is a special case generated by LoggingTableModel.Record.getValueAt(...)
// when the log message span more than one line, and we are asking for any line
// other than the first one.
if (adapter.viewToModel(adapter.column) != Column.MESSAGE.ordinal()) {
return false;
}
level = ((Integer) value).intValue();
} else {
return false;
}
return level >= lower && level < upper;
}
/**
* Applies the color, with a special processing for the message column.
*/
@Override
protected Component doHighlight(final Component renderer, final ComponentAdapter adapter) {
if (adapter.viewToModel(adapter.column) == Column.MESSAGE.ordinal()) {
renderer.setBackground(null);
if (getBackground() == null) {
renderer.setForeground(getForeground());
}
renderer.setFont(messageFont);
return renderer;
}
renderer.setFont(null);
return super.doHighlight(renderer, adapter);
}
}
}