Home > Articles > Programming > Java

  • Print
  • + Share This
This chapter is from the book

User Interface Programming with Threads

In the following sections, we discuss threading issues that are of particular interest to user interface programming.

Threads and Swing

As we mentioned in the introduction, one of the reasons to use threads in your programs is to make your programs more responsive. When your program needs to do something time-consuming, then you should fire up another worker thread instead of blocking the user interface.

However, you have to be careful what you do in a worker thread because, perhaps surprisingly, Swing is not thread safe. That is, the majority of methods of Swing classes are not synchronized. If you try to manipulate user interface elements from multiple threads, then your user interface will become corrupted.

For example, run the test program whose code you will find at the end of this section. When you click on the "Bad" button, a new thread is started that edits a combo box, randomly adding and removing values.

class BadWorkerThread extends Thread
{ 
  public BadWorkerThread(JComboBox aCombo)
  { 
   combo = aCombo;
   generator = new Random();
  }

  public void run()
  { 
   try
   {
     while (!interrupted())
     { 
      int i = Math.abs(generator.nextInt());
      if (i % 2 == 0)
        combo.insertItemAt(new Integer(i), 0);
      else if (combo.getItemCount() > 0)
        combo.removeItemAt(i % combo.getItemCount());
      
      sleep(1);
     }
   }
   catch (InterruptedException exception) {}
  }

  private JComboBox combo;
  private Random generator;
}

Try it out. Click on the "Bad" button. If you start the program from a console window, you will see an occasional exception report in the console (see Figure 1–10).

Figure 1–10: Exception reports in the console

What is going on? When an element is inserted into the combo box, the combo box fires an event to update the display. Then, the display code springs into action, reading the current size of the combo box and preparing to display the values. But the worker thread keeps on going, which can occasionally result in a reduction of the count of the values in the combo box. The display code then thinks that there are more values in the model than there actually are, asks for nonexistent values, and triggers an ArrayIndexOutOfBounds exception.

This situation could have been avoided by locking the combo box object while displaying it. However, the designers of Swing decided not to expend any effort to make Swing thread safe, for two reasons. First, synchronization takes time, and nobody wanted to slow down Swing any further. More importantly, the Swing team checked out what experience other teams had with thread-safe user interface toolkits. What they found was not encouraging. When building a user interface toolkit, you want it to be extensible so that other programmers can add their own user interface components. But user interface programmers using thread-safe toolkits turned out to be confused by the demands for synchronization and tended to create components that were prone to deadlocks.

Therefore, when you use threads together with Swing, you have to follow a few simple rules. First, however, let's see what threads are present in a Swing program.

Every Java application starts with a main method that runs in the main thread. In a Swing program, the main method typically does the following:

  • First it calls a constructor that lays out components in a frame window;

  • then it invokes the show or setVisible method on the window.

When the first window is shown, a second thread is created, the event dispatch thread. All event notifications, such as calls to actionPerformed or paintComponent, run in the event dispatch thread. The main thread keeps running until the main method exits. Usually, of course, the main method exits immediately after displaying the frame window (see Figure 1–11). Other threads are running behind the scenes, such as the thread that posts events into the event queue, but those threads are invisible to the application programmer.

Figure 1–11: Threads in a Swing program

In a Swing application, essentially all code is contained in event handlers to respond to user interface and repaint requests. All that code runs on the event dispatch thread. Here are the rules that you need to follow.

  1. If an action takes a long time, fire up a new thread to do the work. If you take a long time in the event dispatch thread, the application seems "dead" since it cannot respond to any events.

  2. If an action can block on input or output, fire up a new thread to do the work. You don't want to freeze the user interface for the potentially indefinite time that a network connection is unresponsive.

  3. If you need to wait for a specific amount of time, don't sleep in the event dispatch thread. Instead, use a timer.

  4. The work that you do in your threads cannot touch the user interface. Read any information from the UI before you launch your threads, launch them, and then update the user interface from the event dispatching thread once the threads have completed.

The last rule is often called the single thread rule for Swing programming. There are a few exceptions to the single thread rule.

  1. A few Swing methods are thread safe. They are specially marked in the API documentation with the sentence "This method is thread safe, although most Swing methods are not." The most useful among these thread-safe methods are:

    JTextComponent.setText
    JTextArea.insert
    JTextArea.append
    JTextArea.replaceRange
  2. The following methods of the JComponent class can be called from any thread:

    repaint
    revalidate

    The repaint method schedules a repaint event. You use the revalidate method if the contents of a component have changed and the size and position of the component must be updated. The revalidate method marks the component's layout as invalid and schedules a layout event. (Just like paint events, layout events are coalesced. If there are multiple layout events in the event queue, the layout is only recomputed once.)

    NOTE

    We have used the repaint method many times in volume 1 of this book, but the revalidate method is less common. Its purpose is to force a layout of a component after the contents has changed. The traditional AWT has invalidate and validate methods to mark a component's layout as invalid and to force the layout of a component. For Swing components, you should simply call revalidate instead. (However, to force the layout of a JFrame, you still need to call validate—a JFrame is a Component but not a JComponent.)

  3. You can safely add and remove event listeners in any thread. Of course, the listener methods will be invoked in the event dispatching thread.

  4. You can construct components, set their properties, and add them into containers, as long as none of the components have been realized. A component has been realized if it can receive paint or validation events. This is the case as soon as the show, setVisible(true), or pack methods have been invoked on the component, or if the component has been added to a container that has been realized. Once a component has been realized, you can no longer manipulate it from another thread.

    In particular, you can create the GUI of an application in the main method before calling show, and you can create the GUI of an applet in the applet constructor or the init method.

These rules look complex, but they aren't actually difficult to follow. It is an easy matter to start a new thread to start a time-consuming process. Upon a user request, gather all the necessary information from the GUI, pass them to a thread, and start the thread.

public void actionPerformed(ActionEvent e)
{ 
  // gather data needed by thread
  MyThread t = new MyThread(data);
  t.start();
}

The difficult part is to update the GUI to indicate progress within your thread and to present the result of the work when your thread is finished. Remember that you can't touch any Swing components from your thread. For example, if you want to update a progress bar or a label text, then you can't simply set its value from your thread.

To solve this problem, there are two convenient utility methods that you can use in any thread to add arbitrary actions to the event queue. For example, suppose you want to periodically update a label "x% complete" in a thread, to indicate progress. You can't call label.setText from your thread, but you can use the invokeLater and invokeAndWait methods of the EventQueue class to have that call executed in the event dispatching thread.

NOTE

These methods are also available in the javax.swing.SwingUtilities class. If you use Swing with Java Development Kit (JDK) 1.1, you need to use that class—the methods were added to EventQueue in JDK 1.2.

Here is what you need to do. You place the Swing code into the run method of a class that implements the Runnable interface. Then, you create an object of that class and pass it to the static invokeLater or invokeAndWait method. For example, here is how you can update a label text. First, create the class with the run method.

public class LabelUpdater implements Runnable
{ 
  public LabelUpdater(JLabel aLabel, int aPercentage)
  { 
   label = aLabel;
   percentage = aPercentage;
  }
 
  public void run()
  { 
   label.setText(percentage + "% complete");
  }
}

Then, create an object and pass it to the invokeLater method.

Runnable updater = new LabelUpdater(label, percentage);
EventQueue.invokeLater(updater);

The invokeLater method returns immediately when the event is posted to the event queue. The run method is executed asynchronously. The invokeAndWait method waits until the run method has actually been executed. The EventQueue class handles the details of the synchronization. In the situation of updating a progress label, the invokeLater method is more appropriate. Users would rather have the worker thread make more progress than insist on the most precise display of the completed percentage.

To invoke code in the event dispatch thread, anonymous inner classes offer a useful shortcut. For example, the sample code given above can be simplified to the following cryptic, but shorter, command:

EventQueue.invokeLater(new 
  Runnable()
  { 
   public void run()
   { 
     label.setText(percentage + "% complete");
   }
  });

NOTE

The invokeLater and invokeAndWait methods use objects that implement the Runnable interface. You already saw how to construct new threads out of Runnable objects. However, in this case, the code of the run method executes in the event dispatching thread, not a new thread.

Example 1–7 demonstrates how to use the invokeLater method to safely modify the contents of a combo box. If you click on the "Good" button, a thread inserts and removes numbers. However, the actual modification takes place in the event dispatching thread.

Example 1–7: SwingThreadTest.java

	1.	import java.awt.*;
	2.	import java.awt.event.*;
	3.	import java.util.*;
	4.	import javax.swing.*;
	5.	
	6.	/**
	7.	  This program demonstrates that a thread that 
	8.	  runs in parallel with the event dispatch thread
	9.	  can cause errors in Swing components.
	10.	*/
	11.	public class SwingThreadTest
	12.	{ 
	13.	  public static void main(String[] args)
	14.	  { 
	15.	   SwingThreadFrame frame = new SwingThreadFrame();
	16.	   frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
	17.	   frame.show();
	18.	  }
	19.	}
	20.	
	21.	/**
	22.	  This frame has two buttons to fill a combo box from a
	23.	  separate thread. The "Good" button uses the event queue, 
	24.	  the "Bad" button modifies the combo box directly.
	25.	*/
	26.	class SwingThreadFrame extends JFrame
	27.	{ 
	28.	  public SwingThreadFrame()
	29.	  { 
	30.	   setTitle("SwingThread");
	31.	   setSize(WIDTH, HEIGHT);
	32.	
	33.	   final JComboBox combo = new JComboBox();
	34.	
	35.	   JPanel p = new JPanel();
	36.	   p.add(combo);
	37.	   getContentPane().add(p, BorderLayout.CENTER);
	38.	
	39.	   JButton b = new JButton("Good");
	40.	   b.addActionListener(new ActionListener()
	41.	     { 
	42.	      public void actionPerformed(ActionEvent event)
	43.	      { 
	44.	        combo.showPopup();
	45.	        new GoodWorkerThread(combo).start();
	46.	      }
	47.	     });
	48.	   p = new JPanel();
	49.	   p.add(b);
	50.	   b = new JButton("Bad");
	51.	   b.addActionListener(new ActionListener()
	52.	     { 
	53.	      public void actionPerformed(ActionEvent event)
	54.	      { 
	55.	        combo.showPopup();
	56.	        new BadWorkerThread(combo).start();
	57.	      }
	58.	     });
	59.	   p.add(b);
	60.	
	61.	   getContentPane().add(p, BorderLayout.NORTH);
	62.	  }
	63.	
	64.	  public static final int WIDTH = 450;
	65.	  public static final int HEIGHT = 300;
	66.	}
	67.	
	68.	/**
	69.	  This thread modifies a combo box by randomly adding
	70.	  and removing numbers. This can result in errors because
	71.	  the combo box is not synchronized and the event dispatch
	72.	  thread accesses the combo box to repaint it.
	73.	*/ 
	74.	class BadWorkerThread extends Thread
	75.	{ 
	76.	  public BadWorkerThread(JComboBox aCombo)
	77.	  { 
	78.	   combo = aCombo;
	79.	   generator = new Random();
	80.	  }
	81.	
	82.	  public void run()
	83.	  { 
	84.	   try
	85.	   {
	86.	     while (!interrupted())
	87.	     { 
	88.	      int i = Math.abs(generator.nextInt());
	89.	      if (i % 2 == 0)
	90.	        combo.insertItemAt(new Integer(i), 0);
	91.	      else if (combo.getItemCount() > 0)
	92.	        combo.removeItemAt(i % combo.getItemCount());
	93.	      
	94.	      sleep(1);
	95.	     }
	96.	   }
	97.	   catch (InterruptedException exception) {}
	98.	  }
	99.	
	100.	  private JComboBox combo;
	101.	  private Random generator;
	102.	}
	103.	
	104.	/**
	105.	  This thread modifies a combo box by randomly adding
	106.	  and removing numbers. In order to ensure that the
	107.	  combo box is not corrupted, the editing operations are
	108.	  forwarded to the event dispatch thread.
	109.	*/ 
	110.	class GoodWorkerThread extends Thread
	111.	{ 
	112.	  public GoodWorkerThread(JComboBox aCombo)
	113.	  { 
	114.	   combo = aCombo;
	115.	   generator = new Random();
	116.	  }
	117.	
	118.	  public void run()
	119.	  { 
	120.	   try
	121.	   {
	122.	     while (!interrupted())
	123.	     { 
	124.	      EventQueue.invokeLater(new 
	125.	        Runnable()
	126.	        { 
	127.	         public void run()
	128.	         { 
	129.	           int i = Math.abs(generator.nextInt());
	130.	           
	131.	           if (i % 2 == 0)
	132.	            combo.insertItemAt(new Integer(i), 0);
	133.	           else if (combo.getItemCount() > 0)
	134.	            combo.removeItemAt(i % combo.getItemCount());
	135.	         }
	136.	        });
	137.	      Thread.sleep(1); 
	138.	     }
	139.	   }
	140.	   catch (InterruptedException exception) {} 
	141.	  }
	142.	
	143.	  private JComboBox combo;
	144.	  private Random generator;
	145.	}

java.awt.EventQueue

  • static void invokeLater(Runnable runnable)
    Causes the run method of the runnable object to be executed in the event dispatch thread, after pending events have been processed.

  • static void invokeAndWait(Runnable runnable)
    Causes the run method of the runnable object to be executed in the event dispatch thread, after pending events have been processed. This call blocks until the run method has terminated.

Animation

In this section, we dissect one of the most common uses for threads in applets: animation. An animation sequence displays images, giving the viewer the illusion of motion. Each of the images in the sequence is called a frame. If the frames are complex, they should be rendered ahead of time—the computer running the applet may not have the horsepower to compute images fast enough for real-time animation.

You can put each frame in a separate image file or put all frames into one file. We do the latter. It makes the process of loading the image much easier. In our example, we use a file with 36 images of a rotating globe, courtesy of Silviu Marghescu of the University of Maryland. Figure 1–12 shows the first few frames.

Figure 1–12: This file has 36 images

The animation applet must first acquire all the frames. Then, it shows each of them in turn for a fixed time. To draw the i'th frame, we make a method call as follows:

g.drawImage(image, 0, - i * imageHeight 
  / imageCount, null);

Figure 1–13 shows the negative offset of the y-coordinate.

This offset causes the first frame to be well above the origin of the canvas. The top of the i'th frame becomes the top of the canvas. After a delay, we increment i and draw the next frame.

We use a MediaTracker object to load the image. Behind the scenes and transparent to the programmer, the addImage method fires up a new thread to acquire the image. Loading an image can be very slow, especially if the image has many frames or is located across the network. The waitForID call blocks until the image is fully loaded.

NOTE

Readers who are familiar with the SDK 1.4 image I/O package may wonder why we don't use ImageIO.read to read the image. That method creates a temporary file—an operation that is not legal for an applet in the sandbox.

Figure 1–13: Picking a frame from a strip of frames Once the image is loaded, we render one frame at a time. Our applet starts a single thread.

class Animation extends JApplet
{ 
		. . .
  private Thread runner = null;
}

You will see such a thread variable in many applets. Often, it is called kicker, and we once saw killer as the variable name. We think runner makes more sense, though.

First and foremost, we will use this thread to:

  • Start the animation when the user is watching the applet;

  • Stop the animation when the user has switched to a different page in the browser.

We do these tasks by creating the thread in the start method of the applet and by interrupting it in the stop method.

class Animation extends JApplet
{ 
  public void start()
  { 
   if (runner == null)
   { 
     runner = new 
      Thread()
      {
        public void run()
        {
         . . .
        }
       };
     runner.start();
   }
  }

  public void stop()
  { 
   runner.interrupt();
   runner = null;
  }
  . . .
}

Here is the run method. It simply loops, painting the screen, advancing the frame counter, and sleeping when it can.

public void run()
{ 
  try
  { 
   while (!Thread.interrupted())
   { 
     repaint();
     current = (current + 1) % imageCount;
     Thread.sleep(200);
   }
  }
  catch(InterruptedException e) {}
}

Finally, we implement another mechanism for stopping and restarting the animation. When you click the mouse on the applet window, the animation stops. When you click again, it restarts. Note that we use the thread variable, runner, as an indication whether the thread is currently running or not. Whenever the thread is terminated, the variable is set to null. This is a common idiom that you will find in many multithreaded applets.

public void init() 
{ 
  addMouseListener(new 
   MouseAdapter()
   { 
     public void mousePressed(MouseEvent evt)
     { 
      if (runner == null) 
        start();
      else 
        stop();
     }
   });

  . . .
}

The applet reads the name of the image and the number of frames in the strip from the param section in the HTML file.

<applet code=Animation.class width=100 height=100>
<param name=imagename value="globe.gif">
<param name=imagecount value="36">
</applet>

Example 1–8 is the code of the applet. Note that the start and stop methods start and stop the applet—they are not methods of the thread that is generated.

This animation applet is simplified to show you what goes on behind the scenes, and to help you understand other applets with a similar structure. If you are interested only in how to put a moving image on your web page, look instead at the Animator applet in the demo section of the JDK. That applet has many more options than ours, and it enables you to add sound.

Furthermore, as you will see in the next section, you really don't need to implement your own threads for a simple animation—you can just use a Swing timer.

Example 1–8: Animation.java

	1.	import java.awt.*;
	2.	import java.awt.image.*;
	3.	import java.awt.event.*;
	4.	import javax.swing.*;
	5.	import java.net.*;
	6.	
	7.	/**
	8.	  An applet that shows a rotating globe.
	9.	*/
	10.	public class Animation extends JApplet
	11.	{ 
	12.	  public void init()
	13.	  { 
	14.	   addMouseListener(new MouseAdapter()
	15.	     { 
	16.	      public void mousePressed(MouseEvent evt)
	17.	      { 
	18.	        if (runner == null)
	19.	         start();
	20.	        else
	21.	         stop();
	22.	      }
	23.	     });
	24.	
	25.	   try
	26.	   { 
	27.	     String imageName = getParameter("imagename");
	28.	     imageCount = 1;
	29.	     String param = getParameter("imagecount");
	30.	     if (param != null)
	31.	      imageCount = Integer.parseInt(param);
	32.	     current = 0;
	33.	     image = null;
	34.	     loadImage(new URL(getDocumentBase(), imageName));
	35.	   }
	36.	   catch (Exception e)
	37.	   { 
	38.	     showStatus("Error: " + e);
	39.	   }
	40.	  }
	41.	
	42.	  /**
	43.	   Loads an image.
	44.	   @param url the URL of the image file
	45.	  */
	46.	  public void loadImage(URL url)
	47.	   throws InterruptedException
	48.	     // thrown by MediaTracker.waitFor
	49.	  { 
	50.	   image = getImage(url);
	51.	   MediaTracker tracker = new MediaTracker(this);
	52.	   tracker.addImage(image, 0);
	53.	   tracker.waitForID(0);
	54.	   imageWidth = image.getWidth(null);
	55.	   imageHeight = image.getHeight(null);
	56.	   resize(imageWidth, imageHeight / imageCount);
	57.	  }
	58.	
	59.	  public void paint(Graphics g)
	60.	  { 
	61.	   if (image == null) return;
	62.	   g.drawImage(image, 0, - (imageHeight / imageCount)
	63.	     * current, null);
	64.	  }
	65.	
	66.	  public void start()
	67.	  { 
	68.	   runner = new 
	69.	     Thread()
	70.	     {
	71.	      public void run()
	72.	      { 
	73.	        try
	74.	        { 
	75.	         while (!Thread.interrupted())
	76.	         { 
	77.	           repaint();
	78.	           current = (current + 1) % imageCount;
	79.	           Thread.sleep(200);
	80.	         }
	81.	        }
	82.	        catch(InterruptedException e) {}
	83.	      }
	84.	     };
	85.	   runner.start();
	86.	   showStatus("Click to stop");
	87.	  }
	88.	
	89.	  public void stop()
	90.	  { 
	91.	   runner.interrupt();
	92.	   runner = null;
	93.	   showStatus("Click to restart");
	94.	  }
	95.	
	96.	  private Image image;
	97.	  private int current;
	98.	  private int imageCount;
	99.	  private int imageWidth;
	100.	  private int imageHeight;
	101.	  private Thread runner;
	102.	}

Timers

In many programming environments, you can set up timers. A timer alerts your program elements at regular intervals. For example, to display a clock in a window, the clock object must be notified once every second.

Swing has a built-in timer class that is easy to use. You construct a timer by supplying an object of a class that implements the ActionListener interface and the delay between timer alerts, in milliseconds.

Timer t = new Timer(1000, listener);

Call

t.start();

to start the timer. Then, the actionPerformed method of the listener class is called whenever a timer interval has elapsed. The actionPerformed method is automatically called on the event dispatch thread, not the timer thread, so that you can freely invoke Swing methods in the callback.

To stop the timer, call

t.stop();

Then the timer stops sending action events until you restart it.

NOTE

SDK 1.3 has an unrelated java.util.Timer class to schedule a TimerTask for later execution. The TimerTask class implements the Runnable interface and also supplies a cancel method to cancel the task. However, the java.util.Timer class has no provision for a periodic callback.

The example program at the end of this section puts the Swing timer to work. Figure 1–14 shows six different clocks.

Figure 1–14: Clock threads

Each clock is an instance of the ClockCanvas class. The constructor sets up the timer:

  public ClockCanvas(String tz)
  { 
   calendar = new GregorianCalendar(TimeZone.getTimeZone(tz));
   Timer t = new Timer(1000, new
     ActionListener()
     {
      public void actionPerformed(ActionEvent event)
      {
        calendar.setTime(new Date());
        repaint();
      }
     });
   t.start();
   . . .
  }

The actionPerformed method of the timer's anonymous action listener gets called approximately once per second. It calls new Date() to get the current time and repaints the clock.

As you can see, no thread programming is required at all in this case. The Swing timer takes care of the thread details. We could have used a timer for the animation of the preceding section as well.

You will find the complete code in Example 1–9.

Example 1–9: TimerTest.java

	1.	import java.awt.*;
	2.	import java.awt.event.*;
	3.	import javax.swing.*;
	4.	import java.util.*;
	5.	import javax.swing.Timer;
	6.	
	7.	/**
	8.	  This class shows a frame with several clocks that
	9.	  are updated by a timer thread.
	10.	*/
	11.	public class TimerTest
	12.	{ 
	13.	  public static void main(String[] args)
	14.	  { 
	15.	   TimerTestFrame frame = new TimerTestFrame();
	16.	   frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
	17.	   frame.show();
	18.	  }
	19.	}
	20.	
	21.	/**
	22.	  The frame holding the clocks.
	23.	*/
	24.	class TimerTestFrame extends JFrame
	25.	{ 
	26.	  public TimerTestFrame()
	27.	  { 
	28.	   setTitle("TimerTest");
	29.	   setSize(WIDTH, HEIGHT);
	30.	
	31.	   Container c = getContentPane();
	32.	   c.setLayout(new GridLayout(2, 3));
	33.	   c.add(new ClockCanvas("America/Los_Angeles"));
	34.	   c.add(new ClockCanvas("America/New_York"));
	35.	   c.add(new ClockCanvas("America/Caracas"));
	36.	   c.add(new ClockCanvas("Europe/Rome"));
	37.	   c.add(new ClockCanvas("Africa/Cairo"));
	38.	   c.add(new ClockCanvas("Asia/Taipei"));
	39.	  }
	40.	
	41.	  public static final int WIDTH = 450;
	42.	  public static final int HEIGHT = 300;
	43.	}
	44.	
	45.	/**
	46.	  The canvas to display a clock that is updated by a timer.
	47.	*/
	48.	class ClockCanvas extends JPanel
	49.	{ 
	50.	  /**
	51.	   Constructs a clock canvas.
	52.	   @param tz the time zone string
	53.	  */
	54.	  public ClockCanvas(String tz)
	55.	  { 
	56.	   zone = tz;
	57.	   calendar = new GregorianCalendar(TimeZone.getTimeZone(tz));
	58.	   Timer t = new Timer(1000, new
	59.	     ActionListener()
	60.	     {
	61.	      public void actionPerformed(ActionEvent event)
	62.	      {
	63.	        calendar.setTime(new Date());
	64.	        repaint();
	65.	      }
	66.	     });
	67.	   t.start();
	68.	   setSize(WIDTH, HEIGHT);
	69.	  }
	70.	
	71.	  public void paintComponent(Graphics g)
	72.	  { 
	73.	   super.paintComponent(g);
	74.	   g.drawOval(0, 0, 100, 100);
	75.	
	76.	   int seconds = calendar.get(Calendar.HOUR) * 60 * 60
	77.	     + calendar.get(Calendar.MINUTE) * 60
	78.	     + calendar.get(Calendar.SECOND);
	79.	   double hourAngle = 2 * Math.PI
	80.	     * (seconds - 3 * 60 * 60) / (12 * 60 * 60);
	81.	   double minuteAngle = 2 * Math.PI
	82.	     * (seconds - 15 * 60) / (60 * 60);
	83.	   double secondAngle = 2 * Math.PI
	84.	     * (seconds - 15) / 60;
	85.	   g.drawLine(50, 50, 50 + (int)(30
	86.	     * Math.cos(hourAngle)),
	87.	     50 + (int)(30 * Math.sin(hourAngle)));
	88.	   g.drawLine(50, 50, 50 + (int)(40
	89.	     * Math.cos(minuteAngle)),
	90.	     50 + (int)(40 * Math.sin(minuteAngle)));
	91.	   g.drawLine(50, 50, 50 + (int)(45
	92.	     * Math.cos(secondAngle)),
	93.	     50 + (int)(45 * Math.sin(secondAngle)));
	94.	   g.drawString(zone, 0, 115);
	95.	  }
	96.	
	97.	  private String zone;
	98.	  private GregorianCalendar calendar;
	99.	
	100.	  public static final int WIDTH = 125;
	101.	  public static final int HEIGHT = 125;
	102.	}

javax.swing.Timer

  • Timer(int delay, ActionListener listener)
    Creates a new timer that sends events to a listener.

Parameters:

delay

the delay, in milliseconds, between event notifications

 

listener

the action listener to be notified when the delay has elapsed

  • void start()
    Start the timer. After this call, the timer starts sending events to its action listener.

  • void stop()
    Stop the timer. After this call, the timer stops sending events to its action listener.

Progress Bars

A progress bar is a simple component—just a rectangle that is partially filled with color to indicate the progress of an operation. By default, progress is indicated by a string "n %". You can see a progress bar in the bottom right of Figure 1–15.

Figure 1–15: A progress bar

You construct a progress bar much as you construct a slider, by supplying the minimum and maximum value and an optional orientation:

progressBar = new JProgressBar(0, 1000);
progressBar = new JProgressBar(SwingConstants.VERTICAL, 0, 1000);

You can also set the minimum and maximum with the setMinimum and setMaximum methods.

Unlike a slider, the progress bar cannot be adjusted by the user. Your program needs to call setValue to update it.

If you call

progressBar.setStringPainted(true);

the progress bar computes the completion percentage and displays a string "n %". If you want to show a different string, you can supply it with the setString method:

if (progressBar.getValue() > 900)
  progressBar.setString("Almost Done");

The program in Example 1–10 shows a progress bar that monitors a simulated time-consuming activity.

The SimulatedActivity class implements a thread that increments a value current ten times per second. When it reaches a target value, the thread finishes. If you want to terminate the thread before it has reached its target, you should interrupt it.

class SimulatedActivity extends Thread
{ . . .
  public void run()
  { 
   try
   {
     while (current < target && !interrupted())
     {  
      sleep(100);
      current++;
     }
   }
   catch(InterruptedException e)
   { 
   }
  }

  int current;
  int target;
}

When you click on the "Start" button, a new SimulatedActivity thread is started. To update the progress bar, it would appear to be an easy matter for the simulated activity thread to make calls to the setValue method. But that is not thread safe. Recall that you should call Swing methods only from the event dispatch thread. In practice, it is also unrealistic. In general, a worker thread is not aware of the existence of the progress bar. Instead, the example program shows how to launch a timer that periodically polls the thread for a progress status and updates the progress bar.

CAUTION

If a worker thread is aware of a progress bar that monitors its progress, remember that it cannot set the progress bar value directly. To set the value in the event dispatch thread, the worker thread can use the SwingUtilities.invokeLater method.

Recall that a Swing timer calls the actionPerformed method of its listeners and that these calls occur in the event dispatch thread. That means it is safe to update Swing components in the timer callback. Here is the timer callback from the example program. The current value of the simulated activity is displayed both in the text area and the progress bar. If the end of the simulation has been reached, the timer is stopped and the "Start" button is reenabled.

public void actionPerformed(ActionEvent event)
{ 
  int current = activity.getCurrent();
  // show progress

  textArea.append(current + "\n");
  progressBar.setValue(current);
  // check if task is completed
  if (current == activity.getTarget())
  { 
   activityMonitor.stop();
   startButton.setEnabled(true);
  }
}

Example 1–10 shows the full program code.

NOTE

SDK 1.4 adds support for an indeterminate progress bar that shows an animation indicating some kind of progress, without giving an indication of the percentage of completion. That is the kind of progress bar that you see in your browser—it indicates that the browser is waiting for the server and has no idea how long the wait may be. To display the "indeterminate wait" animation, call the setIndeterminate method.

Example 1–10: ProgressBarTest.java

	1.	import java.awt.*;
	2.	import java.awt.event.*;
	3.	import java.util.*;
	4.	import javax.swing.*;
	5.	import javax.swing.event.*;
	6.	import javax.swing.Timer;
	7.	
	8.	/**
	9.	  This program demonstrates the use of a progress bar
	10.	  to monitor the progress of a thread.
	11.	*/
	12.	public class ProgressBarTest
	13.	{ 
	14.	  public static void main(String[] args)
	15.	  { 
	16.	   ProgressBarFrame frame = new ProgressBarFrame();
	17.	   frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
	18.	   frame.show();
	19.	  }
	20.	}
	21.	
	22.	/**
	23.	  A frame that contains a button to launch a simulated activity,
	24.	  a progress bar, and a text area for the activity output.
	25.	*/
	26.	class ProgressBarFrame extends JFrame
	27.	{ 
	28.	  public ProgressBarFrame()
	29.	  { 
	30.	   setTitle("ProgressBarTest");
	31.	   setSize(WIDTH, HEIGHT);
	32.	
	33.	   Container contentPane = getContentPane();
	34.	
	35.	   // this text area holds the activity output
	36.	   textArea = new JTextArea();
	37.	
	38.	   // set up panel with button and progress bar
	39.	
	40.	   JPanel panel = new JPanel();
	41.	   startButton = new JButton("Start");
	42.	   progressBar = new JProgressBar();
	43.	   progressBar.setStringPainted(true);
	44.	   panel.add(startButton);
	45.	   panel.add(progressBar);
	46.	   contentPane.add(new JScrollPane(textArea), 
	47.	     BorderLayout.CENTER);
	48.	   contentPane.add(panel, BorderLayout.SOUTH); 
	49.	
	50.	   // set up the button action
	51.	
	52.	   startButton.addActionListener(new 
	53.	     ActionListener()
	54.	     { 
	55.	      public void actionPerformed(ActionEvent event)
	56.	      { 
	57.	        progressBar.setMaximum(1000);
	58.	        activity = new SimulatedActivity(1000);
	59.	        activity.start();
	60.	        activityMonitor.start();
	61.	        startButton.setEnabled(false);
	62.	      }
	63.	     });
	64.	
	65.	
	66.	   // set up the timer action
	67.	
	68.	   activityMonitor = new Timer(500, new 
	69.	     ActionListener()
	70.	     { 
	71.	      public void actionPerformed(ActionEvent event)
	72.	      { 
	73.	        int current = activity.getCurrent();
	74.	        
	75.	        // show progress
	76.	        textArea.append(current + "\n");
	77.	        progressBar.setValue(current);
	78.	        
	79.	        // check if task is completed
	80.	        if (current == activity.getTarget())
	81.	        { 
	82.	         activityMonitor.stop();
	83.	         startButton.setEnabled(true);
	84.	        }
	85.	      }
	86.	     });
	87.	  }
	88.	
	89.	  private Timer activityMonitor;
	90.	  private JButton startButton;
	91.	  private JProgressBar progressBar;
	92.	  private JTextArea textArea;
	93.	  private SimulatedActivity activity;
	94.	
	95.	  public static final int WIDTH = 300;
	96.	  public static final int HEIGHT = 200;
	97.	}
	98.	
	99.	/**
	100.	  A simulated activity thread.
	101.	*/
	102.	class SimulatedActivity extends Thread
	103.	{ 
	104.	  /**
	105.	   Constructs the simulated activity thread object. The
	106.	   thread increments a counter from 0 to a given target.
	107.	   @param t the target value of the counter.
	108.	  */
	109.	  public SimulatedActivity(int t)
	110.	  { 
	111.	   current = 0;
	112.	   target = t;
	113.	  }
	114.	
	115.	  public int getTarget()
	116.	  { 
	117.	   return target;
	118.	  }
	119.	
	120.	  public int getCurrent()
	121.	  { 
	122.	   return current;
	123.	  }
	124.	
	125.	  public void run()
	126.	  { 
	127.	   try
	128.	   {
	129.	     while (current < target && !interrupted())
	130.	     {  
	131.	      sleep(100);
	132.	      current++;
	133.	     }
	134.	   }
	135.	   catch(InterruptedException e)
	136.	   { 
	137.	   }
	138.	  }
	139.	
	140.	  private int current;
	141.	  private int target;
	142.	}

Progress Monitors

A progress bar is a very simple component that can be placed inside a window. In contrast, a ProgressMonitor is a complete dialog box that contains a progress bar (see Figure 1–16). The dialog contains "OK" and "Cancel" buttons. If you click either, the monitor dialog is closed. In addition, your program can query whether the user has canceled the dialog and terminate the monitored action. (Note that the class name does not start with a "J".)

Figure 1–16: A progress monitor dialog

You construct a progress monitor by supplying the following:

  • The parent component over which the dialog should pop up;

  • An object (which should be a string, icon, or component) that is displayed on the dialog;

  • An optional note to display below the object;

  • The minimum and maximum values.

However, the progress monitor cannot measure progress or cancel an activity by itself. You still need to periodically set the progress value by calling the setProgress method. (This is the equivalent of the setValue method of the JProgressBar class.) As you update the progress value, you should also call the isCanceled method to see if the program user has clicked on the "Cancel" button.

When the monitored activity has concluded, you should call the close method to dismiss the dialog. You can reuse the same dialog by calling start again.

The example program looks very similar to that of the preceding section. We still need to launch a timer to watch over the progress of the simulated activity and update the progress monitor. Here is the timer callback.

public void actionPerformed(ActionEvent event)
{ 
  int current = activity.getCurrent();

  // show progress
  textArea.append(current + "\n");
  progressDialog.setProgress(current);

  // check if task is completed or canceled
  if (current == activity.getTarget()
   || progressDialog.isCanceled())
  { 
   activityMonitor.stop();
   progressDialog.close();
   activity.interrupt();
   startButton.setEnabled(true);
  }
}

Note that there are two conditions for termination. The activity might have completed, or the user might have canceled it. In each of these cases, we close down:

  • the timer that monitored the activity;

  • the progress dialog;

  • the activity itself (by interrupting the thread).

If you run the program in Example 1–11, you can observe an interesting feature of the progress monitor dialog. The dialog doesn't come up immediately. Instead, it waits a for a short interval to see if the activity has already been completed or is likely to complete in less time than it would take for the dialog to appear. You control the timing as follows. Use the setMillisToDecidePopup method to set the number of milliseconds to wait between the construction of the dialog object and the decision whether to show the pop-up at all. The default value is 500 milliseconds. The setMillisToPopup is the time that you estimate that the dialog needs to pop up. The Swing designers set this value to a default of 2 seconds. Clearly they were mindful of the fact that Swing dialogs don't always come up as snappily as we all would like. You should probably not touch this value.

Example 1–11 shows the progress monitor in action, again measuring the progress of a simulated activity. As you can see, the progress monitor is convenient to use and only requires that you periodically query the thread that you want to monitor.

Example 1–11: ProgressMonitorTest.java

	1.	import java.awt.*;
	2.	import java.awt.event.*;
	3.	import java.util.*;
	4.	import javax.swing.*;
	5.	import javax.swing.event.*;
	6.	import javax.swing.Timer;
	7.	
	8.	/**
	9.	  A program to test a progress monitor dialog.
	10.	*/
	11.	public class ProgressMonitorTest
	12.	{ 
	13.	  public static void main(String[] args)
	14.	  { 
	15.	   JFrame frame = new ProgressMonitorFrame();
	16.	   frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
	17.	   frame.show();
	18.	  }
	19.	}
	20.	
	21.	/**
	22.	  A frame that contains a button to launch a simulated activity
	23.	  and a text area for the activity output.
	24.	*/
	25.	class ProgressMonitorFrame extends JFrame
	26.	{ 
	27.	  public ProgressMonitorFrame()
	28.	  { 
	29.	   setTitle("ProgressMonitorTest");
	30.	   setSize(WIDTH, HEIGHT);
	31.	
	32.	   Container contentPane = getContentPane();
	33.	
	34.	   // this text area holds the activity output
	35.	   textArea = new JTextArea();
	36.	
	37.	   // set up a button panel
	38.	   JPanel panel = new JPanel();
	39.	   startButton = new JButton("Start");
	40.	   panel.add(startButton);
	41.	
	42.	   contentPane.add(new JScrollPane(textArea), 
	43.	     BorderLayout.CENTER);
	44.	   contentPane.add(panel, BorderLayout.SOUTH);
	45.	
	46.	   // set up the button action
	47.	
	48.	   startButton.addActionListener(new 
	49.	     ActionListener()
	50.	     { 
	51.	      public void actionPerformed(ActionEvent event)
	52.	      { 
	53.	        // start activity
	54.	        activity = new SimulatedActivity(1000);
	55.	        activity.start();
	56.	
	57.	        // launch progress dialog
	58.	        progressDialog = new ProgressMonitor(
	59.	         ProgressMonitorFrame.this,
	60.	         "Waiting for Simulated Activity",
	61.	         null, 0, activity.getTarget());
	62.	        
	63.	        // start timer
	64.	        activityMonitor.start();
	65.	        
	66.	        startButton.setEnabled(false);
	67.	      }
	68.	     });
	69.	
	70.	   // set up the timer action
	71.	
	72.	   activityMonitor = new Timer(500, new 
	73.	     ActionListener()
	74.	     { 
	75.	      public void actionPerformed(ActionEvent event)
	76.	      { 
	77.	        int current = activity.getCurrent();
	78.	
	79.	        // show progress
	80.	        textArea.append(current + "\n");
	81.	        progressDialog.setProgress(current);
	82.	        
	83.	        // check if task is completed or canceled
	84.	        if (current == activity.getTarget()
	85.	         || progressDialog.isCanceled())
	86.	        { 
	87.	         activityMonitor.stop();
	88.	         progressDialog.close();
	89.	         activity.interrupt();
	90.	         startButton.setEnabled(true);
	91.	        }
	92.	      }
	93.	     });
	94.	  }
	95.	
	96.	  private Timer activityMonitor;
	97.	  private JButton startButton;
	98.	  private ProgressMonitor progressDialog;
	99.	  private JTextArea textArea;
	100.	  private SimulatedActivity activity;
	101.	
	102.	  public static final int WIDTH = 300;
	103.	  public static final int HEIGHT = 200;
	104.	}
	105.	
	106.	/**
	107.	  A simulated activity thread.
	108.	*/
	109.	class SimulatedActivity extends Thread
	110.	{ 
	111.	  /**
	112.	   Constructs the simulated activity thread object. The
	113.	   thread increments a counter from 0 to a given target.
	114.	   @param t the target value of the counter.
	115.	  */
	116.	  public SimulatedActivity(int t)
	117.	  { 
	118.	   current = 0;
	119.	   target = t;
	120.	  }
	121.	
	122.	  public int getTarget()
	123.	  { 
	124.	   return target;
	125.	  }
	126.	
	127.	  public int getCurrent()
	128.	  { 
	129.	   return current;
	130.	  }
	131.	
	132.	  public void run()
	133.	  { 
	134.	   try
	135.	   {
	136.	     while (current < target && !interrupted())
	137.	     {  
	138.	      sleep(100);
	139.	      current++;
	140.	     }
	141.	   }
	142.	   catch(InterruptedException e)
	143.	   { 
	144.	   }
	145.	  }
	146.	
	147.	  private int current;
	148.	  private int target;
	149.	}

Monitoring the Progress of Input Streams

The Swing package contains a useful stream filter, ProgressMonitorInputStream, that automatically pops up a dialog that monitors how much of the stream has been read.

This filter is extremely easy to use. You sandwich in a ProgressMonitorInputStream between your usual sequence of filtered streams. (See Chapter 12 of Volume 1 for more information on streams.)

For example, suppose you read text from a file. You start out with a FileInputStream:

FileInputStream in = new FileInputStream(f);

Normally, you would convert fileIn to an InputStreamReader.

InputStreamReader reader = new InputStreamReader(in);

However, to monitor the stream, first turn the file input stream into a stream with a progress monitor:

ProgressMonitorInputStream progressIn
  = new ProgressMonitorInputStream(parent, caption, in);

You need to supply the parent component, a caption, and, of course, the stream to monitor. The read method of the progress monitor stream simply passes along the bytes and updates the progress dialog.

You now go on building your filter sequence:

InputStreamReader reader = new InputStreamReader(progressIn);

That's all there is to it. When the file is read, the progress monitor automatically pops up. This is a very nice application of stream filtering.

CAUTION

The progress monitor stream uses the available method of the InputStream class to determine the total number of bytes in the stream. However, the available method only reports the number of bytes in the stream that are available without blocking. Progress monitors work well for files and HTTP URLs because their length is known in advance, but they don't work with all streams.

Figure 1–17: A progress monitor for an input stream

The program in Example 1–12 counts the lines in a file. If you read in a large file (such as "The Count of Monte Cristo" on the CD), then the progress dialog pops up.

Note that the program doesn't use a very efficient way of filling up the text area. It would be faster to first read in the file into a StringBuffer and then set the text of the text area to the string buffer contents. But in this example program, we actually like this slow approach—it gives you more time to admire the progress dialog.

To avoid flicker, the text area is not displayed while it is filled up.

Example 1–12: ProgressMonitorInputStreamTest.java

	1.	import java.awt.*;
	2.	import java.awt.event.*;
	3.	import java.io.*;
	4.	import java.util.*;
	5.	import javax.swing.*;
	6.	import javax.swing.event.*;
	7.	import javax.swing.Timer;
	8.	
	9.	/**
	10.	  A program to test a progress monitor input stream.
	11.	*/
	12.	public class ProgressMonitorInputStreamTest
	13.	{ 
	14.	  public static void main(String[] args)
	15.	  { 
	16.	   JFrame frame = new TextFrame();
	17.	   frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
	18.	   frame.show();
	19.	  }
	20.	}
	21.	
	22.	/**
	23.	  A frame with a menu to load a text file and a text area
	24.	  to display its contents. The text area is constructed 
	25.	  when the file is loaded and set as the content pane of 
	26.	  the frame when the loading is complete. That avoids flicker
	27.	  during loading.
	28.	*/
	29.	class TextFrame extends JFrame
	30.	{ 
	31.	  public TextFrame()
	32.	  { 
	33.	   setTitle("ProgressMonitorInputStreamTest");
	34.	   setSize(WIDTH, HEIGHT);
	35.	
	36.	   // set up menu
	37.	
	38.	   JMenuBar menuBar = new JMenuBar();
	39.	   setJMenuBar(menuBar);
	40.	   JMenu fileMenu = new JMenu("File");
	41.	   menuBar.add(fileMenu);
	42.	   openItem = new JMenuItem("Open");
	43.	   openItem.addActionListener(new
	44.	     ActionListener()
	45.	     {
	46.	      public void actionPerformed(ActionEvent event)
	47.	      {
	48.	        try
	49.	        {
	50.	         openFile();
	51.	        }
	52.	        catch(IOException exception)
	53.	        { 
	54.	         exception.printStackTrace();
	55.	        }
	56.	      }
	57.	     });
	58.	
	59.	   fileMenu.add(openItem);
	60.	   exitItem = new JMenuItem("Exit");
	61.	   exitItem.addActionListener(new
	62.	     ActionListener()
	63.	     {
	64.	      public void actionPerformed(ActionEvent event)
	65.	      {
	66.	        System.exit(0);
	67.	      }
	68.	     });
	69.	   fileMenu.add(exitItem);
	70.	  }
	71.	
	72.	  /**
	73.	   Prompts the user to select a file, loads the file into
	74.	   a text area, and sets it as the content pane of the frame.
	75.	  */
	76.	  public void openFile() throws IOException
	77.	  { 
	78.	   JFileChooser chooser = new JFileChooser();
	79.	   chooser.setCurrentDirectory(new File("."));
	80.	   chooser.setFileFilter(
	81.	     new javax.swing.filechooser.FileFilter()
	82.	      { 
	83.	        public boolean accept(File f)
	84.	        { 
	85.	         String fname = f.getName().toLowerCase();
	86.	         return fname.endsWith(".txt")
	87.	           || f.isDirectory();
	88.	        }
	89.	        public String getDescription()
	90.	        { 
	91.	         return "Text Files"; 
	92.	        }
	93.	      });
	94.	
	95.	   int r = chooser.showOpenDialog(this);
	96.	   if (r != JFileChooser.APPROVE_OPTION) return;
	97.	   final File f = chooser.getSelectedFile();
	98.	
	99.	   // set up stream and reader filter sequence
	100.	   
	101.	   FileInputStream fileIn = new FileInputStream(f);
	102.	   ProgressMonitorInputStream progressIn
	103.	     = new ProgressMonitorInputStream(this,
	104.	      "Reading " + f.getName(), fileIn);
	105.	   InputStreamReader inReader
	106.	     = new InputStreamReader(progressIn);
	107.	   final BufferedReader in = new BufferedReader(inReader);
	108.	   
	109.	   // the monitored activity must be in a new thread. 
	110.	
	111.	   Thread readThread = new Thread()
	112.	   { 
	113.	     public void run()
	114.	     {       
	115.	      try
	116.	      {
	117.	        final JTextArea textArea = new JTextArea(); 
	118.	
	119.	        String line;
	120.	        while ((line = in.readLine()) != null)
	121.	        {
	122.	         textArea.append(line);
	123.	         textArea.append("\n");
	124.	        }
	125.	        in.close();
	126.	
	127.	        // set content pane in the event dispatch thread
	128.	        EventQueue.invokeLater(new 
	129.	         Runnable()
	130.	         { 
	131.	           public void run()
	132.	           { 
	133.	            setContentPane(new JScrollPane(textArea));
	134.	            validate();
	135.	           }
	136.	         });
	137.	        
	138.	      }
	139.	      catch(IOException exception)
	140.	      { 
	141.	        exception.printStackTrace();
	142.	      }
	143.	     }
	144.	   };
	145.	
	146.	   readThread.start();
	147.	  }
	148.	
	149.	  private JMenuItem openItem;
	150.	  private JMenuItem exitItem;
	151.	
	152.	  public static final int WIDTH = 300;
	153.	  public static final int HEIGHT = 200;
	154.	}

javax.swing.JProgressBar

  • JProgressBar()

  • JProgressBar(int direction)

  • JProgressBar(int min, int max)

  • JProgressBar(int direction, int min, int max)
    construct a horizontal slider with the given direction, minimum and maximum.

Parameters:

direction

one of SwingConstants.HORIZONTAL or SwingConstants.VERTICAL. The default is horizontal.

 

min, max

the minimum and maximum for the progress bar values. Defaults are 0 and 100.

  • int getMinimum()

  • int getMaximum()

  • void setMinimum(int value)

  • void setMaximum(int value)get and set the minimum and maximum values.

  • int getValue()

  • void setValue(int value)
    get and set the current value.

  • String getString()

  • void setString(String s)
    get and set the string to be displayed in the progress bar. If the string is null, then a default string "n %" is displayed.

  • boolean isStringPainted()

  • void setStringPainted(boolean b)
    get and set the "string painted" property. If this property is true, then a string is painted on top of the progress bar. The default is false; no string is painted.

  • boolean isIndeterminate()

  • void setIndeterminate(boolean b)
    get and set the "indeterminate" property (SDK 1.4). If this property is true, then the progress bar becomes a block that moves backwards and forwards, indicating a wait of unknown duration. The default is false.

javax.swing.ProgressMonitor

  • ProgressMonitor(Component parent, Object message, String note, int min, int max)
    constructs a progress monitor dialog.

Parameters:

parent

the parent component over which this dialog pops up.

 

message

the message object to display in the dialog.

 

note

the optional string to display under the message. If this value is null, then no space is set aside for the note, and a later call to setNote has no effect.

 

min, max

the minimum and maximum values of the progress bar.

  • void setNote(String note)
    changes the note text.

  • void setProgress(int value) sets the progress bar value to the given value.

  • void close()
    closes this dialog.

  • boolean isCanceled()
    returns true if the user canceled this dialog.

javax.swing.ProgressMonitorInputStream

  • ProgressMonitorInputStream(Component parent, Object message, InputStream in)
    constructs an input stream filter with an associated progress monitor dialog.

Parameters:

parent

the parent component over which this dialog pops up

 

message

the message object to display in the dialog

 

in

the input stream that is being monitored

  • + Share This
  • 🔖 Save To Your Account