August 3rd, 2009

Using Eclipse jobs as Trident scenario actors

Timeline scenarios in the Trident animation library allow combining multiple timeline scenario actors in a parallel, sequential or custom order. Out of the box, Trident supports timelines, extensions of Runnable class and extensions of SwingWorker class, but the applications can easily create a custom implementation of the TimelineScenarioActor interface.

SwingWorker is an indispensable tool in the arsenal of a Swing programmer, and allows separating the long-running tasks that run on background threads from updating the UI components that must happen on the Event Dispatch Thread. While SWT does not have a direct counterpart, Eclipse core libraries have a very similar concept in Eclipse Jobs. And while the full functionality of Eclipse Jobs allows arbitrary dependencies and fine grained scheduling of interrelated jobs, you can wrap an Eclipse Job as a custom Trident timeline scenario actor and use the TimelineScenario APIs to seamlessly integrate an Eclipse Job in your Trident-powered SWT application.

Here is the complete source code to do this:

import org.eclipse.core.runtime.jobs.*;
import org.pushingpixels.trident.TimelineScenario.TimelineScenarioActor;
 
public abstract class EclipseJobTimelineScenarioActor extends Job implements
      TimelineScenarioActor {
   volatile transient boolean isDone = false;
 
   public EclipseJobTimelineScenarioActor(String name) {
      super(name);
      this.addJobChangeListener(new JobChangeAdapter() {
         @Override
         public void done(IJobChangeEvent event) {
            isDone = true;
         }
      });
   }
 
   @Override
   public boolean isDone() {
      return isDone && (this.getState() == Job.NONE);
   }
 
   @Override
   public void play() {
      this.schedule();
   }
 
   @Override
   public void resetDoneFlag() {
      throw new UnsupportedOperationException();
   }
 
   @Override
   public boolean supportsReplay() {
      return false;
   }
}

The play() method calls Job.schedule(), scheduling it for immediate execution. The isDone() method is called internally by the Trident engine on every pulse. The implementation registers a JobChangeListener to track the state of the job, and returns the relevant boolean value. Just as with SwingWorkers, the supportsReplay() returns false, and resetDoneFlag() throws an exception.

The timeline scenario below has the following steps which run in a sequential fashion:

  1. Load an image from the specified URL.
  2. Scale it to fit the specified area
  3. Fade it in on the screen

The first step is done using our EclipseJobTimelineScenarioActor (which would be done with a TimelineSwingWorker in a Swing application):

   private TimelineScenario getLoadImageScenario(final Item albumItem) {
      TimelineScenario loadScenario = new TimelineScenario.Sequence();
 
      // load the image
      EclipseJobTimelineScenarioActor imageLoadWorker = new EclipseJobTimelineScenarioActor(
            "Load image") {
         @Override
         protected IStatus run(IProgressMonitor monitor) {
            try {
               URL url = new URL(albumItem.getMediumImage().getURL());
               image = new Image(Display.getDefault(), url.openStream());
               return Status.OK_STATUS;
            } catch (Throwable t) {
               t.printStackTrace();
               return Status.CANCEL_STATUS;
            }
         }
      };
      loadScenario.addScenarioActor(imageLoadWorker);
 
      // scale if necessary
      TimelineRunnable scaler = new TimelineRunnable() {
         @Override
         public void run() {
            if (image != null) {
               float vFactor = (float) OVERVIEW_IMAGE_DIM
                     / (float) image.getImageData().height;
               float hFactor = (float) OVERVIEW_IMAGE_DIM
                     / (float) image.getImageData().width;
               float factor = Math.min(1.0f, Math.min(vFactor, hFactor));
               if (factor < 1.0f) {
                  // scaled to fit available area
                  image = GraniteUtils.getScaledInstance(image,
                        (int) (factor * image.getImageData().width),
                        (int) (factor * image.getImageData().height));
               }
 
               imageLoadedDone = true;
            }
         }
      };
      loadScenario.addScenarioActor(scaler);
 
      // and fade it in
      Timeline imageFadeInTimeline = new Timeline(AlbumOverviewComponent.this);
      imageFadeInTimeline.addPropertyToInterpolate("imageAlpha", 0.0f, 1.0f);
      imageFadeInTimeline.addCallback(new SWTRepaintCallback(
            AlbumOverviewComponent.this));
      imageFadeInTimeline.setDuration(500);
      loadScenario.addScenarioActor(imageFadeInTimeline);
 
      return loadScenario;
   }

You will need the following Eclipse libraries in your classpath:

  • org.eclipse.core.jobs
  • org.eclipse.equinox.common
  • org.eclipse.osgi

As you can see, it is quite easy to extend the existing functionality of Trident scenarios by wrapping external modules as custom timeline scenario actors.