0

I am building a method in an image viewer application that will create a slideshow- it will go to the next image in sequence after a certain period of time has passed. I want this to happen to infinity unless the user presses the 'esc' key. Key bindings seem to be the correct solution, but I can't wrap my head around using them. This is my code so far:

private void slideshow(){
    JOptionPane.showMessageDialog(null, "Press 'esc' to stop the slideshow", "Slideshow", JOptionPane.INFORMATION_MESSAGE, slideshow);

    // sets the action listener that the timer triggers
    ActionListener slideshowPerformer = new ActionListener() {
        public void actionPerformed( ActionEvent event )
        {
            //goes to the next image every x seconds
            nextFile();
        }
    };

    Timer slideshowTimer = new Timer(slideshowTime, slideshowPerformer);

    while(true){
        //label is a JLabel
        label.getInputMap().//the key binding code
    }
}

I'm open to alternate solutions, though I'm already using a MouseListener to browse through the images.

  • Possible duplicate https://stackoverflow.com/questions/21969954/how-to-detect-a-key-press-in-java#21970006 – Kevin Dec 02 '17 at 01:31
  • The `Action` associated with the key binding should stop the `Timer` – MadProgrammer Dec 02 '17 at 02:01
  • @Kevin FYI `KeyListener`s are very rarely a good choice and generally introduce more issues then they solve. Swing relies heavy on the key bindings API to solve the issues that `KeyListener` suffers from – MadProgrammer Dec 02 '17 at 02:13
  • You should NOT be using a while loop. For animation you use a Swing Timer. When the Timer fires you go to the next image. When you want the animation to stop you stop the Timer. – camickr Dec 02 '17 at 02:37

3 Answers3

2

Swing, like most UI frameworks, is event driven. Swing, like most UI frameworks, is also single threaded and not thread safe.

This means three things...

  1. You should never perform long running or blocking operations from within the context of the Event Dispatching Thread (like endless loops)
  2. You should not update the state of the UI from outside the context of the EDT
  3. You should use some kind of observer pattern to take action when some kind of state changes.

Since you're already using a Swing Timer and have mentioned using Key Bindings, you're already on the right track, the link you're missing is, the Action associated with the key binding should stop the Timer.

Or, as demonstrated in this example, the Action should notify some other interested party (i.e. a observer), that the Action has been triggered and the observer should take appropriate action.

import com.sun.glass.events.KeyEvent;
import java.awt.Dimension;
import java.awt.EventQueue;
import java.awt.GridBagLayout;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import javax.swing.AbstractAction;
import javax.swing.ActionMap;
import javax.swing.InputMap;
import javax.swing.JFrame;
import javax.swing.JLabel;
import javax.swing.JPanel;
import javax.swing.KeyStroke;
import javax.swing.Timer;
import javax.swing.UIManager;
import javax.swing.UnsupportedLookAndFeelException;

public class Test {

    public static void main(String[] args) {
        new Test();
    }

    public Test() {
        EventQueue.invokeLater(new Runnable() {
            @Override
            public void run() {
                try {
                    UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName());
                } catch (ClassNotFoundException | InstantiationException | IllegalAccessException | UnsupportedLookAndFeelException ex) {
                    ex.printStackTrace();
                }

                JFrame frame = new JFrame("Testing");
                frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
                frame.add(new TestPane());
                frame.pack();
                frame.setLocationRelativeTo(null);
                frame.setVisible(true);
            }
        });
    }

    public interface Engine {
        public void stop();
        public void start();
    }

    public class TestPane extends JPanel implements Engine {

        private Timer timer;
        private JLabel label = new JLabel("Waiting");

        public TestPane() {
            setLayout(new GridBagLayout());
            add(label);
            timer = new Timer(1000, new ActionListener() {
                private int counter = 0;
                @Override
                public void actionPerformed(ActionEvent e) {
                    counter++;
                    label.setText(Integer.toString(counter));
                }
            });
            InputMap inputMap = getInputMap(WHEN_IN_FOCUSED_WINDOW);
            ActionMap actionMap = getActionMap();

            inputMap.put(KeyStroke.getKeyStroke(KeyEvent.VK_ESCAPE, 0), "stop");
            actionMap.put("stop", new StopAction(this));

            start();
        }

        @Override
        public Dimension getPreferredSize() {
            return new Dimension(200, 200);
        }

        @Override
        public void stop() {
            timer.stop();
            label.setText("Stopped");
        }

        @Override
        public void start() {
            timer.start();
        }

    }

    public class StopAction extends AbstractAction {

        private Engine engine;

        public StopAction(Engine engine) {
            this.engine = engine;
        }

        @Override
        public void actionPerformed(ActionEvent e) {
            engine.stop();
        }

    }

}
MadProgrammer
  • 343,457
  • 22
  • 230
  • 366
  • Thanks! I ended up going in a slightly different direction, but that was only possible thanks to the help from you guys! I got rid of the while loop (duh! I've worked with swing timers before, not sure why I thought I'd need a loop for that!), and `Actions` were definitely instrumental. – Duncan Adkins Dec 02 '17 at 02:47
  • 1
    @DuncanAdkins: you should accept this answer as it put you along the correct track. – Hovercraft Full Of Eels Dec 02 '17 at 03:19
0

Thanks for all the help! I totally brain farted with the while loop and forgot how to use Swing timers for a hot minute. This ended up being my final, working code:

    ActionListener stopAction = new ActionListener(){
        public void actionPerformed(ActionEvent e){
            slideshowTimer.stop();
        }
    };

    // listens for the esc key to stop the slideshow
    label.getRootPane().registerKeyboardAction(stopAction,
        KeyStroke.getKeyStroke(KeyEvent.VK_ESCAPE, 0),
        JComponent.WHEN_IN_FOCUSED_WINDOW);

    slideshowTimer.start();
-2

I think a better solution would be to start the timer at 0, and then enter a while loop and increment it by one every iteration. Then modulo it by a number where everytime the modulo equals 0, go to next frame. And this is running forever, until an action listener is listening for the esc key to be pressed down in which you break from the loop.

All in all, I think just by using some lower level ideas, it will make your problem easier to solve.

  • 3
    I don't see the point of having both a `Timer` and a loop - since Swing is single threaded and not thread safe, you're just adding additional issue that need to be solved, which are already solved using just the `Timer` itself to trigger the movement. In the case of the OP's problem, simply having the Key Binding trigger some action that stops the `Timer` and performs what ever other action that needs to be done would be the best solution - IMHO – MadProgrammer Dec 02 '17 at 02:16