Search code examples
javaswingconcurrency

Wait for long-running operation and show popup


Is it possible to wait for a method (say METHOD1) to finish, but if it is running longer than X secs, call another method until METHOD1 returns?

Some pseudocode:

method1();
startCountdown(1000); // time in millis
while (method1() still running) {
    method2(); // shows a popup with spinner (Swing/AWT)
}

I guess, it must be done with concurrency, but I am not used to concurrent programming. So, I have no idea how to start.

The UI framework used is Swing/AWT.


Solution

  • So, the basic idea would be to use a combination of a SwingWorker and a Swing Timer.

    The idea is if the Timer triggers before the SwingWorker is DONE, you execute some other workflow, otherwise you stop the Timer, for example...

    import java.awt.Dimension;
    import java.awt.EventQueue;
    import java.awt.GridBagConstraints;
    import java.awt.GridBagLayout;
    import java.awt.event.ActionEvent;
    import java.awt.event.ActionListener;
    import java.beans.PropertyChangeEvent;
    import java.beans.PropertyChangeListener;
    import javax.swing.JButton;
    import javax.swing.JFrame;
    import javax.swing.JLabel;
    import javax.swing.JPanel;
    import javax.swing.SwingWorker;
    import javax.swing.Timer;
    
    public class Test {
    
        public static void main(String[] args) {
            new Test();
        }
    
        public Test() {
            EventQueue.invokeLater(new Runnable() {
                @Override
                public void run() {
                    JFrame frame = new JFrame();
                    frame.add(new TestPane());
                    frame.pack();
                    frame.setLocationRelativeTo(null);
                    frame.setVisible(true);
                }
            });
        }
    
        public class TestPane extends JPanel {
    
            private JLabel label;
            private JButton startButton;
    
            boolean hasCompleted = false;
    
            public TestPane() {
                setLayout(new GridBagLayout());
                GridBagConstraints gbc = new GridBagConstraints();
                gbc.gridwidth = GridBagConstraints.REMAINDER;
    
                label = new JLabel("Waiting for you");
                startButton = new JButton("Start");
    
                add(label, gbc);
                add(startButton, gbc);
    
                startButton.addActionListener(new ActionListener() {
                    @Override
                    public void actionPerformed(ActionEvent e) {
                        startButton.setEnabled(false);
                        startWork();
                    }
                });
            }
    
            @Override
            public Dimension getPreferredSize() {
                return new Dimension(400, 400);
            }
    
            protected void startWork() {
                label.setText("Something wicked this way comes");
    
                // You could build an isoloated workflow, which allowed you to pass
                // three targets, the thing to be executed, the thing to be 
                // executed if time run over and the thing to be executed when
                // the task completed (all via a single interface),
                // but, you get the idea
                Timer timer = new Timer(2000, new ActionListener() {
                    @Override
                    public void actionPerformed(ActionEvent e) {
                        if (hasCompleted) {
                            return;
                        }
                        label.setText("Wickedness is a bit slow today");
                    }
                });
                timer.setRepeats(false);
    
                SomeLongRunningOperation worker = new SomeLongRunningOperation();
                worker.addPropertyChangeListener(new PropertyChangeListener() {
                    @Override
                    public void propertyChange(PropertyChangeEvent evt) {
                        switch (worker.getState()) {
                            case DONE:
                                hasCompleted = true;
                                timer.stop();
                                label.setText("All is done");
                                startButton.setEnabled(true);
                                break;
                        }
                    }
                });
                worker.execute();
                timer.start();
            }
    
        }
    
        public class SomeLongRunningOperation extends SwingWorker<Void, Void> {
    
            @Override
            protected Void doInBackground() throws Exception {
                Thread.sleep(5000);
                return null;
            }
    
        }
    }
    

    Play around with the timings to see what different effects you get.

    Why use a SwingWorker? Because it has it's own state callbacks, which makes it easier to deal with

    As I said in my comments, you could distill the workflow down into a re-usable concept, something like...

    public class TimedTask<V> {
        
        public static interface Task<V> {
            public V execute() throws Exception;
        }
        
        public static interface TimedTaskListener<V> extends EventListener {
            public void taskIsTakingLongThenExepected(TimedTask task);
            public void taskDidComplete(TimedTask task, V value);
        }
        
        private Task<V> task;
        private TimedTaskListener<V> listener;
        
        private V value;
        
        private int timeOut;
        private Timer timer;
        private SwingWorker<V, Void> worker;
        private boolean hasCompleted = false;
    
        public TimedTask(int timeOut, Task<V> task, TimedTaskListener<V> listener) {
            this.task = task;
            this.listener = listener;
            this.timeOut = timeOut;
        }
    
        public V getValue() {
            return value;
        }
    
        public int getTimeOut() {
            return timeOut;
        }
    
        protected Task<V> getTask() {
            return task;
        }
    
        protected TimedTaskListener<V> getListener() {
            return listener;
        }
        
        public void execute() {
            if (timer != null || worker != null) {
                return;
            }
            
            hasCompleted = false;
            worker = new SwingWorker<V, Void>() {
                @Override
                protected V doInBackground() throws Exception {
                    value = task.execute();
                    return value;
                }
            };
            worker.addPropertyChangeListener(new PropertyChangeListener() {
                @Override
                public void propertyChange(PropertyChangeEvent evt) {
                    switch (worker.getState()) {
                        case DONE:
                            hasCompleted = true;
                            timer.stop();
                            getListener().taskDidComplete(TimedTask.this, value);
                            break;
                    }
                }
            });
            
            timer = new Timer(getTimeOut(), new ActionListener() {
                @Override
                public void actionPerformed(ActionEvent e) {
                    if (hasCompleted) {
                        return;
                    }
                    getListener().taskIsTakingLongThenExepected(TimedTask.this);
                }
            });
            timer.setRepeats(false);
    
            worker.execute();
            timer.start();
        }
        
    }
    

    And then you could replace the startWork method in the first example with something like...

    protected void startWork() {
        label.setText("Something wicked this way comes");
        TimedTask.Task<Void> task = new TimedTask.Task<Void>() {
            @Override
            public Void execute() throws Exception {
                Thread.sleep(5000);
                return null;
            }
        };
        TimedTask<Void> timedTask = new TimedTask(2000, task, new TimedTask.TimedTaskListener<Void>() {
            @Override
            public void taskIsTakingLongThenExepected(TimedTask task) {
                label.setText("Wickedness is taking it's sweet time");
            }
    
            @Override
            public void taskDidComplete(TimedTask task, Void value) {
                label.setText("Wickedness has arrived");
                startButton.setEnabled(true);
            }
        });
        timedTask.execute();
    }