Micha Pringle
Micha Pringle

Reputation: 133

Java swing: Why does the mouse cursor not update for the first paint?

I have written some Java Swing code that collects some input, does some calculations, and then displays the result.

Expected result:

  1. For every user input:
  2. User goes to File -> New -> Enters some data.
  3. Mouse cursor turns into spinner as calculations are computed.
  4. Result is displayed.
  5. Mouse cursor returns to default.

Actual result:

  1. For first user input:
  2. User goes to File -> New -> Enters some data.
  3. Mouse cursor remains unchanged as calculations are computed.
  4. Result is displayed.
  5. For each subsequent usage, the result is the expected result described above.

I am pretty new to Swing, and I have been trying to get to the bottom of this without much luck. I have narrowed down the error to a specific line of the code. If I comment out the line JOptionPane.showConfirmDialog(null, null, "Enter new maze parameters", JOptionPane.OK_CANCEL_OPTION); in the code, then the spinner appears the first time through as expected. If I leave the line in, then I have the problem as described.

Please note that I have heavily trimmed this sample down to eliminate a lot of the U/I flow that is in the actual program, but this sample does break as described. I was not able to simplify my sample code further.

The simplified code

package ca.pringle.maze.ui;

import java.awt.BorderLayout;
import java.awt.Cursor;
import java.awt.Dimension;
import java.awt.event.ActionListener;
import java.awt.event.KeyEvent;
import java.util.Set;

import javax.swing.JFrame;
import javax.swing.JMenu;
import javax.swing.JMenuBar;
import javax.swing.JMenuItem;
import javax.swing.JOptionPane;
import javax.swing.JScrollPane;

import ca.pringle.maze.logic.Edge;
import ca.pringle.maze.logic.MazeConfig;
import ca.pringle.maze.logic.MazeMaker;
import ca.pringle.maze.logic.PathFinder;
import ca.pringle.maze.util.Pair;

public final class MazeDrawer extends JFrame {

    private final MazePanel mazePanel;

    public MazeDrawer() {
        mazePanel = new MazePanel();
    }

    public void init() {
        final JScrollPane scrollPane = new JScrollPane(mazePanel);
        final JMenuBar menuBar = createMenuBar((actionEvent) -> generateNewMaze());
        setJMenuBar(menuBar);

        getContentPane().setLayout(new BorderLayout());
        getContentPane().add(scrollPane, BorderLayout.CENTER);

        setSize(100, 100);
        setVisible(true);
        repaint();
    }

    void generateNewMaze() {
        // if I comment out  this line below, then the spinner appears on the first run as expected.
        // if I leave this line in, then the spinner does not appear on the first run, only on subsequent runs
        JOptionPane.showConfirmDialog(null, null, "Enter new maze parameters", JOptionPane.OK_CANCEL_OPTION);

        final MazeConfig mazeConfig = new MazeConfig(1000, 1000, 1);

        final MazeMaker mazeMaker = new MazeMaker(mazeConfig);
        final PanelDimensions panelDimensions = new PanelDimensions(mazeConfig.getRows(), mazeConfig.getColumns(), 15, 15);

        setCursor(Cursor.getPredefinedCursor(Cursor.WAIT_CURSOR));

        // the two lines below can take a long time depending on the inputs. I tried removing
        // them and replacing with a sleep to simulate and simplify the problem, but no luck,
        // so I left them in, even if I did not include the code.
        final Set<Edge> edges = mazeMaker.generateUndirectedMazeEdges();
        final Pair<Integer, Integer> startAndEndNodes = new PathFinder().findLongestPath(edges, mazeConfig);
        mazePanel.update(edges, startAndEndNodes, panelDimensions);
        mazePanel.setPreferredSize(new Dimension(panelDimensions.panelWidth, panelDimensions.panelHeight));
        mazePanel.repaint();

        setSize(Math.min(1200, panelDimensions.panelWidth + 11), Math.min(700, panelDimensions.panelHeight + 53));
        setCursor(Cursor.getPredefinedCursor(Cursor.DEFAULT_CURSOR));
    }

    private JMenuBar createMenuBar(final ActionListener menuListener) {

        final JMenuBar bar = new JMenuBar();

        final JMenu fileMenu = new JMenu("File");
        fileMenu.setMnemonic(KeyEvent.VK_F);

        final JMenuItem newMenuItem = new JMenuItem("New", KeyEvent.VK_N);
        newMenuItem.addActionListener(menuListener);
        fileMenu.add(newMenuItem);
        bar.add(fileMenu);

        return bar;
    }
}

Any help is appreciated. Please assume I am a complete Swing noob, so a more specific answer/explanation is more likely to help than a high level one that assumes I know something about Swing. I will read any suggested documentation of course.

If there are any problems or clarifications needed, please let me know, I am happy to update the post as many times as required.

Upvotes: 0

Views: 498

Answers (1)

Charlie
Charlie

Reputation: 9108

I suspect your init() and generateNewMaze() are being called from another thread and giving unpredictable results. You should never call swing code or manipulate swing objects from outside the EventDispachThread. If you aren't in fact calling from another thread, you don't need the outer invokelater wrapper.

Here's a better way to do what you are trying to do:

void generateNewMaze() {
    SwingUtilities.invokeLater(() -> {
        JOptionPane.showConfirmDialog(null, null, "Enter new maze parameters", JOptionPane.OK_CANCEL_OPTION);

        setCursor(Cursor.getPredefinedCursor(Cursor.WAIT_CURSOR));
        mazePanel.setEnabled(false);

        new Thread(() -> {
            try {
                // Do the long running maze task; this sleep is to simulate that task
                Thread.sleep(5000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            SwingUtilities.invokeLater(() -> {
                setCursor(Cursor.getPredefinedCursor(Cursor.DEFAULT_CURSOR));
                mazePanel.setEnabled(true);
            });
        }).start();
    });
}

You might also consider using the Glass pane: Wait cursor and disable java application

Upvotes: 1

Related Questions