In addition to the usual bug fixes and getting the internal implementation ready for the changes coming in the next major release, Substance 5.3 (code-named Reykjavik) will introduce three new skins. The brand new Gemini skin is joining Magellan, and to use it in your application, you have the following options:

  • -Dswing.defaultlaf=org.jvnet.substance.api.skin.SubstanceGeminiLookAndFeel
  • UIManager.setLookAndFeel(new SubstanceGeminiLookAndFeel())
  • UIManager.setLookAndFeel("org.jvnet.substance.api.skin.SubstanceGeminiLookAndFeel")
  • SubstanceLookAndFeel.setSkin(new GeminiSkin())

Here are a few screenshots that show this new skin. A small frame with a tabbed pane and a few different controls:

Highlights on selected items in lists and trees:

A frame with menu bar, tool bar and status bar from SwingX project:

A thumbnail of the main Substance test application (click for full-size view):

Error dialog from SwingX components:

Login dialog from SwingX components:

A few sliders:

As with all Substance core skins, this is work in progress and will be polished over time. The Gemini skin will be officially available in the next few days as part of the 5.3 release candidate.

In addition to the usual bug fixes and getting the internal implementation ready for the changes coming in the next major release, Substance 5.3 (code-named Reykjavik) will introduce three new skins. The first is Magellan which is based on the design of the Ballpark website (found via Hongkiat) and is reminiscent of the vibrant colors of the Windows XP taskbar ans Start menu.

To use it in your application, you have the following options:

  • -Dswing.defaultlaf=org.jvnet.substance.api.skin.SubstanceMagellanLookAndFeel
  • UIManager.setLookAndFeel(new SubstanceMagellanLookAndFeel())
  • UIManager.setLookAndFeel("org.jvnet.substance.api.skin.SubstanceMagellanLookAndFeel")
  • SubstanceLookAndFeel.setSkin(new MagellanSkin())

Here are a few screenshots that show this new skin. A small frame with a tabbed pane and a few different controls:

Highlights on selected items in lists and trees:

A frame with menu bar, tool bar and status bar from SwingX project:

A thumbnail of the main Substance test application (click for full-size view):

Error dialog from SwingX components:

Login dialog from SwingX components:

A few sliders:

As with all Substance core skins, this is work in progress and will be polished over time. The Magellan skin will be officially available in the next few days as part of the 5.3 release candidate.

Concluding the series on adding animations to enable rich interactivity expected from modern SWT applications, here is what we have seen so far:

  • Part 1 – adding simple animation behavior to such scenarios as component appearance (fade in) and window disposal (fade-out) using built in and custom class attributes and setters.
  • Part 2 – adding animated load progress indication while the application is loading data.
  • Part 3 – loading the album art matching the specific search string and asynchronously displaying the associated images.
  • Part 4 – scrolling the album covers showed in the container and adding animations to the scrolling.
  • Part 5 – complex transition scenarios.

How can you run this code locally?

  • Get the latest SVN snapshots of Trident and Granite
  • Add the following Eclipse libraries to the project: org.eclipse.core.jobs, org.eclipse.equinox.common, org.eclipse.osgi
  • The Granite distribution contains the lib/amazon.jar. It has been created with the following steps:
    • wsimport -d ./build -s ./src -p com.ECS.client.jax http://ecs.amazonaws.com/AWSECommerceService/AWSECommerceService.wsdl .
    • jar cvf ../amazon.jar .
  • Get an Amazon E-commerce key
  • Run the org.pushingpixels.granite.DemoApp class, passing your Amazon key as the only parameter to this class, adding the Amazon, Trident and Granite classes, as well as Eclipse jars to the classpath

If all went right, you should see the main application running and displaying Sarah McLachlan albums as in this video:

I hope you enjoyed this series. If you’re interested in adding rich animations to your SWT applications, you’re more than welcome to explore Trident and Granite and report any bugs and missing features in the project forums and mailing lists.

After adding such animation effects as fading, load progress, asynchronous load of images and smooth scrolling in the application window connected to the Amazon backend, it’s time to talk about more complex transition scenarios. In this entry i’m going to talk about displaying larger album art and scrollable track listing when the specific album is selected, along with a complex transition between selected albums. This code is part of the Granite project which aims to provide blueprints for animated SWT applications powered by the Trident animation library.

Here is a screenshot that illustrates the detailed view of the selected album (and you can view the videos in the first part of this series):

https://pushingpixels.dev.java.net/images/granite/albumdetails.png

The full sources of this view are in the SVN repository, and i’m going to talk about the full transition scenario that is played when the user selects a specific album. This scenario has six steps:

  1. (Relevant when the details window already shows album art) – collapse the album art component and track listing component to fully overlap.
  2. (In parallel with step 1) – load the new album art from the Internet (based on the URL returned from the original Amazon E-commerce request).
  3. (After steps 1 and 2 have both completed) – set the loaded album art on the album art component. This may also cause resizing the album art if it cannot fully fit in the available space.
  4. (In parallel with step 3) – set the list of album tracks on the track listing component.
  5. (After steps 3 and 4 have both completed) – cross fade the old album art to the new album art.
  6. (After step 5 has been completed) – move the album art component (that displays the new album art) and the track listing to be displayed side by side.

To implement this complex timeline scenario, the code uses the rendezvous timeline scenario provided by Trident. Timeline.RendezvousSequence allows simple branch-and-wait ordering. The rendezvous scenario has a stage-like approach. All actors belonging to the same stage run in parallel, while actors in stage N+1 wait for all actors in stage N to be finished. The RendezvousSequence.rendezvous marks the end of one stage and the beginning of another.

Here is how the code looks like:

   /**
    * Returns the timeline scenario that implements a transition from the
    * currently shown album item (which may be null) to the
    * specified album item.
    *
    * @param albumItem
    *            The new album item to be shown in this window.
    * @return The timeline scenario that implements a transition from the
    *         currently shown album item (which may be null) to
    *         the specified album item.
    */
   private TimelineScenario getShowAlbumDetailsScenario(final Item albumItem) {
      TimelineScenario.RendezvousSequence scenario = new TimelineScenario.RendezvousSequence();

      // step 1 - move album art and track listing to the same location
      Timeline collapseArtAndTracks = new Timeline(this);
      collapseArtAndTracks.addPropertyToInterpolate("overlayPosition",
            this.overlayPosition, 0.0f);
      collapseArtAndTracks.addCallback(new UIThreadTimelineCallbackAdapter() {
         int startingRegionWidth;

         @Override
         public void onTimelineStateChanged(TimelineState oldState,
               TimelineState newState, float durationFraction,
               float timelinePosition) {
            if (newState == TimelineState.READY) {
               startingRegionWidth = (int) (BigAlbumArt.TOTAL_DIM * (1 + overlayPosition));
            }
            updateShellRegion(timelinePosition);
         }

         @Override
         public void onTimelinePulse(float durationFraction,
               float timelinePosition) {
            updateShellRegion(timelinePosition);
         }

         private void updateShellRegion(float timelinePosition) {
            int regionWidth = (int) (startingRegionWidth + timelinePosition
                  * (BigAlbumArt.TOTAL_DIM - startingRegionWidth));
            Region newRegion = new Region();
            newRegion.add(BigAlbumArt.TOTAL_DIM - regionWidth / 2, 0,
                  regionWidth, BigAlbumArt.TOTAL_DIM);
            Shell shell = getShell();
            if (!shell.isDisposed())
               getShell().setRegion(newRegion);
         }
      });
      collapseArtAndTracks.setDuration((int) (500 * this.overlayPosition));
      scenario.addScenarioActor(collapseArtAndTracks);

      // step 2 (in parallel) - load the new album art
      final Image[] albumArtHolder = new Image[1];
      EclipseJobTimelineScenarioActor loadNewAlbumArt = new EclipseJobTimelineScenarioActor(
            "Load album art") {
         @Override
         protected org.eclipse.core.runtime.IStatus run(
               org.eclipse.core.runtime.IProgressMonitor arg0) {
            try {
               URL url = new URL(albumItem.getLargeImage().getURL());
               albumArtHolder[0] = new Image(Display.getDefault(), url
                     .openStream());
               return Status.OK_STATUS;
            } catch (Throwable t) {
               t.printStackTrace();
               return Status.CANCEL_STATUS;
            }
         }
      };
      scenario.addScenarioActor(loadNewAlbumArt);
      scenario.rendezvous();

      // step 3 (wait for steps 1 and 2) - replace album art
      TimelineRunnable replaceAlbumArt = new TimelineRunnable() {
         @Override
         public void run() {
            albumArt.setAlbumArtImage(albumArtHolder[0]);
         }
      };
      scenario.addScenarioActor(replaceAlbumArt);

      // step 4 (in parallel) - replace the track listing
      TimelineRunnable replaceTrackListing = new TimelineRunnable() {
         @Override
         public void run() {
            trackListing.setAlbumItem(albumItem);
         }
      };
      scenario.addScenarioActor(replaceTrackListing);
      scenario.rendezvous();

      // step 5 (wait for steps 3 and 4) - cross fade album art from old to
      // new
      Timeline albumArtCrossfadeTimeline = new Timeline(this.albumArt);
      albumArtCrossfadeTimeline.addPropertyToInterpolate("oldImageAlpha",
            255, 0);
      albumArtCrossfadeTimeline
            .addPropertyToInterpolate("imageAlpha", 0, 255);
      albumArtCrossfadeTimeline.addCallback(new SWTRepaintCallback(
            this.albumArt));
      albumArtCrossfadeTimeline.setDuration(400);

      scenario.addScenarioActor(albumArtCrossfadeTimeline);
      scenario.rendezvous();

      // step 6 (wait for step 5) - move new album art and track listing to
      // be side by side.
      Timeline separateArtAndTracks = new Timeline(this);
      separateArtAndTracks.addPropertyToInterpolate("overlayPosition", 0.0f,
            1.0f);
      separateArtAndTracks.addCallback(new UIThreadTimelineCallbackAdapter() {
         @Override
         public void onTimelinePulse(float durationFraction,
               float timelinePosition) {
            updateShellRegion(timelinePosition);
         }

         @Override
         public void onTimelineStateChanged(TimelineState oldState,
               TimelineState newState, float durationFraction,
               float timelinePosition) {
            updateShellRegion(timelinePosition);
         }

         private void updateShellRegion(float timelinePosition) {
            int regionWidth = (int) (BigAlbumArt.TOTAL_DIM * (1.0 + timelinePosition));
            Region newRegion = new Region();
            newRegion.add(BigAlbumArt.TOTAL_DIM - regionWidth / 2, 0,
                  regionWidth, BigAlbumArt.TOTAL_DIM);
            Shell shell = getShell();
            if (!shell.isDisposed())
               getShell().setRegion(newRegion);
         }
      });
      separateArtAndTracks.setDuration(500);
      scenario.addScenarioActor(separateArtAndTracks);

      return scenario;
   }

This scenario uses the full capabilities offered by the Trident timeline scenarios which allow combining multiple timeline scenario actors in a parallel, sequential or custom order. This code uses the following types of timeline scenario actors:

The only special case not covered by the core Trident APIs is seen in steps 1 and 6. Since SWT does not yet support per-pixel translucency on the shell level (see bug 285026), these steps use Shell.setRegion() API to emulate expanding / collapsing panels. This can also be achieved by changing the size and location of the relevant shell, but i have found changing the region to perform better on my machine. Trident core library does not provide a built-in interpolator for the Region class, and this is why steps 1 and 6 register a custom callback on the relevant timelines.

The rest of the code is pretty straightforward. It defines the components for album art and track listing, as well as the float position of the overlay between them (during the collapse / expand steps):

   /**
    * Component that shows the album art.
    */
   private BigAlbumArt albumArt;

   /**
    * Component that shows the scrollable list of album tracks.
    */
   private TrackListing trackListing;

   /**
    * 0.0f - the album art and track listing are completely overlayed, 1.0f -
    * the album art and track listing are completely separate. Is updated in
    * the {@link #currentShowAlbumDetailsScenario}.
    */
   private float overlayPosition;

When these components are added, we make sure that the album art is displayed on top of the track listing (during the collapse stage). In addition, we install a custom layout manager that respects the current value of the overlayPosition field:

      this.setLayout(new Layout() {
         @Override
         protected void layout(Composite composite, boolean flushCache) {
            int w = composite.getBounds().width;
            int h = composite.getBounds().height;

            // respect the current overlay position to implement the sliding
            // effect in steps 1 and 6 of currentShowAlbumDetailsScenario
            int dim = BigAlbumArt.TOTAL_DIM;
            int dx = (int) (overlayPosition * dim / 2);
            albumArt.setBounds((w - dim) / 2 - dx, (h - dim) / 2, dim, dim);
            trackListing.setBounds((w - dim) / 2 + dx, (h - dim) / 2, dim,
                  dim);
         }

         @Override
         protected Point computeSize(Composite composite, int wHint,
               int hHint, boolean flushCache) {
            return new Point(wHint, hHint);
         }
      });

The overlayPosition is changed in steps 1 and 6 of the main transition scenario, and the public setter requests the container to layout its contents:

   /**
    * Sets the new overlay position of the album art and track listing. This
    * method will also cause revalidation of the main window content pane.
    *
    * @param overlayPosition
    *            The new overlay position of the album art and track listing.
    */
   public void setOverlayPosition(float overlayPosition) {
      this.overlayPosition = overlayPosition;
      this.layout(new Control[] { albumArt, trackListing });
   }

Finally, the scenario itself is created and played when the mouse listener installed on the album overview component detects a mouse click and calls the setAlbumItem API:

   /**
    * Signals that details of the specified album item should be displayed in
    * this window. Note that this window can already display another album item
    * when this method is called.
    *
    * @param albumItem
    *            New album item to show in this window.
    */
   public void setAlbumItem(Item albumItem) {
      if (this.currentShowAlbumDetailsScenario != null)
         this.currentShowAlbumDetailsScenario.cancel();

      this.currentShowAlbumDetailsScenario = this
            .getShowAlbumDetailsScenario(albumItem);
      this.currentShowAlbumDetailsScenario.play();
   }

The rest of the code in this package is very similar to the code examples showed earlier, including custom painting that respects the alpha values, fading out on dispose, translucent window etc.

The final code sample shows how the album details panel is shown. Here, we use a separate Shell placed alongside the bottom edge of the main application window. This shell will have its region animated to emulate expanding / collapsing panels.

currentlyShownWindow = new Shell(mainWindow, SWT.NO_TRIM
      | SWT.DOUBLE_BUFFERED);
currentlyShownWindow.setLayout(new FillLayout());
// place the details window centered along the bottom edge of the
// main application window
Point mainWindowLoc = mainWindow.getLocation();
Point mainWindowDim = mainWindow.getSize();
int x = mainWindowLoc.x + mainWindowDim.x / 2 - BigAlbumArt.TOTAL_DIM;
int y = mainWindowLoc.y + mainWindowDim.y - BigAlbumArt.TOTAL_DIM / 2;
currentlyShownWindow.setSize(2 * BigAlbumArt.TOTAL_DIM,
      BigAlbumArt.TOTAL_DIM);
Region region = new Region();
region.add(BigAlbumArt.TOTAL_DIM / 2, 0, BigAlbumArt.TOTAL_DIM,
      BigAlbumArt.TOTAL_DIM);
currentlyShownWindow.setRegion(region);
region.dispose();
currentlyShownWindow.setLocation(x, y);

currentlyShownWindow.setAlpha(0);
currentlyShownWindow.setVisible(true);
currentlyShownContentPanel = new DetailsContentPanel(
      currentlyShownWindow);
currentlyShownContentPanel.setAlbumItem(albumItem);
currentlyShownWindow
      .layout(new Control[] { currentlyShownContentPanel });

Timeline showWindow = new Timeline(currentlyShownWindow);
showWindow.addPropertyToInterpolate("alpha", 0, 255);
showWindow.setDuration(500);
showWindow.play();

What happens here?

  • Create a new shell and position it in the required location.
  • Set the region to clip the rightmost and leftmost quarters.
  • Set its alpha to 0 (it will be gradually faded in).
  • Set the album item, initiating the transition scenario described above.
  • Create and play the timeline that fades in this window.