Lab #10: Threads

Part 0: The "Event Dispatch" Thread

Consider the following program. Copy the code into files, compile and run this program. Clicking on the button toggles between the text LOVE and HATE. If you run it with argument 0, you can type one of the colors green, red, blue or cyan into the terminal window and it changes the color of the text. If you run it with argument 1, you need to click on the "mystery" button in order to be able to enter a color into the terminal.

L10a.java

import javax.swing.*;
import java.awt.*;
import java.awt.event.*;

public class L10a {
  public static void main(String[] args) {
    if( args.length == 0 ) {
      System.out.println("Run with argument 0 or 1!");
      System.exit(0);
    }
    boolean flag = args[0].equals("1");

    JLabel label = new JLabel(" LOVE ");
    label.setForeground(Color.RED);

    JButton b1 = new JButton("click to toggle");
    b1.addActionListener(new Toggler(label));

    JButton b2 = new JButton("mystery");

    if( flag )
      b2.addActionListener(new Mystery(label));

    JFrame f = new JFrame();
    f.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
    f.add(label, BorderLayout.WEST);
    f.add(b1, BorderLayout.CENTER);
    f.add(b2, BorderLayout.EAST);
    f.pack();
    f.setVisible(true);

    if( !flag ) {
      while (true) {
        CChange.changeColor(label);
      }
    }
  }
}
CChange.java

import javax.swing.*;
import java.awt.*;

public class CChange {
  public static void changeColor(JLabel label) {
    System.out.print("new color: ");
    String s = System.console().readLine();
    Color  c = label.getForeground();

    if( s.equals("red") )
      c = Color.RED;
    else if( s.equals("blue") )
      c = Color.BLUE;
    else if( s.equals("green") )
      c = Color.GREEN;
    else if( s.equals("cyan") )
      c = Color.CYAN;
    else
      System.out.println("Unknown color!");

    label.setForeground(c);
  }
}
Toggler.java

import javax.swing.*;
import java.awt.event.*;

public class Toggler implements ActionListener {
  private JLabel label;
  public Toggler(JLabel label) {
    this.label = label;
  }

  public void actionPerformed(ActionEvent e) {
    label.setText(label.getText().equals(" LOVE ") ? " HATE " : " LOVE ");
  }
}
Mystery.java

import javax.swing.*;
import java.awt.event.*;

public class Mystery implements ActionListener {
  private JLabel label;
  public Mystery(JLabel label) {
    this.label = label;
  }

  public void actionPerformed(ActionEvent e) {
    CChange.changeColor(label);
  }
}
  1. Explore the following: run the program with argument 0, and stop typing halfway through a color name, and go up and click "toggle" a few times. Now run with argument 1, click on the "mystery:" button, and then do the same thing: stop typing halfway through a color name, and go up and click "toggle" a few times. What's the difference in the behavior you observe?
  2. Now add the following statement as the first line of the changeColor method

    System.out.println("Thread ID: " + Thread.currentThread().getId());
    recompile, and then try the same two runs you did in the previous step. What's the difference?

What's going on here? Well it turns out that all GUI programs are inherently multi-threaded. There's the main thread (ID 1), which any program has. There's also a thread called the "event dispatch thread", and that's the thread on which all calls to GUI "listeners" (ActionListeners, WindowListeners, etc) occur. Somewhere on the event dispatch thread's call-stack is a record for a call to a function that is essentially an infinite loop that waits for the next GUI action, and then starts calling the necessary functions to respond - a process that eventually results in listeners being called. After all those calls are made, the stack eventually goes back down to the function with the infinite loop ... which waits around for the next GUI action.

So now think about our example above: with argument 0, the call to CChange.changeColor() was made in main(), which means a new record for the CChange.changeColor() call was made on the Thread 1 call stack. Thus, the event dispatch thread was left free to wait for events and, ultimately, to call the Toggler actionPerformed() method. On the other hand, with argument 1, the call to CChange.changeColor() was made in Mystery's actionPerformed() method, which means a new record on the event dispatch thread's call stack. Thus, the event dispatch thread couldn't do anything until the call to CChange.changeColor() returned, which required the user to complete typing the color name and press enter. In other words, the GUI was locked up until we finished!

This is a huge issue in programming GUIs with Java's Swing API: you can't execute any method on the event dispatch thread that doesn't return really quickly. Otherwise you'll lock up the GUI. So what do you do if a GUI event like a mouse click is supposed to initiate some long-running function call? Well, you create a new Thread for it to run in!

Part 1: Fixing the Mystery Button

Your job is to fix the Mystery Button so that the GUI doesn't lock up while the user dithers over which color to enter. Concretely, this means that when you press the Mystery Button, the toggle button and the window's "x" for closing still work, even if the user hasn't yet typed a color and pressed enter. That means, spawning a new thread for the CChange.changeColor() to execute in.

You'll want to keep running with mode: java L10a 1

Note: If you think that you've got it solved, you might want to look into the issue of what happens if you go crazy and double-click or triple-click the Mystery Button. It probably doesn't quite work the way you'd like. Look at Thread's isAlive() method. With a few tweaks, you can make it so that after a click of the Mystery Button, all subsequent clicks are ignored until a color is entered in response to the first one.
Fixing this issue now directly translates to preventing similar errors in Part 2 below.

Think that you've got it? Demo this part to your instructor!
If not during this lab, then demo it anytime during the week, or (worst case) at the beginning of the next lab period.

Part 2: Making a timer

Now let's take what we've learned and produce a simple, and yet useful tool: a GUI timer. The program should be called L10Timer, i.e. you should run it like this:

java L10Timer
The timer looks like this when it is launched

but if you click it starts counting down in second increments, replacing the word DONE with the number of seconds remaining.

When it gets down to zero, the word DONE appears again. The user can type a number into the text-field to set the duration of the timer. When the program is first launched, the value in the text-field should be 10.

Note: if, when the button is pressed, what's in the text field is not a valid positive integer, the label should be changed to ERROR.

Here are a few useful tidbits for you:

  1. The call Thread.sleep(1000); will put the thread in which it is executed to sleep for one second. Look at the sleep() API documentation carefully ... there might be exceptions!
  2. If you keep changing the text of the JLabel, its width changes, and that can be a bit disconcerting as the timer counts down. You can set the JLabel's "preferred size" like this:

    JLabel lab = new JLabel("foo");
    lab.setPreferredSize(new Dimension(60,15));
  3. It's also nice to be able to control the size of the JTextField. In this case, the constructor just takes an int argument that is the number of characters wide you'd like the text field to be, e.g.

    JTextField tf = new JTextField(10);
    Note too that you can call setText on a JTextField object to set the text that appears, even though the user might go change it later.

Think you're done? Test your solution thoroughly!

Demo your fully tested solution to your instructor!
If not during this lab, then demo it at the beginning of the next lab period.

Extra Credit (5 points)

If you package your timer into a new class that you derive from JPanel, it's trivial to create a window with multiple independent timers, like this:


Submission

Submit all the java files you need to compile and run your L10Timer program with the following terminal command:

~/bin/submit -c=IC211 -p=Lab10 *.java
Do NOT submit any unused .java files, any .class files, or files that do not compile!