Search code examples
javastringmultithreadingjtextarea

Java Threads not working correctly with JTextArea


In Java, I am creating a login/register program at the moment (hoping to expand it in the future) but have run into a problem.

I am trying to make text appear across the screen in an animated type writer styled fashion.

When I run the class's main method, the text appears just fine, when I run it in a keylistener, it fails and waits for text to appear, THEN updates the frame so I can see it.

Source Code Below:

Login.class

public class Login implements KeyListener {

int BACK_WIDTH = java.awt.Toolkit.getDefaultToolkit().getScreenSize().width;
int BACK_HEIGHT = java.awt.Toolkit.getDefaultToolkit().getScreenSize().height;

JFrame back_frame = new JFrame();

LoginTerminal login = new LoginTerminal();

public Login() {
    back_frame.setSize(BACK_WIDTH, BACK_HEIGHT);
    back_frame.setLocation(0, 0);
    back_frame.getContentPane().setBackground(Color.BLACK);
    back_frame.setUndecorated(true);
    //back_frame.setVisible(true);

    back_frame.addKeyListener(this);
    login.addKeyListener(this);
    login.setLocationRelativeTo(null);
    login.setVisible(true);
    login.field.addKeyListener(this);

    login.slowPrint("Please login to continue...\n"
               + "Type 'help' for more information.\n");
}

public void keyPressed(KeyEvent e) {
    int i = e.getKeyCode();

    if(i == KeyEvent.VK_ESCAPE) {
        System.exit(0);
    }

    if(i == KeyEvent.VK_ENTER) {
        login.slowPrint("\nCommands\n"
                 + "-----------\n"
                 + "register\n"
                 + "login\n");
    }
}

public void keyReleased(KeyEvent e) {}

public void keyTyped(KeyEvent e) {}

}

LoginTerminal.class

public class LoginTerminal implements KeyListener {

CustomFrame frame = new CustomFrame(Types.TERMINAL);

JTextArea log = new JTextArea();
public JTextField field = new JTextField();

public void setVisible(boolean bool) {
    frame.setVisible(bool);
}

public void addKeyListener(KeyListener listener) {
    frame.addKeyListener(listener);
}

public void slowPrint(String str) {
    for(int i = 0; i < str.length(); i++) {
        log.append("" + str.charAt(i));

        try {
            Thread.sleep(50);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        if (i == str.length() - 1) {
            log.append("\n");
        }
    }
}

public void setLocation(int x, int y) {
    frame.setLocation(x, y);
}

public void setLocationRelativeTo(Component c) {
    frame.setLocationRelativeTo(c);
}

public LoginTerminal() {
    try {

        InputStream is = LoginTerminal.class.getResourceAsStream("/fonts/dungeon.TTF");
        Font font = Font.createFont(Font.TRUETYPE_FONT, is);
        font = font.deriveFont(Font.PLAIN, 10);

        frame.add(field);
        frame.add(log);

        log.setBackground(Color.BLACK);
        log.setForeground(Color.WHITE);
        log.setWrapStyleWord(true);
        log.setLineWrap(true);
        log.setBounds(4, 20 + 4, frame.getWidth() - 8, frame.getHeight() - 50);
        log.setFont(font);
        log.setEditable(false);
        log.setCaretColor(Color.BLACK);

        field.setBackground(Color.BLACK);
        field.setForeground(Color.WHITE);
        field.setBounds(2, frame.getHeight() - 23, frame.getWidth() - 5, 20);
        field.setFont(font);
        field.setCaretColor(Color.BLACK);
        field.addKeyListener(this);
        field.setText("  >  ");

    } catch (FontFormatException e) {
        e.printStackTrace();
    } catch (IOException e) {
        e.printStackTrace();
    }
}

public void dumpToLog() {
    log.append(field.getText() + "\n");
    field.setText("  >  ");
}

public void keyPressed(KeyEvent e) {
    int i = e.getKeyCode();

    if(i == KeyEvent.VK_ENTER && field.isFocusOwner()) {
        if(field.getText().equals("  >  HELP") || field.getText().equals("  >  help")) {
            dumpToLog();


        } else {
            dumpToLog();
        }
    }


    if(!field.getText().startsWith("  >  ")) {
        field.setText("  >  ");
    }
}

public void keyReleased(KeyEvent e) {}
public void keyTyped(KeyEvent e) {}

}

Main.class

public class Main {

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

}

My problem is here:

public Login() {
    login.slowPrint("Please login to continue...\n"
               + "Type 'help' for more information.\n");
}

When, I run that, it works as expected.

Whereas below in the same class (Login.class)

public void keyPressed(KeyEvent e) {
    int i = e.getKeyCode();

    if(i == KeyEvent.VK_ENTER) {
        login.slowPrint("\nCommands\n"
                 + "-----------\n"
                 + "register\n"
                 + "login\n");
    }
}

It freezes and waits for finish.

I think it may be the Thread.sleep(50); in LoginTerminal.class as the title states due to the fact that that is what triggers the typing animation.

Hope I made myself clear here. Thanks for the help all!

EDIT

This is what causes the Timer error...

public void timerPrint(String text) {
Timer timer = new Timer(50, new ActionListener() {
    @Override
    public void actionPerformed(ActionEvent e) {
        if (index < text.length() - 1 && index >= 0) {
            String newChar = Character.toString(text.charAt(index));
            textArea.append(newChar);
            index++;
        } else {
            textArea.setText(null);
            index = 0;
           // You could stop the timer here...
        }
   }
});
timer.start();
}

The constructor Timer(int, new ActionListener(){}) is undefined

EDIT EDIT, Whole class:

package com.finn.frametypes;

import java.awt.Color;
import java.awt.Component;
import java.awt.Font;
import java.awt.FontFormatException;
import java.awt.event.KeyEvent;
import java.awt.event.KeyListener;
import java.awt.event.ActionEvent;
import java.io.IOException;
import java.io.InputStream;

import javax.swing.JTextArea;
import javax.swing.JTextField;
import javax.swing.Timer;

import com.finn.gui.CustomFrame;
import com.finn.gui.Types;

public class LoginTerminal implements KeyListener {

CustomFrame frame = new CustomFrame(Types.TERMINAL);

JTextArea log = new JTextArea();
public JTextField field = new JTextField();

public void setVisible(boolean bool) {
    frame.setVisible(bool);
}

public void addKeyListener(KeyListener listener) {
    frame.addKeyListener(listener);
}
public void timerPrint(String text) {
Timer timer = new Timer(50, new ActionListener() {
    @Override
    public void actionPerformed(ActionEvent e) {
        if (index < text.length() - 1 && index >= 0) {
            String newChar = Character.toString(text.charAt(index));
            textArea.append(newChar);
            index++;
        } else {
            textArea.setText(null);
            index = 0;
           // You could stop the timer here...
        }
   }
});
timer.start();
}

public void slowPrint(String str) {
    for(int i = 0; i < str.length(); i++) {
        log.append("" + str.charAt(i));

        try {
            Thread.sleep(50);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        if (i == str.length() - 1) {
            log.append("\n");
        }
    }
}

public void setLocation(int x, int y) {
    frame.setLocation(x, y);
}

public void setLocationRelativeTo(Component c) {
    frame.setLocationRelativeTo(c);
}

public LoginTerminal() {
    try {

        InputStream is = LoginTerminal.class.getResourceAsStream("/fonts/dungeon.TTF");
        Font font = Font.createFont(Font.TRUETYPE_FONT, is);
        font = font.deriveFont(Font.PLAIN, 10);

        frame.add(field);
        frame.add(log);

        log.setBackground(Color.BLACK);
        log.setForeground(Color.WHITE);
        log.setWrapStyleWord(true);
        log.setLineWrap(true);
        log.setBounds(4, 20 + 4, frame.getWidth() - 8, frame.getHeight() - 50);
        log.setFont(font);
        log.setEditable(false);
        log.setCaretColor(Color.BLACK);

        field.setBackground(Color.BLACK);
        field.setForeground(Color.WHITE);
        field.setBounds(2, frame.getHeight() - 23, frame.getWidth() - 5, 20);
        field.setFont(font);
        field.setCaretColor(Color.BLACK);
        field.addKeyListener(this);
        field.setText("  >  ");

    } catch (FontFormatException e) {
        e.printStackTrace();
    } catch (IOException e) {
        e.printStackTrace();
    }
}

public void dumpToLog() {
    log.append(field.getText() + "\n");
    field.setText("  >  ");
}

public void keyPressed(KeyEvent e) {
    int i = e.getKeyCode();

    if(i == KeyEvent.VK_ENTER && field.isFocusOwner()) {
        if(field.getText().equals("  >  HELP") || field.getText().equals("  >  help")) {
            dumpToLog();


        } else {
            dumpToLog();
        }
    }


    if(!field.getText().startsWith("  >  ")) {
        field.setText("  >  ");
    }
}

public void keyReleased(KeyEvent e) {}
public void keyTyped(KeyEvent e) {}

}

ANSWER

If anyone reads this in the future and wants to use slowprinting in Java, use the below:

int index;

public void timerPrint(final String text) {
    Timer timer = new Timer(50, new ActionListener() {
        public void actionPerformed(ActionEvent e) {
            if (index < text.length() - 1 && index >= 0) {
                String newChar = Character.toString(text.charAt(index));
                textarea.append(newChar);
                index++;
            } else {
                index = 0;
                ((Timer)e.getSource()).stop();
            }
       }
    });
    timer.start();
}

Solution

  • Swing is a single threaded framework, that is, anything that blocks the Event Dispatching Thread will prevent the UI from been updated or for your program to respond to new threads

    See Concurrency in Swing for more details.

    You should never perform any blocking (Thread.sleep) or long running processes from within the EDT.

    Swing is also NOT thread safe, this means, that you should never update the UI from outside the context of the EDT.

    This leaves you in a conundrum...

    Luckily, there are options. Probably the simplest solution for your case is to use a Swing Timer, which can be used to schedule a regular callback into the EDT, where you can perform updates

    See How to use Swing Timers for more details

    For example...

    Scrolling Text

    import java.awt.BorderLayout;
    import java.awt.EventQueue;
    import java.awt.event.ActionEvent;
    import java.awt.event.ActionListener;
    import javax.swing.JFrame;
    import javax.swing.JPanel;
    import javax.swing.JTextArea;
    import javax.swing.Timer;
    import javax.swing.UIManager;
    import javax.swing.UnsupportedLookAndFeelException;
    
    public class ScrollingText100 {
    
        public static void main(String[] args) {
            new ScrollingText100();
        }
    
        public ScrollingText100() {
            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 class TestPane extends JPanel {
    
            private String text;
            private int index;
            private JTextArea textArea;
    
            public TestPane() {            
                setLayout(new BorderLayout());
                textArea = new JTextArea(2, 40);
                add(textArea);
    
                text = "Please login to continue...\n" + "Type 'help' for more information.\n";
                Timer timer = new Timer(50, new ActionListener() {
                    @Override
                    public void actionPerformed(ActionEvent e) {
                        if (index < text.length() - 1 && index >= 0) {
                            String newChar = Character.toString(text.charAt(index));
                            textArea.append(newChar);
                            index++;
                        } else {
                            textArea.setText(null);
                            index = 0;
                            // You could stop the timer here...
                        }
                    }
                });
                timer.start();
            }
    
        }
    
    }
    

    Updated

    If I understand your requirements properly, something like this...

    import java.awt.BorderLayout;
    import java.awt.Color;
    import java.awt.EventQueue;
    import java.awt.event.ActionEvent;
    import java.awt.event.ActionListener;
    import javax.swing.JFrame;
    import javax.swing.JScrollPane;
    
    import javax.swing.JTextArea;
    import javax.swing.JTextField;
    import javax.swing.Timer;
    import javax.swing.UIManager;
    import javax.swing.UnsupportedLookAndFeelException;
    
    public class LoginTerminal {
    
        private JTextArea log = new JTextArea(20, 40);
        private JTextField field = new JTextField();
    
        private int index;
        private StringBuilder textToDisplay;
        private Timer timer;
    
        public static void main(String[] args) {
            new LoginTerminal();
        }
    
        public LoginTerminal() {
            EventQueue.invokeLater(new Runnable() {
                @Override
                public void run() {
                    try {
                        UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName());
                    } catch (ClassNotFoundException | InstantiationException | IllegalAccessException | UnsupportedLookAndFeelException ex) {
                        ex.printStackTrace();
                    }
    
                    textToDisplay = new StringBuilder(128);
                    timer = new Timer(50, new ActionListener() {
                        @Override
                        public void actionPerformed(ActionEvent e) {
                            if (textToDisplay.length() > 0) {
                                String newChar = Character.toString(textToDisplay.charAt(0));
                                textToDisplay.delete(0, 1);
                                log.append(newChar);
                            } else {
                                ((Timer) e.getSource()).stop();
                            }
                        }
                    });
    
                    JFrame frame = new JFrame("Testing");
                    frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
    
                    frame.add(field, BorderLayout.NORTH);
                    frame.add(new JScrollPane(log));
    
                    log.setBackground(Color.BLACK);
                    log.setForeground(Color.WHITE);
                    log.setWrapStyleWord(true);
                    log.setLineWrap(true);
                    log.setEditable(false);
                    log.setCaretColor(Color.BLACK);
    
                    field.setBackground(Color.BLACK);
                    field.setForeground(Color.WHITE);
                    field.setCaretColor(Color.BLACK);
                    field.addActionListener(new ActionListener() {
                        @Override
                        public void actionPerformed(ActionEvent e) {
                            if ("  >  HELP".equalsIgnoreCase(field.getText())) {
                                dumpToLog();
                            } else {
                                dumpToLog();
                            }
                        }
                    });
                    field.setText("  >  ");
    
                    frame.pack();
                    frame.setLocationRelativeTo(null);
                    frame.setVisible(true);
                }
            });
        }
    
        public void timerPrint(String text) {
    
            textToDisplay.append(text).append("\n");
            if (!timer.isRunning()) {
                timer.start();
            }
        }
    
        public void slowPrint(String str) {
            for (int i = 0; i < str.length(); i++) {
                log.append("" + str.charAt(i));
    
                try {
                    Thread.sleep(50);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
    
                if (i == str.length() - 1) {
                    log.append("\n");
                }
            }
        }
    
        public void dumpToLog() {
            timerPrint(field.getText());
        }
    
    }
    

    may be what you're actually look for.

    Note, KeyListeners aren't a good choice for JTextComponents, in this case an ActionListener is a better choice.