Thursday, November 08, 2007

IndeterminateProgressMonitor

I couldn't seem to find a version of javax.swing.ProgressMonitor that provided an indeterminate progress bar. So, I broke down and wrote one.

This is basically a copied and modified version of javax.swing.ProgressMonitor. It requires Java 6 for Dialog.ModalityType.DOCUMENT_MODAL modality, but could easily be tweaked to use regular Java 5 modality. It also depends on Apache Commons Collections and some utility classes, which are also listed below.

None of the accessibility stuff has been tested, but the common use case seems to work well.

package ca.digitalrapids.swing;

import java.awt.BorderLayout;
import java.awt.Component;
import java.awt.Container;
import java.awt.Dialog;
import java.awt.IllegalComponentStateException;
import java.awt.Point;
import java.awt.Rectangle;
import java.awt.Window;
import java.awt.event.WindowAdapter;
import java.awt.event.WindowEvent;
import java.beans.PropertyChangeEvent;
import java.beans.PropertyChangeListener;
import java.util.List;
import java.util.Locale;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.TimeUnit;

import javax.accessibility.Accessible;
import javax.accessibility.AccessibleComponent;
import javax.accessibility.AccessibleContext;
import javax.accessibility.AccessibleRole;
import javax.accessibility.AccessibleState;
import javax.accessibility.AccessibleStateSet;
import javax.accessibility.AccessibleText;
import javax.accessibility.AccessibleValue;
import javax.swing.JDialog;
import javax.swing.JLabel;
import javax.swing.JOptionPane;
import javax.swing.JProgressBar;
import javax.swing.ProgressMonitor;
import javax.swing.SwingUtilities;
import javax.swing.UIManager;
import javax.swing.event.ChangeEvent;
import javax.swing.event.ChangeListener;
import javax.swing.text.AttributeSet;

import org.apache.commons.collections.Predicate;

import ca.digitalrapids.util.concurrent.DRThreadFactory;
import ca.digitalrapids.util.concurrent.DRThreadUtils;

/**
* Version of {@link ProgressMonitor} that uses an indeterminate
* {@link JProgressBar}
*/
public class IndeterminateProgressMonitor extends Object implements Accessible
{
   public interface IntermediateProgressMonitorListener {
       void wasCancelled(IndeterminateProgressMonitor source);
   }
  
   private final class OpenDialogRunnable implements Runnable
   {
       public void run()
       {
           /*
            * Ensuring thread-safety here is tricky because setVisible(true)
            * blocks. See close() for the other half of the solution.
            */
           synchronized (closeOpenLock)
           {
               if ( closed )
                   return;
               dialogOpening = true;
              
               myBar = new JProgressBar();
               myBar.setIndeterminate(true);
               if (note != null)
                   noteLabel = new JLabel(note);
               pane = new ProgressOptionPane(
                   new Object[] { message, noteLabel, myBar });
               dialog = pane.createDialog(parentComponent, title);
           }
          
           dialog.setVisible(true);
       }
   }

   private class ProgressOptionPane extends JOptionPane
   {
       ProgressOptionPane(Object messageList)
       {
           super(messageList, JOptionPane.INFORMATION_MESSAGE, JOptionPane.DEFAULT_OPTION, null,
               IndeterminateProgressMonitor.this.cancelOption, null);
       }

       public int getMaxCharactersPerLineCount()
       {
           return 60;
       }

       // Equivalent to JOptionPane.createDialog,
       // but create a modeless dialog.
       // This is necessary because the Solaris implementation doesn't
       // support Dialog.setModal yet.
       public JDialog createDialog(Component parentComponent, String title)
       {
           final JDialog dialog;

           Window window = parentComponent instanceof Window ?
               (Window)parentComponent :
               SwingUtilities.getWindowAncestor(parentComponent);
              
           dialog = new JDialog(window, title, Dialog.ModalityType.DOCUMENT_MODAL);
          
           /* TODO figure out what this does and fix it
           if (window instanceof SwingUtilities.SharedOwnerFrame)
           {
               WindowListener ownerShutdownListener = (WindowListener) SwingUtilities
                   .getSharedOwnerFrameShutdownListener();
               dialog.addWindowListener(ownerShutdownListener);
           }
           */
          
           Container contentPane = dialog.getContentPane();

           contentPane.setLayout(new BorderLayout());
           contentPane.add(this, BorderLayout.CENTER);
           dialog.pack();
           dialog.setLocationRelativeTo(parentComponent);
           dialog.addWindowListener(new WindowAdapter() {
               boolean gotFocus = false;

               public void windowClosing(WindowEvent we)
               {
                   setValue(cancelOption[0]);
               }

               public void windowActivated(WindowEvent we)
               {
                   // Once window gets focus, set initial focus
                   if (!gotFocus)
                   {
                       selectInitialValue();
                       gotFocus = true;
                   }
               }
           });

           addPropertyChangeListener(new PropertyChangeListener() {
               public void propertyChange(PropertyChangeEvent event)
               {
                   if (dialog.isVisible()
                       && event.getSource() == ProgressOptionPane.this
                       && (event.getPropertyName().equals(VALUE_PROPERTY) || event
                           .getPropertyName().equals(INPUT_VALUE_PROPERTY)))
                   {
                       synchronized (closeOpenLock)
                       {
                           closed = true;
                       }
                       dialog.setVisible(false);
                       dialog.dispose();
                       if ( getValue().equals(cancelOption[0]))
                           fireCancelled();
                   }
               }
           });

           return dialog;
       }

       // ///////////////
       // Accessibility support for ProgressOptionPane
       // //////////////

       /**
        * Gets the AccessibleContext for the ProgressOptionPane
        *
        * @return the AccessibleContext for the ProgressOptionPane
        * @since 1.5
        */
       public AccessibleContext getAccessibleContext()
       {
           return IndeterminateProgressMonitor.this.getAccessibleContext();
       }

       /*
        * Returns the AccessibleJOptionPane
        */
       private AccessibleContext getAccessibleJOptionPane()
       {
           return super.getAccessibleContext();
       }
   }
  
   private JDialog dialog;
   private JOptionPane pane;
   private JProgressBar myBar;
   private JLabel noteLabel;
   private Component parentComponent;
   private String note;
   private Object[] cancelOption = null;
   private Object message;
   private int millisToDecideToPopup = 500;
   private ScheduledExecutorService scheduledExecutorService
       = Executors.newSingleThreadScheduledExecutor(
           new DRThreadFactory(getClass().getName(), Thread.NORM_PRIORITY, true));
   private List listeners =
       new CopyOnWriteArrayList();
   private volatile boolean dialogOpening = false;
   private volatile boolean closed = false;
   /**
    * Used for thread safety for concurrent {@link #close()} and
    * {@link OpenDialogRunnable#run()} invocations.
    */
   private Object closeOpenLock = new Object();
   private final String title;

   /**
    * Constructs a graphic object that shows progress, typically by filling in
    * a rectangular bar as the process nears completion.
    *
    * @param parentComponent
    *            the parent component for the dialog box
    * @param title
    *            the title of the dialog (optional)
    * @param message
    *            a descriptive message that will be shown to the user to
    *            indicate what operation is being monitored. This does not
    *            change as the operation progresses. See the message parameters
    *            to methods in {@link JOptionPane#message} for the range of
    *            values.
    * @param note
    *            a short note describing the state of the operation. As the
    *            operation progresses, you can call setNote to change the note
    *            displayed. This is used, for example, in operations that
    *            iterate through a list of files to show the name of the file
    *            being processes. If note is initially null, there will be no
    *            note line in the dialog box and setNote will be ineffective
    * @param min
    *            the lower bound of the range
    * @param max
    *            the upper bound of the range
    * @see JDialog
    * @see JOptionPane
    */
   public IndeterminateProgressMonitor(Component parentComponent,
       String title, Object message, String note)
   {
       this.title = title == null ?
           UIManager.getString("ProgressMonitor.progressText") :
               title;
      
       this.parentComponent = parentComponent;

       cancelOption = new Object[1];
       cancelOption[0] = UIManager.getString("OptionPane.cancelButtonText");

       this.message = message;
       this.note = note;
       openDialogFuture = scheduledExecutorService.schedule(new OpenDialogRunnable(),
           this.millisToDecideToPopup, TimeUnit.MILLISECONDS);
   }

   public void addListener(IntermediateProgressMonitorListener listener) {
       listeners.add(listener);
   }
  
   private void fireCancelled() {
       for (IntermediateProgressMonitorListener listener : listeners)
       {
           listener.wasCancelled(this);
       }
   }
  
   /**
    * Indicate that the operation is complete. This happens automatically when
    * the value set by setProgress is >= max, but it may be called earlier if
    * the operation ends early.
    */
   public void close()
   {
       /*
        * Ensuring thread-safety here is tricky because setVisible(true)
        * blocks. See OpenRunDialog#run() for the other half of the solution.
        * If the dialog is already opening, we wait for that to complete before
        * executing the close.
        */
      
       synchronized (closeOpenLock)
       {
           if ( closed )
               return;
          
           closed = true;
       }
      
       if ( dialogOpening ) {
           /*
            * Wait for dialog to open
            */
           if ( !DRThreadUtils.sleepyWait(new Predicate() {

               public boolean evaluate(Object object)
               {
                   return dialog != null && dialog.isVisible();
               }

           }) ) {
               throw new RuntimeException("dialog did not become visible");
           }
       }

       if ( openDialogFuture != null ) {
           openDialogFuture.cancel(false);
           openDialogFuture = null;
       }
       if (dialog != null)
       {
           dialog.setVisible(false);
           dialog.dispose();
           dialog = null;
           pane = null;
           myBar = null;
       }
   }
  
   /**
    * Returns true if the user hits the Cancel button in the progress dialog.
    */
   public boolean isCanceled()
   {
       if (pane == null)
           return false;
       Object v = pane.getValue();
       return ((v != null) && (cancelOption.length == 1) && (v.equals(cancelOption[0])));
   }

   /**
    * Specifies the amount of time to wait before deciding whether or not to
    * popup a progress monitor.
    *
    * @param millisToDecideToPopup
    *            an int specifying the time to wait, in milliseconds
    * @see #getMillisToDecideToPopup
    */
   public void setMillisToDecideToPopup(int millisToDecideToPopup)
   {
       this.millisToDecideToPopup = millisToDecideToPopup;
   }

   /**
    * Returns the amount of time this object waits before deciding whether or
    * not to popup a progress monitor.
    *
    * @see #setMillisToDecideToPopup
    */
   public int getMillisToDecideToPopup()
   {
       return millisToDecideToPopup;
   }

   /**
    * Specifies the additional note that is displayed along with the progress
    * message. Used, for example, to show which file the is currently being
    * copied during a multiple-file copy.
    *
    * @param note
    *            a String specifying the note to display
    * @see #getNote
    */
   public void setNote(String note)
   {
       this.note = note;
       if (noteLabel != null)
       {
           noteLabel.setText(note);
       }
   }

   /**
    * Specifies the additional note that is displayed along with the progress
    * message.
    *
    * @return a String specifying the note to display
    * @see #setNote
    */
   public String getNote()
   {
       return note;
   }

   // ///////////////
   // Accessibility support
   // //////////////

   /**
    * The AccessibleContext for the ProgressMonitor
    *
    * @since 1.5
    */
   protected AccessibleContext accessibleContext = null;

   private AccessibleContext accessibleJOptionPane = null;
   private ScheduledFuture< ? > openDialogFuture;

   /**
    * Gets the AccessibleContext for the
    * ProgressMonitor
    *
    * @return the AccessibleContext for the
    *         ProgressMonitor
    * @since 1.5
    */
   public AccessibleContext getAccessibleContext()
   {
       if (accessibleContext == null)
       {
           accessibleContext = new AccessibleIntermediateProgressMonitor();
       }
       if (pane != null && accessibleJOptionPane == null)
       {
           // Notify the AccessibleProgressMonitor that the
           // ProgressOptionPane was created. It is necessary
           // to poll for ProgressOptionPane creation because
           // the ProgressMonitor does not have a Component
           // to add a listener to until the ProgressOptionPane
           // is created.
           if (accessibleContext instanceof AccessibleIntermediateProgressMonitor)
           {
               ((AccessibleIntermediateProgressMonitor) accessibleContext).optionPaneCreated();
           }
       }
       return accessibleContext;
   }

   /**
    * AccessibleProgressMonitor implements accessibility support
    * for the ProgressMonitor class.
    *
    * @since 1.5
    */
   protected class AccessibleIntermediateProgressMonitor extends AccessibleContext implements AccessibleText,
       ChangeListener, PropertyChangeListener
   {

       /*
        * The accessibility hierarchy for ProgressMonitor is a flattened
        * version of the ProgressOptionPane component hierarchy.
        *
        * The ProgressOptionPane component hierarchy is: JDialog
        * ProgressOptionPane JPanel JPanel JLabel JLabel JProgressBar
        *
        * The AccessibleProgessMonitor accessibility hierarchy is:
        * AccessibleJDialog AccessibleProgressMonitor AccessibleJLabel
        * AccessibleJLabel AccessibleJProgressBar
        *
        * The abstraction presented to assitive technologies by the
        * AccessibleProgressMonitor is that a dialog contains a progress
        * monitor with three children: a message, a note label and a progress
        * bar.
        */

       private Object oldModelValue;

       /**
        * AccessibleProgressMonitor constructor
        */
       protected AccessibleIntermediateProgressMonitor()
       {
       }

       /*
        * Initializes the AccessibleContext now that the ProgressOptionPane has
        * been created. Because the ProgressMonitor is not a Component
        * implementing the Accessible interface, an AccessibleContext must be
        * synthesized from the ProgressOptionPane and its children.
        *
        * For other AWT and Swing classes, the inner class that implements
        * accessibility for the class extends the inner class that implements
        * implements accessibility for the super class.
        * AccessibleProgressMonitor cannot extend AccessibleJOptionPane and
        * must therefore delegate calls to the AccessibleJOptionPane.
        */
       private void optionPaneCreated()
       {
           accessibleJOptionPane = ((ProgressOptionPane) pane).getAccessibleJOptionPane();

           // add a listener for progress bar ChangeEvents
           if (myBar != null)
           {
               myBar.addChangeListener(this);
           }

           // add a listener for note label PropertyChangeEvents
           if (noteLabel != null)
           {
               noteLabel.addPropertyChangeListener(this);
           }
       }

       /**
        * Invoked when the target of the listener has changed its state.
        *
        * @param e
        *            a ChangeEvent object. Must not be null.
        * @throws NullPointerException
        *             if the parameter is null.
        */
       public void stateChanged(ChangeEvent e)
       {
           if (e == null)
           {
               return;
           }
           if (myBar != null)
           {
               // the progress bar value changed
               Object newModelValue = myBar.getValue();
               firePropertyChange(ACCESSIBLE_VALUE_PROPERTY, oldModelValue, newModelValue);
               oldModelValue = newModelValue;
           }
       }

       /**
        * This method gets called when a bound property is changed.
        *
        * @param e
        *            A PropertyChangeEvent object describing the
        *            event source and the property that has changed. Must not
        *            be null.
        * @throws NullPointerException
        *             if the parameter is null.
        */
       public void propertyChange(PropertyChangeEvent e)
       {
           if (e.getSource() == noteLabel && e.getPropertyName() == "text")
           {
               // the note label text changed
               firePropertyChange(ACCESSIBLE_TEXT_PROPERTY, null, 0);
           }
       }

       /* ===== Begin AccessileContext ===== */

       /**
        * Gets the accessibleName property of this object. The accessibleName
        * property of an object is a localized String that designates the
        * purpose of the object. For example, the accessibleName property of a
        * label or button might be the text of the label or button itself. In
        * the case of an object that doesn't display its name, the
        * accessibleName should still be set. For example, in the case of a
        * text field used to enter the name of a city, the accessibleName for
        * the en_US locale could be 'city.'
        *
        * @return the localized name of the object; null if this object does
        *         not have a name
        *
        * @see #setAccessibleName
        */
       public String getAccessibleName()
       {
           if (accessibleName != null)
           { // defined in AccessibleContext
               return accessibleName;
           }
           else if (accessibleJOptionPane != null)
           {
               // delegate to the AccessibleJOptionPane
               return accessibleJOptionPane.getAccessibleName();
           }
           return null;
       }

       /**
        * Gets the accessibleDescription property of this object. The
        * accessibleDescription property of this object is a short localized
        * phrase describing the purpose of the object. For example, in the case
        * of a 'Cancel' button, the accessibleDescription could be 'Ignore
        * changes and close dialog box.'
        *
        * @return the localized description of the object; null if this object
        *         does not have a description
        *
        * @see #setAccessibleDescription
        */
       public String getAccessibleDescription()
       {
           if (accessibleDescription != null)
           { // defined in AccessibleContext
               return accessibleDescription;
           }
           else if (accessibleJOptionPane != null)
           {
               // delegate to the AccessibleJOptionPane
               return accessibleJOptionPane.getAccessibleDescription();
           }
           return null;
       }

       /**
        * Gets the role of this object. The role of the object is the generic
        * purpose or use of the class of this object. For example, the role of
        * a push button is AccessibleRole.PUSH_BUTTON. The roles in
        * AccessibleRole are provided so component developers can pick from a
        * set of predefined roles. This enables assistive technologies to
        * provide a consistent interface to various tweaked subclasses of
        * components (e.g., use AccessibleRole.PUSH_BUTTON for all components
        * that act like a push button) as well as distinguish between sublasses
        * that behave differently (e.g., AccessibleRole.CHECK_BOX for check
        * boxes and AccessibleRole.RADIO_BUTTON for radio buttons).
        * 
        * Note that the AccessibleRole class is also extensible, so custom
        * component developers can define their own AccessibleRole's if the set
        * of predefined roles is inadequate.
        *
        * @return an instance of AccessibleRole describing the role of the
        *         object
        * @see AccessibleRole
        */
       public AccessibleRole getAccessibleRole()
       {
           return AccessibleRole.PROGRESS_MONITOR;
       }

       /**
        * Gets the state set of this object. The AccessibleStateSet of an
        * object is composed of a set of unique AccessibleStates. A change in
        * the AccessibleStateSet of an object will cause a PropertyChangeEvent
        * to be fired for the ACCESSIBLE_STATE_PROPERTY property.
        *
        * @return an instance of AccessibleStateSet containing the current
        *         state set of the object
        * @see AccessibleStateSet
        * @see AccessibleState
        * @see #addPropertyChangeListener
        */
       public AccessibleStateSet getAccessibleStateSet()
       {
           if (accessibleJOptionPane != null)
           {
               // delegate to the AccessibleJOptionPane
               return accessibleJOptionPane.getAccessibleStateSet();
           }
           return null;
       }

       /**
        * Gets the Accessible parent of this object.
        *
        * @return the Accessible parent of this object; null if this object
        *         does not have an Accessible parent
        */
       public Accessible getAccessibleParent()
       {
           if (dialog != null)
           {
               return dialog;
           }
           return null;
       }

       /**
        * Gets the 0-based index of this object in its accessible parent.
        *
        * @return the 0-based index of this object in its parent; -1 if this
        *         object does not have an accessible parent.
        *
        * @see #getAccessibleParent
        * @see #getAccessibleChildrenCount
        * @see #getAccessibleChild
        */
       public int getAccessibleIndexInParent()
       {
           if (accessibleJOptionPane != null)
           {
               // delegate to the AccessibleJOptionPane
               return accessibleJOptionPane.getAccessibleIndexInParent();
           }
           return -1;
       }

       /**
        * Returns the number of accessible children of the object.
        *
        * @return the number of accessible children of the object.
        */
       public int getAccessibleChildrenCount()
       {
           // return the number of children in the JPanel containing
           // the message, note label and progress bar
           AccessibleContext ac = getPanelAccessibleContext();
           if (ac != null)
           {
               return ac.getAccessibleChildrenCount();
           }
           return 0;
       }

       /**
        * Returns the specified Accessible child of the object. The Accessible
        * children of an Accessible object are zero-based, so the first child
        * of an Accessible child is at index 0, the second child is at index 1,
        * and so on.
        *
        * @param i
        *            zero-based index of child
        * @return the Accessible child of the object
        * @see #getAccessibleChildrenCount
        */
       public Accessible getAccessibleChild(int i)
       {
           // return a child in the JPanel containing the message, note label
           // and progress bar
           AccessibleContext ac = getPanelAccessibleContext();
           if (ac != null)
           {
               return ac.getAccessibleChild(i);
           }
           return null;
       }

       /*
        * Returns the AccessibleContext for the JPanel containing the message,
        * note label and progress bar
        */
       private AccessibleContext getPanelAccessibleContext()
       {
           if (myBar != null)
           {
               Component c = myBar.getParent();
               if (c instanceof Accessible)
               {
                   return ((Accessible) c).getAccessibleContext();
               }
           }
           return null;
       }

       /**
        * Gets the locale of the component. If the component does not have a
        * locale, then the locale of its parent is returned.
        *
        * @return this component's locale. If this component does not have a
        *         locale, the locale of its parent is returned.
        *
        * @exception IllegalComponentStateException
        *                If the Component does not have its own locale and has
        *                not yet been added to a containment hierarchy such
        *                that the locale can be determined from the containing
        *                parent.
        */
       public Locale getLocale() throws IllegalComponentStateException
       {
           if (accessibleJOptionPane != null)
           {
               // delegate to the AccessibleJOptionPane
               return accessibleJOptionPane.getLocale();
           }
           return null;
       }

       /* ===== end AccessibleContext ===== */

       /**
        * Gets the AccessibleComponent associated with this object that has a
        * graphical representation.
        *
        * @return AccessibleComponent if supported by object; else return null
        * @see AccessibleComponent
        */
       public AccessibleComponent getAccessibleComponent()
       {
           if (accessibleJOptionPane != null)
           {
               // delegate to the AccessibleJOptionPane
               return accessibleJOptionPane.getAccessibleComponent();
           }
           return null;
       }

       /**
        * Gets the AccessibleValue associated with this object that supports a
        * Numerical value.
        *
        * @return AccessibleValue if supported by object; else return null
        * @see AccessibleValue
        */
       public AccessibleValue getAccessibleValue()
       {
           if (myBar != null)
           {
               // delegate to the AccessibleJProgressBar
               return myBar.getAccessibleContext().getAccessibleValue();
           }
           return null;
       }

       /**
        * Gets the AccessibleText associated with this object presenting text
        * on the display.
        *
        * @return AccessibleText if supported by object; else return null
        * @see AccessibleText
        */
       public AccessibleText getAccessibleText()
       {
           if (getNoteLabelAccessibleText() != null)
           {
               return this;
           }
           return null;
       }

       /*
        * Returns the note label AccessibleText
        */
       private AccessibleText getNoteLabelAccessibleText()
       {
           if (noteLabel != null)
           {
               // AccessibleJLabel implements AccessibleText if the
               // JLabel contains HTML text
               return noteLabel.getAccessibleContext().getAccessibleText();
           }
           return null;
       }

       /* ===== Begin AccessibleText impl ===== */

       /**
        * Given a point in local coordinates, return the zero-based index of
        * the character under that Point. If the point is invalid, this method
        * returns -1.
        *
        * @param p
        *            the Point in local coordinates
        * @return the zero-based index of the character under Point p; if Point
        *         is invalid return -1.
        */
       public int getIndexAtPoint(Point p)
       {
           AccessibleText at = getNoteLabelAccessibleText();
           if (at != null && sameWindowAncestor(pane, noteLabel))
           {
               // convert point from the option pane bounds
               // to the note label bounds.
               Point noteLabelPoint = SwingUtilities.convertPoint(pane, p, noteLabel);
               if (noteLabelPoint != null)
               {
                   return at.getIndexAtPoint(noteLabelPoint);
               }
           }
           return -1;
       }

       /**
        * Determines the bounding box of the character at the given index into
        * the string. The bounds are returned in local coordinates. If the
        * index is invalid an empty rectangle is returned.
        *
        * @param i
        *            the index into the String
        * @return the screen coordinates of the character's bounding box, if
        *         index is invalid return an empty rectangle.
        */
       public Rectangle getCharacterBounds(int i)
       {
           AccessibleText at = getNoteLabelAccessibleText();
           if (at != null && sameWindowAncestor(pane, noteLabel))
           {
               // return rectangle in the option pane bounds
               Rectangle noteLabelRect = at.getCharacterBounds(i);
               if (noteLabelRect != null)
               {
                   return SwingUtilities.convertRectangle(noteLabel, noteLabelRect, pane);
               }
           }
           return null;
       }

       /*
        * Returns whether source and destination components have the same
        * window ancestor
        */
       private boolean sameWindowAncestor(Component src, Component dest)
       {
           if (src == null || dest == null)
           {
               return false;
           }
           return SwingUtilities.getWindowAncestor(src) == SwingUtilities.getWindowAncestor(dest);
       }

       /**
        * Returns the number of characters (valid indicies)
        *
        * @return the number of characters
        */
       public int getCharCount()
       {
           AccessibleText at = getNoteLabelAccessibleText();
           if (at != null)
           { // JLabel contains HTML text
               return at.getCharCount();
           }
           return -1;
       }

       /**
        * Returns the zero-based offset of the caret.
        *
        * Note: That to the right of the caret will have the same index value
        * as the offset (the caret is between two characters).
        *
        * @return the zero-based offset of the caret.
        */
       public int getCaretPosition()
       {
           AccessibleText at = getNoteLabelAccessibleText();
           if (at != null)
           { // JLabel contains HTML text
               return at.getCaretPosition();
           }
           return -1;
       }

       /**
        * Returns the String at a given index.
        *
        * @param part
        *            the CHARACTER, WORD, or SENTENCE to retrieve
        * @param index
        *            an index within the text
        * @return the letter, word, or sentence
        */
       public String getAtIndex(int part, int index)
       {
           AccessibleText at = getNoteLabelAccessibleText();
           if (at != null)
           { // JLabel contains HTML text
               return at.getAtIndex(part, index);
           }
           return null;
       }

       /**
        * Returns the String after a given index.
        *
        * @param part
        *            the CHARACTER, WORD, or SENTENCE to retrieve
        * @param index
        *            an index within the text
        * @return the letter, word, or sentence
        */
       public String getAfterIndex(int part, int index)
       {
           AccessibleText at = getNoteLabelAccessibleText();
           if (at != null)
           { // JLabel contains HTML text
               return at.getAfterIndex(part, index);
           }
           return null;
       }

       /**
        * Returns the String before a given index.
        *
        * @param part
        *            the CHARACTER, WORD, or SENTENCE to retrieve
        * @param index
        *            an index within the text
        * @return the letter, word, or sentence
        */
       public String getBeforeIndex(int part, int index)
       {
           AccessibleText at = getNoteLabelAccessibleText();
           if (at != null)
           { // JLabel contains HTML text
               return at.getBeforeIndex(part, index);
           }
           return null;
       }

       /**
        * Returns the AttributeSet for a given character at a given index
        *
        * @param i
        *            the zero-based index into the text
        * @return the AttributeSet of the character
        */
       public AttributeSet getCharacterAttribute(int i)
       {
           AccessibleText at = getNoteLabelAccessibleText();
           if (at != null)
           { // JLabel contains HTML text
               return at.getCharacterAttribute(i);
           }
           return null;
       }

       /**
        * Returns the start offset within the selected text. If there is no
        * selection, but there is a caret, the start and end offsets will be
        * the same.
        *
        * @return the index into the text of the start of the selection
        */
       public int getSelectionStart()
       {
           AccessibleText at = getNoteLabelAccessibleText();
           if (at != null)
           { // JLabel contains HTML text
               return at.getSelectionStart();
           }
           return -1;
       }

       /**
        * Returns the end offset within the selected text. If there is no
        * selection, but there is a caret, the start and end offsets will be
        * the same.
        *
        * @return the index into teh text of the end of the selection
        */
       public int getSelectionEnd()
       {
           AccessibleText at = getNoteLabelAccessibleText();
           if (at != null)
           { // JLabel contains HTML text
               return at.getSelectionEnd();
           }
           return -1;
       }

       /**
        * Returns the portion of the text that is selected.
        *
        * @return the String portion of the text that is selected
        */
       public String getSelectedText()
       {
           AccessibleText at = getNoteLabelAccessibleText();
           if (at != null)
           { // JLabel contains HTML text
               return at.getSelectedText();
           }
           return null;
       }
       /* ===== End AccessibleText impl ===== */
   }
   // inner class AccessibleProgressMonitor

}

/*
* MyThreadFactory.java
*
* Created on September 13, 2005, 12:07 PM
*
*/

package ca.digitalrapids.util.concurrent;

import java.util.concurrent.*;

/** Provides for setting various thread parameters
* @author craig.white
*/
public class DRThreadFactory implements ThreadFactory {
   private int          priority = Thread.NORM_PRIORITY;
   private boolean      isDaemon = false;
   private volatile int counter  = 0;
   private String       nameBase = "DRThread";
  
   public DRThreadFactory( String nameBase, int priority, boolean isDaemon ) {
       this.nameBase = nameBase;
       this.priority = priority;
       this.isDaemon = isDaemon;
   }

   public Thread newThread(Runnable runnable) {
       Thread t = new Thread( runnable, nameBase+"-"+(++counter) );
       t.setDaemon( isDaemon );
       t.setPriority( priority );
       return t;
   }
  
}

package ca.digitalrapids.util.concurrent;

import java.util.concurrent.TimeoutException;

import org.apache.commons.collections.Predicate;
import org.apache.commons.lang.time.StopWatch;

/**
 * Collection of {@link Thread} related utility methods
 */
public abstract class DRThreadUtils
{
    /** Condition that evaluates <code>true</code>/<code>false</code>
     * @param <T> parameter to the condition
     */
    public static interface Condition<T> {
        boolean evaluate(T param);
    }
    
    /** Synchronizes and waits on {@code object} until a return condition
     * evaluates <code>true</code>, or optionally a timeout occurs.
     * @param <T> type of the object to wait on
     * @param object the object to wait on
     * @param timeout_ms (optional) wait timeout
     * @param returnCondition condition that returns true when the wait should end
     * @return <code>false</code> on a timeout, <code>true</code> otherwise
     * @throws InterruptedException if the thread is interrupted
     */
    public static <T> boolean wait(T object, Integer timeout_ms, 
        Condition<T> returnCondition) throws InterruptedException 
    {
        boolean result;
        StopWatch timeoutStopWatch = new StopWatch();
        timeoutStopWatch.start();
        synchronized (object)
        {
            while ( !(result = returnCondition.evaluate(object))
                && ( timeout_ms == null
                     || timeoutStopWatch.getTime() < timeout_ms ) ) 
            {
                if ( timeout_ms == null )
                    object.wait();
                else {
                    long timeout = timeout_ms - timeoutStopWatch.getTime();
                    if (timeout <= 0) return false;
                    object.wait(timeout);
                }
            }
        }
        
        return result;
    }

    /**
     * For {@link DRThreadUtils#sleep(long, ca.digitalrapids.util.concurrent.DRThreadUtils.InterruptAction)}
     */
    static public enum InterruptAction {RETURN, THROW_RUNTIME}
    
    /** Does a {@link Thread#sleep(long)}, taking the specified action on
     * an interruption.  Regardless of the specified action, this method
     * will re-interrupt the current thread on an {@link InterruptedException}.
     * @param millis
     * @param action
     */
    static public void sleep(long millis, InterruptAction action) {
        try
        {
            Thread.sleep(millis);
        }
        catch (InterruptedException e)
        {
            Thread.currentThread().interrupt();
            switch(action) {
            case RETURN:
                return;
            case THROW_RUNTIME:
                throw new RuntimeException(e);
            }
        }
    }
    
    /** Calls {@link #sleepyWait(Predicate, Object, String, Integer, 
     * ca.digitalrapids.util.concurrent.DRThreadUtils.InterruptAction)} with
     * default values. 
     * @param predicate
     * @throws TimeoutException
     * @return <code>false</code> on a timeout, <code>true</code> otherwise
     */
    static public boolean sleepyWait(Predicate predicate)
    {
        return sleepyWait(predicate, null, null, null, null);
    }
    
    /** Waits on a predicate by sleeping and retrying
     * @param predicate condition to wait upon
     * @param parameter (optional) parameter for predicate
     * @param timeout_ms (optional) milliseconds before a timeout should occur. 
     * Default is 5000.
     * @param interruptAction (optional) action to take on thread interrupt.
     * Default is {@link InterruptAction#RETURN}
     * @param retryPeriod (optional) milliseconds to sleep for between retries.
     * Default is 500.
     * @return the result of the predicate, regardless of whether completion is
     * due to timeout, interrupt, or normal completion
     */
    static public boolean sleepyWait(Predicate predicate, Object parameter,
        Long timeout_ms, InterruptAction interruptAction, Integer retryPeriod_ms)
    {
        if ( timeout_ms == null ) timeout_ms = 5000L;
        if ( retryPeriod_ms == null ) retryPeriod_ms = 500;
        if ( interruptAction == null ) interruptAction = InterruptAction.RETURN;
        StopWatch stopWatch = new StopWatch();
        stopWatch.start();
        while ( !Thread.currentThread().isInterrupted()
            && !predicate.evaluate(parameter) ) 
        {
            long timeToTimeout = timeout_ms - stopWatch.getTime();
            if ( timeToTimeout <= 0 )
                return false;
            long sleepTime = timeToTimeout < retryPeriod_ms ? 
                timeToTimeout : retryPeriod_ms;
            if ( sleepTime > 0 )
                DRThreadUtils.sleep(sleepTime, interruptAction);
        }
        return predicate.evaluate(parameter);
    }
}

8 Comments:

Blogger Unknown said...

This comment has been removed by the author.

5:06 AM  
Blogger Unknown said...

This comment has been removed by the author.

8:09 AM  
Blogger Unknown said...

Hi,

This code is just what I'm looking for but there are a couple of errors. In the sleepyWait method of the DRUtils class the while loop has an error in the code at long sleepTime = timeToTimeout <> 0);.

Would love to get this working so any hints would be much appreciated.

All the best

Chris

8:11 AM  
Blogger Unknown said...

How can this compile? T isn't declared anywhere?

package ca.digitalrapids.util.concurrent;

import java.util.concurrent.TimeoutException;

import org.apache.commons.collections.Predicate;
import org.apache.commons.lang.time.StopWatch;

/**
* Collection of {@link Thread} related utility methods
*/
public abstract class DRThreadUtils
{
public static interface Condition {
boolean evaluate(T param);
}

8:20 AM  
Blogger Unknown said...

Hi Jason,

It's a generic interface. More info here: http://java.sun.com/docs/books/tutorial/extra/generics/literals.html

All the best

Chris

10:12 AM  
Blogger Unknown said...

Generic interfaces are declared with the greater than and less than signs like so:

Condition. Maybe it's an html thing, but I didn't see T declared. I also have the same compile problem that Christopher has. Can you post the actual .java code instead of posting the text?

11:01 AM  
Blogger Unknown said...

Ok, this posting mechanism isn't letting me post generic properly, maybe this will work:

Condition<T>

Ok, so it's an HTML thing. posting your source code will surely alleviate the issues that Christopher and I had.

11:03 AM  
Blogger Kev said...

I reposted the DRThreadUtils class with the HTML escaping fixed (hopefully). I think the bug Christopher mentioned might be fixed too, but I haven't looked at this file in a while, so I'm not sure.

I couldn't find a good way to post my .java files on Blogger directly, and seem to be too lazy right now to post them on a file-hosting site.

1:17 PM  

Post a Comment

<< Home