Over the course of this week i’m talking about different concepts in the Trident animation library for Java applications. Part seven shows a more complex Swing / SWT example that illustrates usage of multiple timelines running in parallel and affecting different visual areas of the same application window.

Multiple timelines in Swing applications

Trident supports running multiple independent timelines at the same time. This page shows the Swing application behind this video, where every cell rollover is implemented as a separate timeline.

We start with a class that implements a specific grid cell:

   public static class SnakePanelRectangle {
      private Color backgroundColor;

      private boolean isRollover;

      private Timeline rolloverTimeline;

      public SnakePanelRectangle() {
         this.backgroundColor = Color.black;
         this.isRollover = false;

         this.rolloverTimeline = new Timeline(this);
         this.rolloverTimeline.addPropertyToInterpolate("backgroundColor",
               Color.yellow, Color.black);
         this.rolloverTimeline.setDuration(2500);
      }

      public void setRollover(boolean isRollover) {
         if (this.isRollover == isRollover)
            return;
         this.isRollover = isRollover;
         if (this.isRollover) {
            this.rolloverTimeline.replay();
         }
      }

      public void setBackgroundColor(Color backgroundColor) {
         this.backgroundColor = backgroundColor;
      }

      public Color getBackgroundColor() {
         return backgroundColor;
      }
   }

A few major points in this class:

  • The default background color of a cell is black.
  • The rollover timeline interpolates the background color from yellow to black over a period of 2.5 seconds.
  • The rollover timeline is replayed when setRollover is called with true. This restarts the timeline to interpolate the foreground color from yellow.

The next class implements a cell grid, tracing the mouse events and dispatching the rollover events to the relevant cells:

   private static class SnakePanel extends JPanel {

      private SnakePanelRectangle[][] grid;

      private int ROWS = 10;

      private int COLUMNS = 20;

      private int DIM = 20;

      public SnakePanel() {
         this.grid = new SnakePanelRectangle[COLUMNS][ROWS];
         for (int i = 0; i < COLUMNS; i++) {
            for (int j = 0; j < ROWS; j++) {
               this.grid[i][j] = new SnakePanelRectangle();
            }
         }
         this.setPreferredSize(new Dimension(COLUMNS * (DIM + 1), ROWS
               * (DIM + 1)));

         Timeline repaint = new SwingRepaintTimeline(this);
         repaint.playLoop(RepeatBehavior.LOOP);

         this.addMouseMotionListener(new MouseMotionAdapter() {
            int rowOld = -1;
            int colOld = -1;

            @Override
            public void mouseMoved(MouseEvent e) {
               int x = e.getX();
               int y = e.getY();

               int column = x / (DIM + 1);
               int row = y / (DIM + 1);

               if ((column != colOld) || (row != rowOld)) {
                  if ((colOld >= 0) && (rowOld >= 0))
                     grid[colOld][rowOld].setRollover(false);
                  grid[column][row].setRollover(true);
               }
               colOld = column;
               rowOld = row;
            }
         });
      }

      @Override
      protected void paintComponent(Graphics g) {
         Graphics2D g2d = (Graphics2D) g.create();

         g2d.setColor(Color.black);
         g2d.fillRect(0, 0, getWidth(), getHeight());

         for (int i = 0; i < COLUMNS; i++) {
            for (int j = 0; j < ROWS; j++) {
               SnakePanelRectangle rect = this.grid[i][j];
               Color backgr = rect.getBackgroundColor();

               if (!Color.black.equals(backgr)) {
                  g2d.setColor(backgr);
                  g2d.fillRect(i * (DIM + 1), j * (DIM + 1), DIM, DIM);
               }
            }
         }

         g2d.dispose();
      }
   }

A few major points in this class:

  • A special type of timeline is created and played in a loop. In this example, each cell rollover timeline changes the background color of that cell, but does not cause the repaint. Instead, we have a "master" repaint timeline that runs in a loop and causes the repaint of the entire grid panel.
  • The mouse motion listener tracks the mouse location, calling the setRollover method on relevant cells. Since each cell rollover timeline runs for 2.5 seconds, quick mouse moves will result in multiple timelines running in parallel.
  • The painting of each cell respects the current background color of that cell.

Finally, the main method that creates a host frame and adds the cell grid panel to it:

   public static void main(String[] args) {
      SwingUtilities.invokeLater(new Runnable() {
         @Override
         public void run() {
            JFrame frame = new JFrame("Snake");
            frame.add(new SnakePanel());
            frame.pack();
            frame.setLocationRelativeTo(null);
            frame.setDefaultCloseOperation(JFrame.DISPOSE_ON_CLOSE);
            frame.setVisible(true);
         }
      });
   }

Multiple timelines in SWT applications

The matching SWT code is quite similar. The single grid cell:

   public static class SnakePanelRectangle {
      private Color backgroundColor;

      private boolean isRollover;

      private Timeline rolloverTimeline;

      public SnakePanelRectangle() {
         this.backgroundColor = Display.getDefault().getSystemColor(
               SWT.COLOR_BLACK);
         this.isRollover = false;

         this.rolloverTimeline = new Timeline(this);
         this.rolloverTimeline.addPropertyToInterpolate("backgroundColor",
               Display.getDefault().getSystemColor(SWT.COLOR_YELLOW),
               Display.getDefault().getSystemColor(SWT.COLOR_BLACK));
         this.rolloverTimeline.setDuration(2500);
      }

      public void setRollover(boolean isRollover) {
         if (this.isRollover == isRollover)
            return;
         this.isRollover = isRollover;
         if (this.isRollover) {
            this.rolloverTimeline.replay();
         }
      }

      public void setBackgroundColor(Color backgroundColor) {
         this.backgroundColor = backgroundColor;
      }

      public Color getBackgroundColor() {
         return backgroundColor;
      }
   }

The cell grid:

   private static class SnakePanel extends Canvas {

      private SnakePanelRectangle[][] grid;

      private int ROWS = 10;

      private int COLUMNS = 20;

      private int DIM = 20;

      public SnakePanel(Composite parent) {
         super(parent, SWT.DOUBLE_BUFFERED);

         this.grid = new SnakePanelRectangle[COLUMNS][ROWS];
         for (int i = 0; i < COLUMNS; i++) {
            for (int j = 0; j < ROWS; j++) {
               this.grid[i][j] = new SnakePanelRectangle();
            }
         }

         Timeline repaint = new SWTRepaintTimeline(this);
         repaint.playLoop(RepeatBehavior.LOOP);

         this.addMouseMoveListener(new MouseMoveListener() {
            int rowOld = -1;
            int colOld = -1;

            @Override
            public void mouseMove(MouseEvent e) {
               int x = e.x;
               int y = e.y;

               int column = x / (DIM + 1);
               int row = y / (DIM + 1);

               if ((column >= COLUMNS) || (row >= ROWS))
                  return;

               if ((column != colOld) || (row != rowOld)) {
                  if ((colOld >= 0) && (rowOld >= 0))
                     grid[colOld][rowOld].setRollover(false);
                  grid[column][row].setRollover(true);
               }
               colOld = column;
               rowOld = row;
            }
         });

         this.addPaintListener(new PaintListener() {
            @Override
            public void paintControl(PaintEvent e) {
               GC gc = e.gc;
               gc.setBackground(e.display.getSystemColor(SWT.COLOR_BLACK));
               gc.fillRectangle(e.x, e.y, e.width, e.height);

               for (int i = 0; i < COLUMNS; i++) {
                  for (int j = 0; j < ROWS; j++) {
                     SnakePanelRectangle rect = grid[i][j];
                     Color backgr = rect.getBackgroundColor();
                     gc.setBackground(backgr);
                     gc.fillRectangle(i * (DIM + 1), j * (DIM + 1), DIM,
                           DIM);
                  }
               }
            }
         });
      }
   }

and the main method:

   public static void main(String[] args) {
      Display display = new Display();
      Shell shell = new Shell(display);
      shell.setSize(430, 240);
      shell.setText("SWT Snake");
      FillLayout layout = new FillLayout();
      shell.setLayout(layout);

      SnakePanel snake = new SnakePanel(shell);

      shell.open();
      while (!shell.isDisposed()) {
         if (!display.readAndDispatch())
            display.sleep();
      }
      display.dispose();
   }

In this two examples we have multiple timeline running in parallel. The main repaint timeline continuously repaints the grid, and each cell has its own rollover timeline. If you move the mouse quickly over the grid, you can end up with dozens of timelines, each updating its own cell - with the "master" repaint timeline looking at the current cell color during the painting.

Click below for the WebStart demo of the Swing version

Over the course of the next few days i’m going to talk about different concepts in the Trident animation library for Java applications. Part six shows simple Swing / SWT examples that highlight Trident support for Java-based UI toolkits.

Simple Swing example

The following example shows how to smoothly change the foreground color of a Swing button on mouse rollover.

import java.awt.Color;
import java.awt.FlowLayout;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;

import javax.swing.*;

import org.pushingpixels.trident.Timeline;

public class ButtonFg extends JFrame {
   public ButtonFg() {
      JButton button = new JButton("sample");
      button.setForeground(Color.blue);

      this.setLayout(new FlowLayout());
      this.add(button);

      final Timeline rolloverTimeline = new Timeline(button);
      rolloverTimeline.addPropertyToInterpolate("foreground", Color.blue,
            Color.red);
      rolloverTimeline.setDuration(2500);
      button.addMouseListener(new MouseAdapter() {
         @Override
         public void mouseEntered(MouseEvent e) {
            rolloverTimeline.play();
         }

         @Override
         public void mouseExited(MouseEvent e) {
            rolloverTimeline.playReverse();
         }
      });

      this.setSize(400, 200);
      this.setLocationRelativeTo(null);
      this.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
   }

   public static void main(String[] args) {
      SwingUtilities.invokeLater(new Runnable() {
         @Override
         public void run() {
            new ButtonFg().setVisible(true);
         }
      });
   }
}

Here, we have a timeline that interpolates the foreground color between blue and red. The mouse listener registered on the button plays this timeline on mouse enter, and plays this timeline in reverse on mouse exit.

This example shows how the JComponent.setForeground(Color) method is used together with the built in property interpolator for the java.awt.Color class to run the timeline that interpolates the foreground color of a Swing button. Note that since the JComponent.setForeground(Color) also repaints the button, there is no need to explicitly repaint it on every timeline pulse.

If you debug this application and put a breakpoint in the JComponent.setForeground(Color) method, you will see that it is called on the Event Dispatch Thread. This is a built-in capability of the Trident core. It recognizes that the timeline is associated with a Swing component, and calls the setter method (during the timeline pulses) on the EDT.

Simple SWT example

The following example is the SWT version of changing the control foreground color on mouse rollover:

import org.eclipse.swt.SWT;
import org.eclipse.swt.events.MouseEvent;
import org.eclipse.swt.events.MouseTrackAdapter;
import org.eclipse.swt.graphics.Color;
import org.eclipse.swt.layout.GridData;
import org.eclipse.swt.layout.GridLayout;
import org.eclipse.swt.widgets.*;
import org.pushingpixels.trident.Timeline;

public class ButtonFg {
   public static void main(String[] args) {
      Display display = new Display();
      Shell shell = new Shell(display);
      shell.setSize(300, 200);
      GridLayout layout = new GridLayout();
      shell.setLayout(layout);

      Button button = new Button(shell, SWT.RADIO);
      GridData gridData = new GridData(GridData.CENTER, GridData.CENTER,
            true, false);
      button.setLayoutData(gridData);

      button.setText("sample");

      Color blue = display.getSystemColor(SWT.COLOR_BLUE);
      Color red = display.getSystemColor(SWT.COLOR_RED);
      button.setForeground(blue);

      final Timeline rolloverTimeline = new Timeline(button);
      rolloverTimeline.addPropertyToInterpolate("foreground", blue, red);
      rolloverTimeline.setDuration(2500);
      button.addMouseTrackListener(new MouseTrackAdapter() {
         @Override
         public void mouseEnter(MouseEvent e) {
            rolloverTimeline.play();
         }

         @Override
         public void mouseExit(MouseEvent e) {
            rolloverTimeline.playReverse();
         }
      });

      shell.open();
      while (!shell.isDisposed()) {
         if (!display.readAndDispatch())
            display.sleep();
      }
      display.dispose();
   }
}

As with Swing, the Control.setForeground(Color) method is used together with the built in property interpolator for the org.eclipse.swt.graphics.Color class to run the timeline that interpolates the foreground color of an SWT radio button. Note that since the Control.setForeground(Color) also repaints the button, there is no need to explicitly repaint it on every timeline pulse.

If you debug this application and put a breakpoint in the Control.setForeground(Color) method, you will see that it is called on the SWT Thread. This is a built-in capability of the Trident core. It recognizes that the timeline is associated with a SWT component, and calls the setter method (during the timeline pulses) on the SWT thread.

Finally, since both examples are using the Timeline.play() and Timeline.playReverse() methods, the interpolation can be reversed in the middle if the user moves the mouse quickly. The rollover timeline in our example takes 2.5 seconds to complete. Suppose the user moves the mouse over the button, and then after one second moves the mouse away. The call to playReverse detects that this very timeline is already playing, and starts playing it in reverse from its current position.

I’ve just finished presenting my EclipseCon session. Thanks to everybody for coming, even at the expense of not attending the main-stage e4 talk. To all of you who attended but want to see the slides once again, and to those of you who couldn’t attend, here are the slides:

The Problems view in Eclipse provides filtering and grouping support for quickly navigating large sets of problem markers. If you’re creating custom problem markers, you can register a custom marker group and associate priorities to the different group entries. This will make navigating your custom markers much faster.

There are three extension points that you need to implement:

The first extension point defines your custom marker type, associating a custom ID with it. The persistent attribute should be set to true if you want your markers to be persisted across the workbench invocations. The list of super types would usually include org.eclipse.core.resources.problemmarker and org.eclipse.core.resources.textmarker. If your custom markers are associated with Java classes, use org.eclipse.jdt.core.problem as well.

The second extension point is used to associate the markers with the text documents. Whenever the document is displayed in the associated editor, it will show the marker annotations in the relevant areas (usually the gutters). The markerType attribute should be the ID of the marker type specified above, and the super attribute would usually be org.eclipse.ui.workbench.texteditor.*, where the last segment is info, warning or error.

The third extension point is used to create a custom marker group, and further categorize the custom markers.

The markerTypeCategory sub-element declares the custom category shown in the Types part of the Configure Contents dialog. The name attribute will be used in the relevant tree node. The markerTypeReference should point to the ID of the marker type specified in the first extension point.

The markerGrouping sub-element declares the custom grouping shown in the Group By popup menu. The name attribute will be used for the popup menu entry, and the id attribute will be used to associate the group entries (see below).

The markerGroupingEntry sub-element is used to group the different custom markers when the parent group is selected in the matching Group By popup menu. If you have different validation / analysis rules, you can group the resulting markers to present a clean visual separation. You can have multiple grouping entries. Each entry has the associated priority – groups with higher priority (use values in 0..100 range) are shown first in the Problems view.

The markerGrouping attribute of the markerGroupingEntry sub-element should point to the ID of the markerGrouping sub-element declared above. The label attribute is used in the Problems view parent node that groups all markers belonging to this grouping entry.

The last piece of configuration is the markerAttributeGrouping sub-element. Its markerType points to the ID of the custom marker from the first extension point, and its attribute is used as a custom attribute on the IMarker (see below) to associate the specific marker with its grouping entry. The markerAttributeMapping sub-elements specify the mapping itself.

Suppose the markerAttributeGrouping.attribute is categoryId. Then, the IMarker will have the following:

marker.setAttribute(“categoryId”, 50)

where 50 is a sample value. Then matching markerAttributeMapping will have value attribute equal to 50, and markerGroupingEntry attribute equal to ID of one of the markerGroupingEntry elements above.

How does it work at runtime? Suppose your org.eclipse.core.resources.marker extension point has the id attribute equal to com.my.company.custom.problem. Then, you create the marker with

IMarker marker = resource.createMarker(“com.my.company.custom.problem”)

set all the usual attributes (line number, priority, severity, message), and then set the categoryId attribute (the name must correspond to the matching markerAttributeGrouping.attribute entry) to one of the values of markerAttributeMapping. This marker will:

  • Be associated with the specific resource.
  • Be shown in the problems view with the set message, priority and severity.
  • If the Problems view is grouped with the custom grouping, the marker’s categoryId value will be used to map it to its grouping entry, and the marker will be placed under the relevant parent node in the tree.