0

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.

Micha Pringle
  • 123
  • 2
  • 8
  • This first time the code is executed the GUI isn't even visible yet, so I would guess no component has focus to the setCursor() method does nothing. I would guess you should should create an make the frame visible first before you start executing other code to get the user input. – camickr May 11 '20 at 23:31
  • @camickr that sounds reasonable. I do have setVisible(true); and repaint(); in my init method which is called first thing. I guess the other question is can your theory explain why removing that prompt causes the code to work as expected (except I want the prompt obviously). – Micha Pringle May 11 '20 at 23:47
  • maybe this is related? https://stackoverflow.com/questions/32929557/incorrect-cursor-when-outside-of-modal-jdialog – Micha Pringle May 12 '20 at 00:03
  • Does this answer your question? [Swing GUI doesn't update during data processing](https://stackoverflow.com/questions/24084102/swing-gui-doesnt-update-during-data-processing) – Charlie May 12 '20 at 19:52
  • The long running task is executed after you change the cursor but still inside the event dispatch thread; you need to return from the method that changes the cursor before the cursor change takes effect. You need to move the maze calculation to another thread then, when it's done, you use SwingUtilities.invokeLater to change the cursor back. – Charlie May 12 '20 at 19:54
  • @Charlie Thanks for the suggestion, I will try that out. How does that explain the bit where I remove the prompting window, and everything works as expected? – Micha Pringle May 12 '20 at 23:24
  • I suggest you look at [Concurrency in Swing](https://docs.oracle.com/javase/tutorial/uiswing/concurrency/index.html) – Abra May 13 '20 at 03:55

1 Answers1

1

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

Charlie
  • 8,530
  • 2
  • 55
  • 53
  • thanks for the answer. i will try that out, and some variations. I have been trying the other suggestions, but no luck so far. I still plan to read the full swing tutorial. I also thought of trying the glass pane, but it feels like it is a bandaid that is not addressing the root cause (i.e. my lousy Swing code). – Micha Pringle May 13 '20 at 17:27
  • The trouble you might run into is that you probably want to block interaction with the panel/frame while the maze generates, which sounds like you want to 'disable' it, but swing wont render the cursor of a component if it's disabled, which is where the glass pane comes in; it offers another layer to change the cusor while the components 'show through' – Charlie May 13 '20 at 21:22
  • Ah, that makes good sense, and you are right - while the maze is generated, the user is expected to sit back and relax, so to speak. I will have time to try out your suggestion on Friday, and I will see how it turns out. I really want to say thanks again for providing actual example code, that will save me a lot of time trying out 30 variations of what I think what was meant. BTW, i did check before I made the post, everything is happening on the EventDispachThread. – Micha Pringle May 14 '20 at 20:07
  • @MichaPringle any luck? – Charlie Jun 02 '20 at 06:51
  • believe it or not, I still have not had a chance to try it out. I will as soon as I can. – Micha Pringle Jun 03 '20 at 16:47
  • Sorry for the enormous delay. I finally have been able to come back to the code. I tried running without modification and it I find it works exactly as expected now. The only difference I can think of is that the last time I ran the code on Java 9, but since then that has been entirely cleared from my system and only Java 11 remains. In any case, as I recall from all the reading I did, your answer is the recommended way. – Micha Pringle Aug 15 '21 at 21:25
  • The problem seemed to have disappeared with Java 11, but actually, it just took some time to manifest again. Since adding @Charile's solution however, everything works as expected, and of course the U/I is nice and snappy. I thought you would like to know that your suggestion fixed my issue. Many thanks. I used the same idea for the save function as well, since larger mazes can create 80 MB files, and take a while to save, which is too long without a mouse spinner. – Micha Pringle Nov 07 '21 at 19:03