Java Swing

Perhaps we should consider using VisualJ, JBuilder or some other visual development environment for Java

http://docs.oracle.com/javafx/2/layout/jfxpub-layout.htm
https://wiki.smu.edu.sg/w/is480/images/2/20/The_Java_Swing_tutorial.pdf
http://notes.corewebprogramming.com/student/Basic-Swing.pdf
http://docs.oracle.com/javase/tutorial/uiswing/index.html
http://java.sun.com/developer/onlineTraining/GUI/Swing1/shortcourse.html
http://docs.oracle.com/javase/tutorial/uiswing/components/index.html
http://docs.oracle.com/javase/tutorial/uiswing/TOC.html
http://cs.nyu.edu/~yap/classes/visual/03s/lect/l7/
http://www.roseindia.net/java/example/java/swing/
http://docs.oracle.com/javase/tutorial/uiswing/layout/visual.html

Bare minimal Swing application consisting of a single button: (demonstrating how to create the basic application, and how to handle events)

import javax.swing.*;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.FlowLayout;
import java.awt.Dimension;
import java.awt.Color;
import javax.swing.SwingConstants;
import org.apache.commons.lang.StringUtils;
import org.apache.commons.lang.exception.ExceptionUtils;

public class MakeWarFile extends JFrame {
    public MakeWarFile() throws Exception {
        this.createMainWindow();
    }

    private void createMainWindow() throws Exception {
        this.setLocationRelativeTo(null); // Center the window
        this.setTitle("My Application Title");
        this.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); 
        this.setSize(800, 210);
        this.setVisible(true);
        this.getContentPane().setLayout(new FlowLayout());

        JButton goButton = new JButton("Go");
        goButton.addActionListener(new ActionListener() {
            public void actionPerformed(ActionEvent event) {
                // Logic to handle the click event
            }
            private void copyFile(String from, String to) {
                // If necessary, we can define additional methods as
                // part of our anonymous class, and use them inside
                // the actionPerformed method
            }
        });
        goButton.setToolTipText("Tooltip for the button");
        JPanel panel4 = new JPanel();
        panel4.setLayout(new FlowLayout());
        panel4.add(goButton);

        this.getContentPane().add(panel1); // Add the panel to the window
    }
    public static void main (String args[]) throws Exception { 
        MakeWarFile win = new MakeWarFile();
        win.show();
    }
}

Another example of how to write a Swing application:

public class MainFrame extends javax.swing.JFrame {
  public static void main(String[] args) {
    SwingUtilities.invokeLater(new Runnable() {
      public void run() {
        new MainFrame().setVisible(true);
      }
    });
  }
}

In the first example, the initialization and rendering of the GUI is done inside createMainWindow, which is probably run inside the initial thread. It violates the rule that you should not interact with Swing components from any thread except the event dispatcher thread. This particular mistake is easy to make, and thread synchronization problems may not be immediately obvious. This example should be used instead of the first example.

Threads:

A Swing programmer deals with the following kinds of threads:

  • Initial threads, the threads that execute initial application code.
  • The event dispatch thread, where all event-handling code is executed. Most code that interacts with the Swing framework must also execute on this thread
  • Worker threads, also known as background threads, where time-consuming background tasks are executed.

The programmer does not need to provide code that explicitly creates these threads: they are provided by the runtime or the Swing framework. The programmer's job is to utilize these threads to create a responsive, maintainable Swing program.

Like any other program running on the Java platform, a Swing program can create additional threads and thread pools.

Every program has a set of threads where the application logic begins. In standard programs, there is only one such thread (the thread that invoke the main method). In applets, the initial threads are the ones that construct the applet object, and invoke its init and start methods. These actions may occur on a single thread, or on two or three different threads, depending on the Java platform implementation.

In Swing programs, the initial threads don't have a lot to do. Their most essential job is to create a Runnable object that initializes the GUI and schedule that object for execution on the event dispatch thread. Once the GUI is created, the program is primarily driven by GUI events, each of which causes the execution of a short task on the event dispatch thread. Application code can schedule additionals tasks on the event dispatch thread (if they complete quickly, so as not to interfere with event processing) or a worker thread (for long-running tasks).

An initial thread schedules the GUI creation task by invoking javax.swing.SwingUtilities.invokeLater or javax.swing.SwingUtilities.invokeAndWait . Both of these methods take a single argument: the Runnable that defines the new task. Their only difference is indicated by their names: invokeLater simply schedules the task and returns; invokeAndWait waits for the task to finish before returning.

SwingUtilities.invokeLater(new Runnable() {
    public void run() {
        createAndShowGUI();
    }
});

In an applet, the GUI-creation task must be launched from the init method using invokeAndWait; otherwise, init may return before the GUI is created, which may cause problems for a web browser launching an applet. In any other kind of program, scheduling the GUI-creation task is usually the last thing the initial thread does, so it doesn't matter whether it uses invokeLater or invokeAndWait.

Although you can use invokeLater from either your application or EDT, you should never use the invokeAndWait method from that thread. Using invokeAndWait from the EDT would create the same response delays that you should avoid.

Swing event handling code runs on a special thread known as the event dispatch thread. Most code that invokes Swing methods also runs on this thread. This is necessary because most Swing object methods are not "thread safe": invoking them from multiple threads risks thread interference or memory consistency errors. Some Swing component methods are labelled "thread safe" in the API specification; these can be safely invoked from any thread. All other Swing component methods must be invoked from the event dispatch thread. Programs that ignore this rule may function correctly most of the time, but are subject to unpredictable errors that are difficult to reproduce.

It's useful to think of the code running on the event dispatch thread as a series of short tasks. Most tasks are invocations of event-handling methods, such as ActionListener.actionPerformed. Other tasks can be scheduled by application code, using invokeLater or invokeAndWait. Tasks on the event dispatch thread must finish quickly; if they don't, unhandled events back up and the user interface becomes unresponsive.

Beside the initial threads (which should not do much), as Swing developer only have to concern with two type of threads (the event dispatcher thread, and the worker threads). Code that run inside the event dispatcher thread must be short tasks. Code that takes a long time to finish should be run inside a worker thread.

When writing Swing applications, there are two constraints to keep in mind:

  1. Time-consuming tasks should not be run in the event dispatcher thread (otherwise, the application becomes unresponsive.
  2. Swing components should be accessed on the event dispatcher thread only.

These constraints mean that a GUI application needs at least two threads (a worker thread to perform the lengthy task, and the event dispatcher thread for all GUI-related activities).

SwingWorker is designed for situation where you need to have a long running task in a background thread and provide updates to the UI either when done, or while processing. Subclass of SwingWorker must implement the doInBackground() method to perform background computation.

There are three threads involved in the life cycle of a SwingWorker:

  • Current thread: The execute() method is called on this thread. It schedules SwingWorker for the execution on a worker thread and returns immediately. One can wait for the SwingWorker to complete using the get methods.
  • Worker thread: The doInBackground() method is called on this thread. This is where all background activities should happen. To notify PropertyChangeListeners about bound properties changes use the firePropertyChange and getPropertyChangeSupport() methods. By default there are two bound properties available: state and progress.
  • Event Dispatch Thread: All Swing related activities occur on this thread. SwingWorker invokes the process and done() methods and notifies any PropertyChangeListeners on this thread.

Often, the Current thread is the Event Dispatch Thread.

Before the doInBackground method is invoked on a worker thread, SwingWorker notifies any PropertyChangeListeners about the state property change to StateValue.STARTED. After the doInBackground method is finished the done method is executed. Then SwingWorker notifies any PropertyChangeListeners about the state property change to StateValue.DONE.

SwingWorker is only designed to be executed once. Executing a SwingWorker more than once will not result in invoking the doInBackground method twice.

   final JLabel label;
   class MeaningOfLifeFinder extends SwingWorker<String, Object> {
       @Override
       public String doInBackground() {
           return findTheMeaningOfLife();
       }

       @Override
       protected void done() {
           try { 
               label.setText(get());
           } catch (Exception ignore) {
           }
       }
   }

   (new MeaningOfLifeFinder()).execute();
 class PrimeNumbersTask extends 
         SwingWorker<List<Integer>, Integer> {
     PrimeNumbersTask(JTextArea textArea, int numbersToFind) { 
         //initialize 
     }

     @Override
     public List<Integer> doInBackground() {
         while (! enough && ! isCancelled()) {
                 number = nextPrimeNumber();
                 publish(number);
                 setProgress(100 * numbers.size() / numbersToFind);
             }
         }
         return numbers;
     }

     @Override
     protected void process(List<Integer> chunks) {
         for (int number : chunks) {
             textArea.append(number + "\n");
         }
     }
 }

 JTextArea textArea = new JTextArea();
 final JProgressBar progressBar = new JProgressBar(0, 100);
 PrimeNumbersTask task = new PrimeNumbersTask(textArea, N);
 task.addPropertyChangeListener(
     new PropertyChangeListener() {
         public  void propertyChange(PropertyChangeEvent evt) {
             if ("progress".equals(evt.getPropertyName())) {
                 progressBar.setValue((Integer)evt.getNewValue());
             }
         }
     });

 task.execute();
 System.out.println(task.get()); //prints all prime numbers we have got

Notice that we create the SwingWorker object, and then we use of execute(). When we construct the SwingWorker object, doInBackground is not automatically executed. The execute() method cause doInBackground to be invoked.

Also, just as normal class, we can still use constructor to initialize our object. Additionally, this example demonstrates how to use addPropertyChangeListener.

Because SwingWorker implements Runnable, a SwingWorker can be submitted to an Executor for execution.

If you need to determine whether your code is running on the event dispatch thread, invoke javax.swing.SwingUtilities.isEventDispatchThread.

When a Swing program needs to execute a long-running task, it usually uses one of the worker threads, also known as the background threads. Each task running on a worker thread is represented by an instance of javax.swing.SwingWorker. SwingWorker itself is an abstract class; you must define a subclass in order to create a SwingWorker object. Anonymous inner classes are often useful for creating very simple SwingWorker objects.

SwingWorker provides a number of communication and control features:

  • The SwingWorker subclass can define a method, done, which is automatically invoked on the event dispatch thread when the background task is finished.
  • SwingWorker implements java.util.concurrent.Future. This interface allows the background task to provide a return value to the other thread. Other methods in this interface allow cancellation of the background task and discovering whether the background task has finished or been cancelled.
  • The background task can provide intermediate results by invoking SwingWorker.publish, causing SwingWorker.process to be invoked from the event dispatch thread.
  • The background task can define bound properties. Changes to these properties trigger events, causing event-handling methods to be invoked on the event dispatch thread.

The javax.swing.SwingWorker class was added to the Java platform in Java SE 6. Prior to this, another class, also called SwingWorker, was widely used for some of the same purposes. The old SwingWorker was not part of the Java platform specification, and was not provided as part of the JDK.  The new javax.swing.SwingWorker is a completely new class. Its functionality is not a strict superset of the old SwingWorker. Methods in the two classes that have the same function do not have the same names. Also, instances of the old SwingWorker class were reusable, while a new instance of javax.swing.SwingWorker is needed for each new background task.

A backport of the Java 6 SwingWorker to Java 5 has been available at https://swingworker.dev.java.net/ since March 2007. Apart from the package name ( org.jdesktop.swingworker ), it is compatible with the Java 6 SwingWorker.

Simple Background Tasks:

Let's start with a task that is very simple, but potentially time-consuming. The TumbleItem applet loads a set of graphic files used in an animation. If the graphic files are loaded from an initial thread, there may be a delay before the GUI appears. If the graphic files are loaded from the event dispatch thread, the GUI may be temporarily unresponsive. To avoid these problems, TumbleItem creates and executes an instance of SwingWorker from its initial threads. The object's doInBackground method, executing in a worker thread, loads the images into an ImageIcon array, and returns a reference to it. Then the done method, executing in the event dispatch thread, invokes get to retrieve this reference, which it assigns to an applet class field named imgs. This allows TumbleItem to construct the GUI immediately, without waiting for the images to finish loading.

SwingWorker worker = new SwingWorker<ImageIcon[], Void>() {
    @Override
    public ImageIcon[] doInBackground() {
        final ImageIcon[] innerImgs = new ImageIcon[nimgs];
        for (int i = 0; i < nimgs; i++) {
            innerImgs[i] = loadImage(i+1);
        }
        return innerImgs;
    }

    @Override
    public void done() {
        //Remove the "Loading images" label.
        animator.removeAll();
        loopslot = -1;
        try {
            imgs = get();
        } catch (InterruptedException ignore) {}
        catch (java.util.concurrent.ExecutionException e) {
            String why = null;
            Throwable cause = e.getCause();
            if (cause != null) {
                why = cause.getMessage();
            } else {
                why = e.getMessage();
            }
            System.err.println("Error retrieving file: " + why);
        }
    }
};

All concrete subclasses of SwingWorker implement doInBackground. Implementation of done is optional.

Notice that SwingWorker is a generic class, with two type parameters. The first type parameter specifies a return type for doInBackground, and also for the get method, which is invoked by other threads to retrieve the object returned by doInBackground. SwingWorker's second type parameter specifies a type for interim results returned while the background task is still active. Since this example doesn't return interim results, Void is used as a placeholder.

You may wonder if the code that sets imgs is unnecessarily complicated. Why make doInBackground return an object and use done to retrieve it? Why not just have doInBackground set imgs directly? The problem is that the object imgs refers to is created in the worker thread and used in the event dispatch thread. When objects are shared between threads in this way, you must make sure that changes made in one thread are visible to the other. Using get guarantees this, because using get creates a happens before relationship between the code that creates imgs and the code that uses it. For more on the happens before relationship, refer to Memory Consistency Errors in the Concurrency lesson.

There are actually two ways to retrieve the object returned by doInBackground:

  • Invoke SwingWorker.get with no arguments. If the background task is not finished, get blocks until it is.
  • Invoke SwingWorker.get with arguments indicating a timeout. If the background task is not finished, get blocks until it is — unless the timeout expires first, in which case get throws java.util.concurrent.TimeoutException.

Be careful when invoking either overload of get from the event dispatch thread. Until get returns, no GUI events are being processed, and the GUI is "frozen". Don't invoke get without arguments unless you are confident that the background task is complete or close to completion.

For more on the TumbleItem example, refer to How to Use Swing Timers in the lesson Using Other Swing Features.

Tasks that have interim results:

It is often useful for a background task to provide interim results while it is still working. The task can do this by invoking SwingWorker.publish. This method accepts a variable number of arguments. Each argument must be of the type specified by SwingWorker's second type parameter. To collect results provided by publish, override SwingWorker.process This method will be invoked from the event dispatch thread. Results from multiple invocations of publish are often accumulated for a single invocation of process.

private class FlipTask extends SwingWorker<Void, FlipPair> {
    // Since the task does not return a final result, it does
    // not matter what the first type parameter is; Void is 
    // used as a placeholder. The task invokes publish after 
    // each "coin flip"
    @Override
    protected Void doInBackground() {
        long heads = 0;
        long total = 0;
        Random random = new Random();
        while (!isCancelled()) {
            total++;
            if (random.nextBoolean()) {
                heads++;
            }
            publish(new FlipPair(heads, total));
        }
        return null;
    }
    protected void process(List<FlipPair> pairs) {
        FlipPair pair = pairs.get(pairs.size() - 1);
        headsText.setText(String.format("%d", pair.heads));
        totalText.setText(String.format("%d", pair.total));
        devText.setText(String.format("%.10g", ((double) pair.heads)/((double) pair.total) - 0.5));
    }
}

Because publish is invoked very frequently, a lot of FlipPair values will probably be accumulated before process is invoked in the event dispatch thread; process is only interested in the last value reported each time, using it to update the GUI. If Random is fair, the value displayed in devText should get closer and closer to 0 as Flipper runs.

Cancelling Background Tasks:

To cancel a running background task, invoke SwingWorker.cancel The task must cooperate with its own cancellation. There are two ways it can do this:

  • By terminating when it receives an interrupt. This procedures is described in Interrupts in Concurrency.
  • By invoking SwingWorker.isCancelled at short intervals. This method returns true if cancel has been invoked for this SwingWorker.

The cancel method takes a single boolean argument. If the argument is true, cancel sends the background task an interrupt. Whether the argument is true or false, invoking cancel changes the cancellation status of the object to true. This is the value returned by isCancelled. Once changed, the cancellation status cannot be changed back.

The Flipper example from the previous section uses the status-only idiom. The main loop in doInBackground exits when isCancelled returns true. This will occur when the user clicks the "Cancel" button, triggering code that invokes cancel with an argument of false. The status-only approach makes sense for Flipper because its implementation of SwingWorker.doInBackground does not include any code that might throw InterruptedException. To respond to an interrupt, the background task would have to invoke Thread.isInterrupted at short intervals. It's just as easy to use SwingWorker.isCancelled for the same purpose

If get is invoked on a SwingWorker object after its background task has been cancelled, java.util.concurrent.CancellationException is thrown.

Bound Properties and Status Methods:

SwingWorker supports bound properties, which are useful for communicating with other threads. Two bound properties are predefined: progress and state. As with all bound properties, progress and state can be used to trigger event-handling tasks on the event dispatch thread.

By implementing a property change listener, a program can track changes to progress, state, and other bound properties. For more information, refer to How to Write a Property Change Listener in Writing Event Listeners.

The progress bound variable is an int value that can range from 0 to 100. It has a predefined setter method (the protected SwingWorker.setProgress) and a predefined getter method (the public SwingWorker.getProgress). The ProgressBarDemo example uses progress to update a ProgressBar control from a background task. For a detailed discussion of this example, refer to How to Use Progress Bars in Using Swing Components.

The state bound variable indicates where the SwingWorker object is in its lifecycle. The bound variable contains an enumeration value of type SwingWorker.StateValue. Possible values are:

  • PENDING: The state during the period from the construction of the object until just before doInBackground is invoked.
  • STARTED: The state during the period from shortly before doInBackground is invoked until shortly before done is invoked.
  • DONE: The state for the remainder of the existence of the object.

The current value of the state bound variable is returned by SwingWorker.getState.

Two methods, part of the Future interface, also report on the status of the background task. As we saw in Canceling Background Tasks, isCancelled returns true if the task has been canceled. In addition, isDone returns true if the task has finished, either normally, or by being cancelled.

Example to demonstrate passing information to doInBackground, retrieving the result from doInBackground, and displaying the result in the UI:

import java.io.File;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Set;
import java.util.List;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.InputStreamReader;
import java.io.BufferedReader;
import java.io.InputStream;
import java.io.Reader;
import java.io.DataInputStream;
import java.nio.channels.FileChannel;
import java.util.Arrays;
import java.util.Collection;
import java.util.Vector;
import java.util.Map;

import javax.swing.*;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.FlowLayout;
import java.awt.Dimension;
import java.awt.Color;
import java.awt.TextArea;
import javax.swing.SwingConstants;
import java.util.concurrent.ExecutionException;
import org.apache.commons.lang.StringUtils;
import org.apache.commons.lang.exception.ExceptionUtils;
import org.tmatesoft.svn.core.*;
import org.tmatesoft.svn.core.io.*;
import org.tmatesoft.svn.core.auth.*;
import org.tmatesoft.svn.core.wc.*;
import org.tmatesoft.svn.core.internal.io.fs.FSRepositoryFactory;
import org.tmatesoft.svn.core.internal.io.dav.DAVRepositoryFactory;
import org.tmatesoft.svn.core.internal.io.svn.SVNRepositoryFactoryImpl;
import org.tmatesoft.svn.core.SVNURL;
import org.tmatesoft.svn.core.io.SVNRepository;
import org.tmatesoft.svn.core.io.SVNRepositoryFactory;
import org.tmatesoft.svn.core.wc.SVNWCUtil;
import org.tmatesoft.svn.core.SVNException;

public class SVNTool extends JFrame {
    class SVNToolResult {
        public String strDetails, strAddedOrModifiedFiles, strDeletedFiles, strStackTrace;
        public SVNToolResult(String details, String addedOrModifiedFiles, String deletedFiles, String stackTrace) {
            strDetails = details;
            strAddedOrModifiedFiles = addedOrModifiedFiles;
            strDeletedFiles = deletedFiles;
            strStackTrace = stackTrace;
        }
    }
    class SVNToolSwingWorker extends SwingWorker<SVNToolResult, Void> {
        private String strSVNURL;
        private String strUsername;
        private String strPassword;
        protected TextArea addedOrModifiedFilesTextArea;
        protected TextArea deletedFilesTextArea;
        protected TextArea detailTextArea;
        public SVNToolSwingWorker(String strURL, String username, String password, TextArea addedOrModifiedFilesTextArea, TextArea deletedFilesTextArea, TextArea detailTextArea) {
            // initialization
            this.strSVNURL = strURL;
            this.strUsername = username;
            this.strPassword = password;
            this.addedOrModifiedFilesTextArea = addedOrModifiedFilesTextArea;
            this.deletedFilesTextArea = deletedFilesTextArea;
            this.detailTextArea = detailTextArea;
        }

        @Override
        public SVNToolResult doInBackground() {
            // Do our time-consuming task
            String stackTrace = "";
            String details = "";
            String addedOrModifiedFiles = "";
            String deletedFiles = "";
            try {
                System.out.println("doInBackground started");
                String lineSeparator = System.getProperty("line.separator");
                SVNRepository repository = SVNRepositoryFactory.create( SVNURL.parseURIDecoded( this.strSVNURL ) );
                ISVNAuthenticationManager authManager = SVNWCUtil.createDefaultAuthenticationManager( this.strUsername , this.strPassword );
                repository.setAuthenticationManager( authManager );
                long startRevision = 0;
                long endRevision = -1; //HEAD (the latest) revision

                Collection logEntries = repository.log( new String[] { "" } , null , startRevision , endRevision , true , true );
                StringBuffer sb = new StringBuffer();
                HashMap hmc = new HashMap(); // hash map of changed files
                HashMap hmd = new HashMap(); // hash map of deleted files
                Vector vc = new Vector(); // vector of changed files
                Vector vd = new Vector(); // vector of deleted files
                for ( Iterator entries = logEntries.iterator( ); entries.hasNext( ); ) {
                    SVNLogEntry logEntry = ( SVNLogEntry ) entries.next( );

                    sb.append("---------------------------------------------" + lineSeparator);
                    sb.append("revision: " + logEntry.getRevision( ) + lineSeparator);
                    sb.append("author: " + logEntry.getAuthor( ) + lineSeparator);
                    sb.append("date: " + logEntry.getDate( ) + lineSeparator);

                    if ( logEntry.getChangedPaths( ).size( ) > 0 ) {
                        sb.append("" + lineSeparator);
                        sb.append( "changed paths:" + lineSeparator );
                        Set changedPathsSet = logEntry.getChangedPaths( ).keySet( );

                        for ( Iterator changedPaths = changedPathsSet.iterator( ); changedPaths.hasNext( ); ) {
                            SVNLogEntryPath entryPath = ( SVNLogEntryPath ) logEntry.getChangedPaths( ).get( changedPaths.next( ) );
                            String changeType = entryPath.getType() + "";
                            String changePath = entryPath.getPath();
                            sb.append( " "
                                    + changeType
                                    + " "
                                    + changePath
                                    + ( ( entryPath.getCopyPath( ) != null ) ? " (from "
                                            + entryPath.getCopyPath( ) + " revision "
                                            + entryPath.getCopyRevision( ) + ")" : "" )  + lineSeparator);
                            if (changeType.equalsIgnoreCase("D")) {
                                if (hmc.containsKey(changePath)) {
                                    hmc.remove(changePath);
                                    hmd.put(changePath,true);
                                }
                            } else {
                                if (! hmc.containsKey(changePath)) {
                                    hmc.put(changePath,true);
                                }
                                if (hmd.containsKey(changePath)) {
                                    // if it was deleted (perhaps by mistake), but now being added back
                                    hmd.remove(changePath);
                                }
                            }
                        }
                    }
                }

                details = sb.toString();

                Iterator it = hmc.entrySet().iterator();
                while (it.hasNext()) {
                    Map.Entry pair = (Map.Entry) it.next();
                    String changePath = pair.getKey() + "";
                    vc.add(changePath);
                }

                it = hmd.entrySet().iterator();
                while (it.hasNext()) {
                    Map.Entry pair = (Map.Entry) it.next();
                    String changePath = pair.getKey() + "";
                    vd.add(changePath);
                }

                Object[] ac = vc.toArray();
                Arrays.sort(ac, null);
                String sc = StringUtils.join(ac, lineSeparator);
                addedOrModifiedFiles = sc;

                Object[] ad = vd.toArray();
                Arrays.sort(ad, null);
                String sd = StringUtils.join(ad, lineSeparator);
                deletedFiles = sd;
            } catch (Exception e) {
                System.out.println("Exception occurred in doInBackground");
                stackTrace = ExceptionUtils.getStackTrace(e);
                System.out.println(stackTrace);
            }

            SVNToolResult result = new SVNToolResult(details, addedOrModifiedFiles, deletedFiles, stackTrace);
            return result;
        }

        @Override
        public void done() {
            // Update the UI
            System.out.println("done invoked");
            try {
                SVNToolResult result = get();
                if (! result.strStackTrace.equals("")) {
                    JOptionPane.showMessageDialog(null, "An exception occurred.");
                    System.out.println("Added or Modified Files:" + result.strStackTrace);
                } else {
                    System.out.println("Added or Modified Files:" + result.strAddedOrModifiedFiles);
                    System.out.println("Deleted Files:" + result.strDeletedFiles);
                    System.out.println("Details:" + result.strDetails);
                    this.addedOrModifiedFilesTextArea.setText(result.strAddedOrModifiedFiles);
                    this.deletedFilesTextArea.setText(result.strDeletedFiles);
                    this.detailTextArea.setText(result.strDetails);
                }
            } catch (InterruptedException ex) {
                ex.printStackTrace();
            } catch (ExecutionException ex) {
                ex.printStackTrace();
            }
        }

    }
    public SVNTool() throws Exception {
        this.createMainWindow();
    }

    private void createMainWindow() throws Exception {
        int fieldWidth = 770;

        this.setLocationRelativeTo(null); // Center the window
        this.setTitle("SVNTool");
        this.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); 
        this.setSize(800, 500);
        this.setVisible(true);
        this.getContentPane().setLayout(new FlowLayout());

        Color bg = this.getBackground();

        JPanel panel1 = new JPanel();
        panel1.setLayout(new FlowLayout(FlowLayout.LEFT));
        String svnURLTooltipText = "The URL for the branch";
        final JTextField svnURLTextField = new JTextField();
        svnURLTextField.setText("");
        svnURLTextField.setToolTipText(svnURLTooltipText);
        svnURLTextField.setPreferredSize(new Dimension(690,20));
        JLabel svnURLLabel = new JLabel("Branch URL:", SwingConstants.LEFT);
        svnURLLabel.setToolTipText(svnURLTooltipText);
        svnURLLabel.setLabelFor(svnURLTextField);
        svnURLLabel.setPreferredSize(new Dimension(70,16));
        panel1.add(svnURLLabel);
        panel1.add(svnURLTextField);
        //panel1.setBorder(BorderFactory.createLineBorder(Color.BLACK));
        panel1.setPreferredSize(new Dimension(780,30));

        final TextArea addedOrModifiedPane = new TextArea();
        addedOrModifiedPane.setPreferredSize(new Dimension(fieldWidth, 80));
        addedOrModifiedPane.setBackground(Color.WHITE);
        JLabel modifiedAddedLabel = new JLabel("Modified / Added Files:", SwingConstants.LEFT);
        JPanel panel3 = new JPanel();
        panel3.setLayout(new FlowLayout(FlowLayout.LEFT));
        panel3.add(modifiedAddedLabel);
        panel3.setPreferredSize(new Dimension(780,30));
        JPanel panel4 = new JPanel();
        panel4.setLayout(new FlowLayout(FlowLayout.LEFT));
        panel4.add(addedOrModifiedPane);
        //panel4.setPreferredSize(new Dimension(780,100));

        final TextArea deletedPane = new TextArea();
        deletedPane.setPreferredSize(new Dimension(fieldWidth, 80));
        deletedPane.setBackground(Color.WHITE);
        JLabel deletedLabel = new JLabel("Deleted Files:");
        JPanel panel5 = new JPanel();
        panel5.setPreferredSize(new Dimension(780,30));
        panel5.setLayout(new FlowLayout(FlowLayout.LEFT));
        panel5.add(deletedLabel);
        panel5.setPreferredSize(new Dimension(780,30));
        JPanel panel6 = new JPanel();
        panel6.setLayout(new FlowLayout(FlowLayout.LEFT));
        panel6.add(deletedPane);

        final TextArea detailPane = new TextArea();
        detailPane.setPreferredSize(new Dimension(fieldWidth, 80));
        detailPane.setBackground(Color.WHITE);
        JLabel detailLabel = new JLabel("Details");
        JPanel panel7 = new JPanel();
        panel7.setPreferredSize(new Dimension(780,30));
        panel7.setLayout(new FlowLayout(FlowLayout.LEFT));
        panel7.add(detailLabel);
        panel7.setPreferredSize(new Dimension(780,30));
        JPanel panel8 = new JPanel();
        panel8.setLayout(new FlowLayout(FlowLayout.LEFT));
        panel8.add(detailPane);

        JButton goButton = new JButton("Go");
        goButton.addActionListener(new ActionListener() {
            public void actionPerformed(ActionEvent event) {
                this.processGo();
            }
            private synchronized void processGo() {
                String strURL = svnURLTextField.getText().trim();
                if ((strURL.equals("")) || (strURL.indexOf("/trunk") > -1)) {
                    JOptionPane.showMessageDialog(null, "Must specify URL for a branch");
                    return;
                }
                String username = System.getenv("SVNTOOL_USERNAME");
                String password = System.getenv("SVNTOOL_PASSWORD");
                if ((username == null) || (password == null) || (username.equals("")) || (password.equals(""))) {
                    JOptionPane.showMessageDialog(null, "Environment variables SVNTOOL_USERNAME and SVNTOOL_PASSWORD are not configured.");
                    return;                        
                }
                SVNToolSwingWorker worker = new SVNToolSwingWorker(strURL, username, password, addedOrModifiedPane, deletedPane, detailPane);
                worker.execute();
            }
            public void copyFile(String source, String destination) throws Exception {
                // http://www.exampledepot.com/egs/java.nio/File2File.html
                // http://www.exampledepot.com/egs/java.io/CopyFile.html
                FileChannel srcChannel = new FileInputStream(source).getChannel();
                FileChannel dstChannel = new FileOutputStream(destination).getChannel();
                dstChannel.transferFrom(srcChannel, 0, srcChannel.size());
                srcChannel.close();
                dstChannel.close();
            }
        });
        goButton.setToolTipText("Retrieve the list of files that were added, modified, or deleted.");
        JPanel panel2 = new JPanel();
        panel2.setLayout(new FlowLayout());
        panel2.add(goButton);

        this.getContentPane().add(panel1);
        this.getContentPane().add(panel2);
        this.getContentPane().add(panel3);
        this.getContentPane().add(panel4);
        this.getContentPane().add(panel5);
        this.getContentPane().add(panel6);
        this.getContentPane().add(panel7);
        this.getContentPane().add(panel8);
    }
    public static void main (String args[]) throws Exception { 
        SVNTool win = new SVNTool();
        /* Example of adding listener.  Here we use an anonymous class that extends WindowAdapter
        win.addWindowListener(new WindowAdaper() {
            public void windowClosing(WindowEvent e) {
                System.exit(0);
            }
        });
        */
        win.show();
    }
}

In this example, we have an inner class SVNToolSwingWorker which is a sub-class of SwingWorker. This SVNToolSwingWorker class has a constructor. We instantiate an object of this SVNToolSwingWorker class, passing all the required information to the constructor. We than invoke the execute() method from this object in the event dispatcher thread. This cause the doInBackground method to be executed in the worker thread. The doInBackground method now have access to all information it needs. We also have an inner class SVNToolResult. When doInBackground finish, it instantiate an instance of SVNToolResult, and return that instance, and cause the done method to be executed. In the done method, we retrieve the result by invoking the get method().

http://docs.oracle.com/javase/tutorial/uiswing/index.html
http://docs.oracle.com/javase/tutorial/uiswing/concurrency/index.html
http://java.sun.com/products/jfc/tsc/articles/threads/threads1.html
http://java.sun.com/developer/technicalArticles/Threads/swing/
MultiThreaded toolkits: A failed dream?

Using Borders:

JPanel pane = new JPanel();
pane.setBorder(BorderFactory.createLineBorder(Color.black));

blackline = BorderFactory.createLineBorder(Color.black);
raisedetched = BorderFactory.createEtchedBorder(EtchedBorder.RAISED);
loweredetched = BorderFactory.createEtchedBorder(EtchedBorder.LOWERED);
raisedbevel = BorderFactory.createRaisedBevelBorder();
loweredbevel = BorderFactory.createLoweredBevelBorder();
empty = BorderFactory.createEmptyBorder();

To put a border around a JComponent, you use its setBorder method. You can use the BorderFactory class to create most of the borders that Swing provides. If you need a reference to a border — say, because you want to use it in multiple components

As you probably noticed, the code uses the BorderFactory class to create each border. The BorderFactory class, which is in the javax.swing package, returns objects that implement the Border interface.

The Border interface, as well as its Swing-provided implementations, is in the javax.swing.border package. You often don't need to directly use anything in the border package, except when specifying constants that are specific to a particular border class or when referring to the Border type.

Refers to How to Use Borders for details on borders.
Example of creating a basic Swing application and handling event:

import java.io.File;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Set;
import java.util.List;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.InputStreamReader;
import java.io.BufferedReader;
import java.io.InputStream;
import java.io.Reader;
import java.io.DataInputStream;
import java.nio.channels.FileChannel;

import javax.swing.*;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.FlowLayout;
import java.awt.Dimension;
import java.awt.Color;
import javax.swing.SwingConstants;
import org.apache.commons.lang.StringUtils;
import org.apache.commons.lang.exception.ExceptionUtils;

public class MakeWarFile extends JFrame {
    public MakeWarFile() throws Exception {
        this.createMainWindow();
    }

    private void createMainWindow() throws Exception {
        this.setLocationRelativeTo(null); // Center the window
        this.setTitle("Make quantros.war file");
        this.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); 
        this.setSize(800, 210);
        this.setVisible(true);
        this.getContentPane().setLayout(new FlowLayout());

        Color bg = this.getBackground();
        JTextPane introPane = new JTextPane();
        String introText = "The purpose of this application is to automate " +
        "the creation of quantros.war for a build.  To use this tool, you " +
        "must first create a folder.  This folder can be anywhere, and " +
        "you can give this folder any name.  This folder is known as the " +
        "destination folder.  You will also need to provide the location " +
        "of the quantros.war file in your bea folder";
        introPane.setText(introText);
        introPane.setPreferredSize(new Dimension(780, 60));
        introPane.setBackground(bg);
        JPanel panel1 = new JPanel();
        panel1.setLayout(new FlowLayout());
        panel1.add(introPane);

        String sourceFolderTooltipText = "The location of quantros.war inside your BEA folder";
        final JTextField sourceFolderTextField = new JTextField("",60);
        sourceFolderTextField.setText("C:\\bea\\user_projects\\domains\\quantrosDev\\applications\\myquantros.ear\\quantros.war");
        sourceFolderTextField.setToolTipText(sourceFolderTooltipText);
        JLabel sourceFolderLabel = new JLabel("Source Folder", SwingConstants.RIGHT);
        sourceFolderLabel.setToolTipText(sourceFolderTooltipText);
        sourceFolderLabel.setLabelFor(sourceFolderTextField);
        sourceFolderLabel.setPreferredSize(new Dimension(100,14));
        JPanel panel2 = new JPanel();
        panel2.setLayout(new FlowLayout());
        panel2.add(sourceFolderLabel);
        panel2.add(sourceFolderTextField);

        String destinationFolderTooltipText = "The location of where the new quantros.war will be created";
        final JTextField destinationFolderTextField = new JTextField("",60);
        destinationFolderTextField.setToolTipText(destinationFolderTooltipText);
        JLabel destinationFolderLabel = new JLabel("Destination Folder", SwingConstants.RIGHT);
        destinationFolderLabel.setToolTipText(destinationFolderTooltipText);
        destinationFolderLabel.setLabelFor(destinationFolderTextField);
        JPanel panel3 = new JPanel();
        panel3.setLayout(new FlowLayout());
        panel3.add(destinationFolderLabel);
        panel3.add(destinationFolderTextField);

        JButton goButton = new JButton("Go");
        goButton.addActionListener(new ActionListener() {
            public void actionPerformed(ActionEvent event) {
                // Make quantros.war
                String destinationFolderText = destinationFolderTextField.getText().trim();
                if (destinationFolderText.equals("")) {
                    JOptionPane.showMessageDialog(null, "Must specify the destination folder");
                    return;
                }
                // JOptionPane.showMessageDialog(null, "Base Location:" + destinationFolderText);
                String sourceFolderText = sourceFolderTextField.getText();
                ArrayList components = new ArrayList();
                components.add(destinationFolderText);
                components.add("docs");
                components.add("manifest.txt");
                String manifestFileName = StringUtils.join(components, File.separator);
                File manifestFile = new File(manifestFileName);
                if (!manifestFile.exists()) {
                    JOptionPane.showMessageDialog(null, manifestFile.getAbsolutePath() + " does not exist");
                    return;
                }
                try {
                    FileInputStream stream = new FileInputStream(manifestFile);
                    DataInputStream in = new DataInputStream(stream);
                    BufferedReader br = new BufferedReader(new InputStreamReader(in));
                    String line;
                    while ((line = br.readLine()) != null) {
                        System.out.println(line);
                        line = line.replaceAll("\\+","/"); // temporarily replace all backslah with forward slash (which is much easier to work with
                        line = line.replaceAll("^/", ""); // remove the leading slash
                        System.out.println(line);
                        line = line.replace('/', File.separatorChar);
                        if (line.startsWith("src" + File.separator)) {
                            line = line.replaceAll("\\.java",".class"); // Replace .java with .class
                            line = line.substring(4); // Remove src\
                            components = new ArrayList();
                            components.add(sourceFolderText);
                            components.add("WEB-INF");
                            components.add("classes");
                            components.add(line);
                            String source = StringUtils.join(components, File.separator);
                            File f = new File(source);
                            System.out.println("SOURCE:" + source);
                            if (! f.exists()) {
                                throw new Exception("Source file (" + source + ") does not exist");
                            }
                            if (f.isDirectory()) {
                                continue; // Skip this line if it exists in the manifest due to our error
                            }

                            // To create missing parents directories, we must 
                            components = new ArrayList();
                            components.add(destinationFolderText);
                            components.add("quantros.war");
                            components.add("WEB-INF");
                            components.add("classes");
                            components.add(line);
                            String destination = StringUtils.join(components, File.separator);
                            String destinationFolder = destination.substring(0, destination.lastIndexOf(File.separator));
                            f = new File(destinationFolder);
                            f.mkdirs();

                            System.out.println("DESTINATION:" + destination);
                            this.copyFile(source, destination);
                        } else if (line.startsWith("webapp" + File.separator)) {
                            line = line.substring(7); // Remove webapp\
                            components = new ArrayList();
                            components.add(sourceFolderText);
                            components.add("qsupport");
                            components.add(line);
                            String source = StringUtils.join(components, File.separator);
                            File f = new File(source);
                            System.out.println("SOURCE:" + source);
                            if (! f.exists()) {
                                throw new Exception("Source file (" + source + ") does not exist");
                            }
                            if (f.isDirectory()) {
                                continue; // Skip this line if it exists in the manifest due to our error
                            }

                            // To create missing parents directories, we must 
                            components = new ArrayList();
                            components.add(destinationFolderText);
                            components.add("quantros.war");
                            components.add("qsupport");
                            components.add(line);
                            String destination = StringUtils.join(components, File.separator);
                            String destinationFolder = destination.substring(0, destination.lastIndexOf(File.separator));
                            f = new File(destinationFolder);
                            f.mkdirs();

                            System.out.println("DESTINATION:" + destination);
                            this.copyFile(source, destination);

                        }
                    }                    
                    in.close();
                } catch (Exception e) {
                    String stackTrace = ExceptionUtils.getStackTrace(e);
                    System.out.println(stackTrace);
                }
                System.exit(0);
            }
            public void copyFile(String source, String destination) throws Exception {
                // http://www.exampledepot.com/egs/java.nio/File2File.html
                // http://www.exampledepot.com/egs/java.io/CopyFile.html
                FileChannel srcChannel = new FileInputStream(source).getChannel();
                FileChannel dstChannel = new FileOutputStream(destination).getChannel();
                dstChannel.transferFrom(srcChannel, 0, srcChannel.size());
                srcChannel.close();
                dstChannel.close();
            }
        });
        goButton.setToolTipText("Make the quantros.war file and exit");
        JPanel panel4 = new JPanel();
        panel4.setLayout(new FlowLayout());
        panel4.add(goButton);

        this.getContentPane().add(panel1);
        this.getContentPane().add(panel2);
        this.getContentPane().add(panel3);
        this.getContentPane().add(panel4);
    }
    public static void main (String args[]) throws Exception { 
        MakeWarFile win = new MakeWarFile();
        /* Example of adding listener.  Here we use an anonymous class that extends WindowAdapter
        win.addWindowListener(new WindowAdaper() {
            public void windowClosing(WindowEvent e) {
                System.exit(0);
            }
        });
        */
        win.show();
    }
}

http://zetcode.com/tutorials/javaswingtutorial/
http://docs.oracle.com/javase/tutorial/uiswing/layout/visual.html
http://docs.oracle.com/javase/1.4.2/docs/api/javax/swing/JComponent.html
http://docs.oracle.com/javase/1.4.2/docs/api/javax/swing/package-summary.html
http://docs.oracle.com/javase/7/docs/api/java/awt/FlowLayout.html

SwingWorker:
http://en.wikipedia.org/wiki/SwingWorker
http://www.javaworld.com/cgi-bin/mailto/x_java.cgi?pagetosend=/export/home/httpd/javaworld/javaworld/jw-06-2003/jw-0606-swingworker.html&pagename=/javaworld/jw-06-2003/jw-0606-swingworker.html&pageurl=http://www.javaworld.com/javaworld/jw-06-2003/jw-0606-swingworker.html&site=jw_core
http://www.javaswingcomponents.com/blog/taming-swing-threads-part-3-swing-worker
http://www.kodejava.org/examples/381.html
http://albertattard.blogspot.com/2008/09/practical-example-of-swing-worker.html
http://www.java2s.com/Tutorial/Java/0240__Swing/SwingWorkerinJava6.htm
http://groups.google.com/group/comp.lang.java.gui/browse_thread/thread/1592e77d6f74bf5e?pli=1 (The ActionListener can be implemented as part of the main class, which make thing a tiny bit cleaner. Look like one actionPerformed can be used with different buttons.)
http://www.java2s.com/Code/Java/Swing-JFC/SwingworkerExample.htm
http://java.sun.com/developer/technicalArticles/javase/swingworker/

Unless otherwise stated, the content of this page is licensed under Creative Commons Attribution-ShareAlike 3.0 License