package org.rsbot.gui.component; import org.rsbot.log.LogFormatter; import org.rsbot.util.StringUtil; import javax.swing.*; import javax.swing.border.Border; import javax.swing.border.EmptyBorder; import java.awt.*; import java.text.SimpleDateFormat; import java.util.ArrayList; import java.util.List; import java.util.logging.Formatter; import java.util.logging.Level; import java.util.logging.LogRecord; /** * Non swing methods are thread safe. */ public class LogTextArea extends JList { private static final int MAX_ENTRIES = 100; private static final Rectangle BOTTOM_OF_WINDOW = new Rectangle(0, Integer.MAX_VALUE, 0, 0); private static final long serialVersionUID = 0; private final LogQueue logQueue = new LogQueue(); private final LogAreaListModel model = new LogAreaListModel(); private final Runnable scrollToBottom = new Runnable() { public void run() { scrollRectToVisible(LogTextArea.BOTTOM_OF_WINDOW); } }; private static final Formatter formatter = new Formatter() { private final SimpleDateFormat dateFormat = new SimpleDateFormat("hh:mm:ss"); @Override public String format(final LogRecord record) { final String[] className = record.getLoggerName().split("\\."); final String name = className[className.length - 1]; final int maxLen = 16; final String append = "..."; return String.format("[%s] %-" + maxLen + "s %s %s", dateFormat.format(record.getMillis()), name.length() > maxLen ? name.substring(0, maxLen - append.length()) + append : name, record.getMessage(), StringUtil.throwableToString(record.getThrown())); } }; private static final Formatter copyPasteFormatter = new LogFormatter(false); public LogTextArea() { setModel(model); setCellRenderer(new Renderer()); setSelectionMode(ListSelectionModel.MULTIPLE_INTERVAL_SELECTION); String fontName = Font.MONOSPACED; for (final Font font : GraphicsEnvironment.getLocalGraphicsEnvironment().getAllFonts()) { final String name = font.getName(); if (name.matches("Monaco|Consolas")) { fontName = name; break; } } setFont(new Font(fontName, Font.PLAIN, 12)); new Thread(logQueue, "LogGuiQueue").start(); } /** * Logs a new entry to be shown in the list. Thread safe. * * @param logRecord The entry. */ public void log(final LogRecord logRecord) { logQueue.queue(new WrappedLogRecord(logRecord)); } private class LogAreaListModel extends AbstractListModel { private static final long serialVersionUID = 0; private List<WrappedLogRecord> records = new ArrayList<WrappedLogRecord>(LogTextArea.MAX_ENTRIES); public void addAllElements(final List<WrappedLogRecord> obj) { records.addAll(obj); if (getSize() > LogTextArea.MAX_ENTRIES) { records.subList(0, (getSize() - LogTextArea.MAX_ENTRIES)).clear(); fireContentsChanged(this, 0, (getSize() - 1)); } else { fireIntervalAdded(this, (getSize() - 1), (getSize() - 1)); } } public Object getElementAt(final int index) { return records.get(index); } public int getSize() { return records.size(); } } /** * Flushes every #FLUSH_RATE (milliseconds) */ private class LogQueue implements Runnable { public static final int FLUSH_RATE = 1000; private final Object lock = new Object(); private List<WrappedLogRecord> queue = new ArrayList<WrappedLogRecord>(100); public void queue(final WrappedLogRecord record) { synchronized (lock) { queue.add(record); } } public void run() { while (true) { List<WrappedLogRecord> toFlush = null; synchronized (lock) { if (queue.size() != 0) { toFlush = new ArrayList<WrappedLogRecord>(queue); queue = queue.subList(0, 0); } } if (toFlush != null) { // Hold the lock for as little time as possible model.addAllElements(toFlush); SwingUtilities.invokeLater(scrollToBottom); } try { Thread.sleep(LogQueue.FLUSH_RATE); } catch (final InterruptedException e) { throw new RuntimeException(e); } } } } private static class Renderer implements ListCellRenderer { private final Border EMPTY_BORDER = new EmptyBorder(1, 1, 1, 1); private final Border SELECTED_BORDER = UIManager.getBorder("List.focusCellHighlightBorder"); private final Color DARK_GREEN = new Color(0, 90, 0); public Component getListCellRendererComponent(final JList list, final Object value, final int index, final boolean isSelected, final boolean cellHasFocus) { if (!(value instanceof WrappedLogRecord)) { return new JLabel(); } final WrappedLogRecord wlr = (WrappedLogRecord) value; final JTextPane result = new JTextPane(); result.setDragEnabled(true); result.setText(wlr.formatted); result.setComponentOrientation(list.getComponentOrientation()); result.setFont(list.getFont()); result.setBorder(cellHasFocus || isSelected ? SELECTED_BORDER : EMPTY_BORDER); result.setForeground(Color.DARK_GRAY); result.setBackground(Color.WHITE); if (wlr.record.getLevel() == Level.SEVERE) { result.setBackground(Color.RED); result.setForeground(Color.WHITE); } if (wlr.record.getLevel() == Level.WARNING) { result.setForeground(Color.RED); } if (wlr.record.getLevel() == Level.FINE || wlr.record.getLevel() == Level.FINER || wlr.record.getLevel() == Level.FINEST) { result.setForeground(DARK_GREEN); } final Object[] parameters = wlr.record.getParameters(); if (parameters != null) { for (final Object parameter : parameters) { if (parameter == null) { continue; } if (parameter instanceof Color) { result.setForeground((Color) parameter); } else if (parameter instanceof Font) { result.setFont((Font) parameter); } } } return result; } } /** * Wrap the log records so we can control the copy paste text (via * #toString) */ private class WrappedLogRecord { public final LogRecord record; public final String formatted; public WrappedLogRecord(final LogRecord record) { this.record = record; formatted = LogTextArea.formatter.format(record); } @Override public String toString() { return LogTextArea.copyPasteFormatter.format(record); } } }