Stimpson Cat
Stimpson Cat

Reputation: 1508

Java Swing show a BusyIndicator during time consuming task

My problem seems to arise from the way Swing is using threads internally. I want to show a busy indicator when the application does a time consuming task, which i call a Spinner. I have a Swing JDialog and adapted the code from the Oracle Tutorial

How do I fix the problem?

I have got a startSpinner()- and a stopSpinner()- method. They are working perfectly. I got 2 JButtons that show or hide it. But the problem is, when I call the startSpinner() method, while perfoming a task that runs for about 5 seconds, the spinner does not show up. I think i must deal with SwingUtilities.invokeLater. But i do not have experience with it. This my method for starting the display of the spinner.

StartSpinner method:

    private void startSpinner() {
    SwingUtilities.invokeLater(new Runnable() {
        @Override
        public void run() {
            layerUI.start();
            if (!stopper.isRunning()) {
                stopper.start();
                setTitle("Working...");
            }
        }
    });
}

Stop Spinner method:

public void stopSpinner() {
    logger.info("stopSpinner");
    layerUI.stop();
}  

WaitLayerUI class:

package view;

import java.awt.AlphaComposite;
import java.awt.BasicStroke;
import java.awt.Color;
import java.awt.Composite;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.RenderingHints;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.beans.PropertyChangeEvent;

import javax.swing.JComponent;
import javax.swing.JLayer;
import javax.swing.JPanel;
import javax.swing.Timer;
import javax.swing.plaf.LayerUI;

@SuppressWarnings("serial")
class WaitLayerUI extends LayerUI<JPanel> implements ActionListener {
private boolean mIsRunning;
private boolean mIsFadingOut;
private Timer mTimer;

private int mAngle;
private int mFadeCount;
private int mFadeLimit = 15;

@Override
public void paint(Graphics g, JComponent c) {
    int w = c.getWidth();
    int h = c.getHeight();

    // Paint the view.
    super.paint(g, c);

    if (!mIsRunning) {
        return;
    }

    Graphics2D g2 = (Graphics2D) g.create();

    float fade = (float) mFadeCount / (float) mFadeLimit;
    // Gray it out.
    Composite urComposite = g2.getComposite();
    g2.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC_OVER, .5f * fade));
    g2.fillRect(0, 0, w, h);
    g2.setComposite(urComposite);

    // Paint the wait indicator.
    int s = Math.min(w, h) / 5;
    int cx = w / 2;
    int cy = h / 2;
    g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
    g2.setStroke(new BasicStroke(s / 4, BasicStroke.CAP_ROUND, BasicStroke.JOIN_ROUND));
    g2.setPaint(Color.white);
    g2.rotate(Math.PI * mAngle / 180, cx, cy);
    for (int i = 0; i < 12; i++) {
        float scale = (11.0f - i) / 11.0f;
        g2.drawLine(cx + s, cy, cx + s * 2, cy);
        g2.rotate(-Math.PI / 6, cx, cy);
        g2.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC_OVER, scale * fade));
    }

    g2.dispose();
}

@Override
public void actionPerformed(ActionEvent e) {
    if (mIsRunning) {
        firePropertyChange("tick", 0, 1);
        mAngle += 3;
        if (mAngle >= 360) {
            mAngle = 0;
        }
        if (mIsFadingOut) {
            if (--mFadeCount == 0) {
                mIsRunning = false;
                mTimer.stop();
            }
        } else if (mFadeCount < mFadeLimit) {
            mFadeCount++;
        }
    }
}

public void start() {
    if (mIsRunning) {
        return;
    }

    // Run a thread for animation.
    mIsRunning = true;
    mIsFadingOut = false;
    mFadeCount = 0;
    int fps = 24;
    int tick = 1000 / fps;
    mTimer = new Timer(tick, this);
    mTimer.start();
}

public void stop() {
    mIsFadingOut = true;
}

@Override
public void applyPropertyChange(PropertyChangeEvent pce, JLayer l) {
    if ("tick".equals(pce.getPropertyName())) {
        l.repaint();
    }
}
}

I am using the code from the tutorial, except the main method, maybe this is the reason for the problem. The solution should be like this:

private void save(){
startSpinner();
performTimeConsumingSave();
stopSpinner();
}

Currently the spinner does not show up at all. But it shows up, when I show a JOptionPane (which pauses the current thread). I also tried Thread.sleep(100) but that was no good idea.

Upvotes: 0

Views: 2306

Answers (1)

Stimpson Cat
Stimpson Cat

Reputation: 1508

Thanks to Michal's comment and this post How do I use SwingWorker in Java? I was able to solve the issue. The potenitally long running method performTimeConsumingSave() is now called by a SwingWorker. I did like the tutorial advised. So the SwingWorkers method doInBackground() is now called from the ActionListener's actionPerformed method. Instead of calling save() directly from EDT, the call to save() is done by the SwingWorker. To determine whether the save was sucessfull, I am inspecting the retval. Now the spinner is showing up and the system is doing the long time consuming task (save()) simultaneously. Here is the PostWorker code:

    class PostWorker extends SwingWorker<Integer, Integer> {
    @Override
    protected Integer doInBackground() throws Exception {
        // Do a time-consuming task.
        starteSpinner();
        int status = save();
        stopSpinner();
        return status;
    }


    @Override
    protected void done() {
        try {
            if (get()== SAVE_OK) {
                JOptionPane.showMessageDialog(PostAusDetailDialog.this, "Mail saved.");
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

Upvotes: 1

Related Questions