GuiLogPanel.java

package cern.accsoft.steering.jmad.gui.panels;

import javax.swing.*;
import javax.swing.text.BadLocationException;
import javax.swing.text.Document;
import javax.swing.text.Style;
import javax.swing.text.StyleConstants;
import java.awt.*;
import java.util.Map;
import java.util.Optional;
import java.util.concurrent.atomic.AtomicBoolean;

import cern.accsoft.steering.jmad.gui.executor.ActiveJobsEvent;
import com.google.common.collect.ImmutableMap;
import org.apache.log4j.Appender;
import org.apache.log4j.AppenderSkeleton;
import org.apache.log4j.Level;
import org.apache.log4j.PatternLayout;
import org.apache.log4j.spi.LoggingEvent;
import org.springframework.context.event.EventListener;

/**
 * Panel that includes a logger appender for displaying the logs in the GUI
 */
public class GuiLogPanel extends JPanel {
    private static final String SHOW_LOGS = "Show logs";
    private static final String HIDE_LOGS = "Hide logs";
    private static final int EXPANDED_HEIGHT = 300;
    private static final int CLOSED_HEIGHT = 0;
    private static final Color DEFAULT_BACKGROUND_COLOR = Color.WHITE;
    private final GuiLogAppender guiLogAppender;
    private final AtomicBoolean isExpanded = new AtomicBoolean(false);
    private final JLabel activeJobsLabel;

    public GuiLogPanel() {
        setLayout(new BorderLayout());

        JTextPane fullLogText = new JTextPane();
        fullLogText.setEditable(false);
        JTextField lastEventText = new JTextField();
        lastEventText.setEditable(false);

        JScrollPane fullLogPane = new JScrollPane(fullLogText);

        JButton showLogsButton = new JButton(SHOW_LOGS);
        showLogsButton.setPreferredSize(new Dimension(120, showLogsButton.getPreferredSize().height));
        showLogsButton.addActionListener(c -> {
            if (isExpanded.get()) {
                setHeight(fullLogPane, CLOSED_HEIGHT);
                showLogsButton.setText(SHOW_LOGS);
            } else {
                setHeight(fullLogPane, EXPANDED_HEIGHT);
                showLogsButton.setText(HIDE_LOGS);
            }
            isExpanded.set(!isExpanded.get());
            JFrame frame = (JFrame) SwingUtilities.getAncestorOfClass(JFrame.class, GuiLogPanel.this);
            frame.pack();
        });

        setHeight(fullLogPane, CLOSED_HEIGHT);

        activeJobsLabel = new JLabel();
        activeJobsLabel.setPreferredSize(new Dimension(120, activeJobsLabel.getPreferredSize().height));
        activeJobsLabel.setBackground(DEFAULT_BACKGROUND_COLOR);

        JPanel southBar = new JPanel();
        southBar.setLayout(new BoxLayout(southBar, BoxLayout.X_AXIS));
        southBar.setBackground(DEFAULT_BACKGROUND_COLOR);
        southBar.add(lastEventText);
        southBar.add(activeJobsLabel);
        southBar.add(showLogsButton);

        add(fullLogPane, BorderLayout.NORTH);
        add(southBar, BorderLayout.SOUTH);

        guiLogAppender = new GuiLogAppender(lastEventText, fullLogText);
    }

    public void init() {
        setActiveJobText(0);
    }

    @EventListener
    public void activeJobsCountChanged(ActiveJobsEvent e) {
        SwingUtilities.invokeLater(() -> setActiveJobText(e.getActiveJobsCount()));
    }

    private void setActiveJobText(int activeJobs) {
        activeJobsLabel.setText(formatActiveJobsCount(activeJobs));
        activeJobsLabel.setForeground(backgroundForActiveJobsCount(activeJobs));
    }

    private static Color backgroundForActiveJobsCount(int activeJobs) {
        if (activeJobs == 0) {
            return Color.GRAY;
        }
        return Color.BLUE.brighter();
    }

    private static String formatActiveJobsCount(int count) {
        if (count == 0) {
            return "No active jobs";
        }

        return String.format("Active jobs: %2d", count);
    }

    private static void setHeight(JScrollPane fullLogPane, int closedHeight) {
        fullLogPane.setPreferredSize(new Dimension(fullLogPane.getWidth(), closedHeight));
        fullLogPane.setSize(new Dimension(fullLogPane.getWidth(), closedHeight));
    }

    public Appender getGuiLogAppender() {
        return guiLogAppender;
    }

    public static class GuiLogAppender extends AppenderSkeleton {
        private static final Map<Level, Color> LEVEL_STATUS_COLOR = ImmutableMap.of( //
                Level.ERROR, Color.RED, //
                Level.INFO, Color.GREEN, //
                Level.WARN, Color.ORANGE);
        private static final Map<Level, Color> LEVEL_TEXT_COLOR = ImmutableMap.of( //
                Level.ERROR, Color.RED, //
                Level.INFO, Color.BLUE, //
                Level.WARN, Color.ORANGE.darker());

        private static final int BACKGROUND_RESET_TIMEOUT_MS = 4000;

        private final JTextField lastEventPane;
        private final JTextPane fullEventPane;
        private final Timer lastEventBackgroundResetTime;

        private final Map<Level, Style> levelStyles;
        private final Style defaultStyle;

        GuiLogAppender(JTextField lastEventPane, JTextPane fullEventPane) {
            super(true);

            this.lastEventPane = lastEventPane;
            this.fullEventPane = fullEventPane;

            ImmutableMap.Builder<Level, Style> levelStyleBuilder = ImmutableMap.builder();
            LEVEL_TEXT_COLOR.forEach((level, color) -> {
                Style style = fullEventPane.addStyle(level.toString(), null);
                StyleConstants.setForeground(style, color);
                levelStyleBuilder.put(level, style);
            });
            this.levelStyles = levelStyleBuilder.build();
            Style style = fullEventPane.addStyle("DEFAULT", null);
            StyleConstants.setForeground(style, Color.BLACK);
            this.defaultStyle = style;

            lastEventPane.setBorder(BorderFactory.createEmptyBorder());

            setThreshold(Level.INFO);
            setLayout(new PatternLayout("%d{ABSOLUTE} %-5p [%t]: %m %n"));

            lastEventBackgroundResetTime = new Timer(BACKGROUND_RESET_TIMEOUT_MS,
                    a -> lastEventPane.setBackground(DEFAULT_BACKGROUND_COLOR));
        }

        @Override
        protected void append(LoggingEvent event) {
            String msg = layout.format(event);
            String exceptionInfo = Optional.ofNullable(event.getThrowableStrRep()) //
                    .map(lines -> String.join("\n", lines)) //
                    .orElse("");
            Level level = event.getLevel();
            /* It's in theory possible that the logs messages are printed not in order. */
            /* It's ok since they are just informative and the overhead would be a bit too much */
            SwingUtilities.invokeLater(() -> {
                lastEventBackgroundResetTime.restart();
                lastEventPane.setText(msg);
                Document document = fullEventPane.getDocument();
                try {
                    document.insertString(document.getLength(), msg + exceptionInfo,
                            levelStyles.getOrDefault(level, defaultStyle));
                } catch (BadLocationException e) {
                    e.printStackTrace(); /* on purpose, do not use logger to prevent infinite loops! */
                }
                lastEventPane.setBackground(LEVEL_STATUS_COLOR.getOrDefault(level, DEFAULT_BACKGROUND_COLOR));
            });
        }

        @Override
        public void close() {

        }

        @Override
        public boolean requiresLayout() {
            return false;
        }

    }

}