import javax.swing.*;
import javax.swing.border.EmptyBorder;
import javax.swing.event.AncestorEvent;
import javax.swing.event.AncestorListener;
import java.awt.*;
import java.util.Random;
import java.util.TimerTask;

public class AWTEventMulticaster_UITest extends JFrame {
    public static void main(String[] args) {
        setupEventDispatchThreadMonitor();
        SwingUtilities.invokeLater(() -> {
            AWTEventMulticaster_UITest t = new AWTEventMulticaster_UITest();
            t.pack();
            t.setVisible(true);
        });
    }

    /**
     * Setup a java.util.Timer that pings the event dispatch thread every 100ms. Each ping expects to be
     * able to execute a Runnable on the EDT within 100 ms. This logs periods of unresponsiveness to System.out
     */
    private static void setupEventDispatchThreadMonitor() {
        java.util.Timer timer = new java.util.Timer();
        long PING_INTERVAL = 100;
        timer.scheduleAtFixedRate(new TimerTask() {
            long unresponsiveStartTime = -1;

            @Override
            public void run() {
                final long startTime = System.currentTimeMillis();
                SwingUtilities.invokeLater(() -> {
                    long t = System.currentTimeMillis();
                    long elapsed = t - startTime;
                    if (elapsed > 100) {
                        if (unresponsiveStartTime == -1)
                            unresponsiveStartTime = startTime;
                    } else if (unresponsiveStartTime != -1) {
                        // we just recovered; the UI is responsive again

                        long z = t - unresponsiveStartTime - PING_INTERVAL;
                        if (z > 0)
                            System.out.println("The event dispatch thread was unresponsive for approx " + z + " millis");

                        unresponsiveStartTime = -1;
                    }
                });
            }
        }, PING_INTERVAL, PING_INTERVAL);
    }

    Random random = new Random(0);

    /**
     * This is a colored square with one JCheckBox in each corner.
     * Toggling any checkbox will hide and reshow the window's content pane. The
     * end result is not visually noticeable to the user, but this generates a lot
     * of events. In some cases this generates a StackOverflowError
     */
    class Cell extends JPanel {
        Cell() {
            setBackground(new Color(random.nextInt(0xffffff)));
            setPreferredSize(new Dimension(80, 80));
            setLayout(new GridBagLayout());
            add(createCheckBox(), new GridBagConstraints(0, 0, 1, 1, 1, 1, GridBagConstraints.CENTER, GridBagConstraints.NONE, new Insets(0,0,0,0), 0, 0));
            add(createCheckBox(), new GridBagConstraints(1, 0, 1, 1, 1, 1, GridBagConstraints.CENTER, GridBagConstraints.NONE, new Insets(0,0,0,0), 0, 0));
            add(createCheckBox(), new GridBagConstraints(0, 1, 1, 1, 1, 1, GridBagConstraints.CENTER, GridBagConstraints.NONE, new Insets(0,0,0,0), 0, 0));
            add(createCheckBox(), new GridBagConstraints(1, 1, 1, 1, 1, 1, GridBagConstraints.CENTER, GridBagConstraints.NONE, new Insets(0,0,0,0), 0, 0));
        }

        private JCheckBox createCheckBox() {
            JCheckBox cb = new JCheckBox();
            // on Mac you get an AncestorListener in the AquaButtonCheckBoxUI, but for the sake of this test
            // let's make sure you get one no matter which L&F you're using:
            if (cb.getAncestorListeners().length == 0) {
                cb.addAncestorListener(new AncestorListener() {
                    @Override
                    public void ancestorAdded(AncestorEvent event) {}

                    @Override
                    public void ancestorRemoved(AncestorEvent event) {}

                    @Override
                    public void ancestorMoved(AncestorEvent event) {}
                });
            }
            cb.addActionListener(e -> {
                System.out.println("hide and reshow content pane");
                getContentPane().setVisible(false);
                getContentPane().setVisible(true);
            });
            return cb;
        }
    }

    public AWTEventMulticaster_UITest() {
        int CELL_COUNT = 2_000;
        JEditorPane instructions = new JEditorPane();
        instructions.setText("This window contains " + (CELL_COUNT * 4) + " checkboxes. It demonstrates two problems:\n\n" +
                "1. When you toggle any checkbox this window's content pane toggles to hidden and shown. This can throw StackOverflowErrors, because each button attached an AncestorListener via the AWTEventMulticaster to an ancestor component. The AWTEventMulticaster defines a two-node tree, and recursing through that tree can be expensive.\n\n" +
                "2. When you use a mouse wheel to scroll over the scrollpane the event dispatch thread can remain unresponsive for several seconds. (\"Several\" can mean anywhere from 3-30.)");
        instructions.setBorder(new EmptyBorder(5,5,5,5));
        getContentPane().setLayout(new BorderLayout());
        getContentPane().add(instructions, BorderLayout.NORTH);

        JPanel p = new JPanel(new GridBagLayout());
        getContentPane().add(new JScrollPane(p), BorderLayout.CENTER);

        GridBagConstraints c = new GridBagConstraints();
        c.gridx = 0; c.gridy = 0; c.weightx = 1; c.weighty = 1; c.insets = new Insets(3,3,3,3);
        for (int a = 0; a < CELL_COUNT; a++) {
            p.add(new Cell(), c);
            c.gridx++;
            if (c.gridx == 10) {
                c.gridx = 0;
                c.gridy++;
            }
        }
    }
} 