Sunday, April 30, 2006

Smooth JList Drop Target Animation


Web Start and source.

I ran across this nice effect for indicating a drop target in a JList. The ghosted image of an item dragged from the list appears to push aside other entries as it moves through the list of items. A very simple method results in very smooth animation that can span several cells at once. The basic idea is to maintain a list of current positions for all cells, and gradually adjust those positions to match what their final positions would be with a proposed insertion. The animation consists of moving half the distance from the current to the final position (rounding up the final pixel difference). The problem with the sample implementation is that it requires tweaks to both the list model and its LAF UI.

I thought it would be nice to be able to apply this effect to any list without having to replace the LAF UI. Since lists render themselves cell by cell anyway, it shouldn't be that hard to just trick the list into painting each cell where we want it, instead of in its default location.

I focused on just handling an internal drag, but the API on the smoother could easily be tweaked to accept native drags from external sources.

First, to separate the functionality from user triggers, I decided to put the mouse event handling into the demo code. I've found that in UI testing, it's almost always beneficial to be able to drive bits of the UI from direct programmatic methods (would that Swing followed this pattern, so it would be easier to test). The mouse listener decides when to start, update, and end the drag, and calls in to the smoother's corresponding methods.

The smoother itself is a decorator which performs the painting of individual cells by having the original list paint itself fully and then clipping out the unwanted stuff. This could be optimized by just asking for the rendering components, but that potentially omits additional decoration provided by the LAF. So I locate the cell I want within the full list as painted by the LAF, and copy that image to its "floating" location.

    // Paint the background for the insertion point
Rectangle b = getDecorationBounds();
g.setColor(list.getBackground());
g.fillRect(b.x, b.y, b.width, b.height);
for (int i=0;i < list.getModel().getSize();i++) {
if (i == draggedIndex)
continue;
Rectangle r = getCurrentCellBounds(i);
Graphics g2 = g.create(r.x, r.y, r.width, r.height);
Rectangle r2 = list.getCellBounds(i, i);
((Graphics2D)g2).translate(0, -r2.y);
list.paint(g2);
}

The Graphics object is set up to paint only in the desired area, then translated such that the JList will paint the desired cell into that area. We skip the index being dragged, since we want to handle that separately.

To draw the ghosted image, I simply used another decorator that uses the JList to render the dragged item. As the insertion location is updated by the mouse listener, the ghost image adjusts its vertical drawing position (being careful to clamp the vertical bounds to the start and end of the list).

The insertion point could use something more fancy if needed, like using the cell renderer or explicitly painting the rectangle. Just leaving the background exposed works well in this case, though.

A timer task periodically moves each cell's current location toward its final location, and triggers a repaint if any of the locations actually changes. This should be optimized to track a smaller range of cells (like only those visible).

The basic calculation of a cell's desired position simply calculates its normal position, then adjusts for any preceding dragged item (to be removed) and any preceding insertion point (to be inserted). The current locations start off with no dragged item or insertion point, and gradually float to their final positions based on a dragged item and possibly changing insertion point. Note that if you rapidly drag across several items, they all will float smoothly even as their destination changes.

Friday, April 28, 2006

Matisse First Impressions

My compliments to the Matisse crew (and a hat tip to those who have walked this path before). This is truly what a layout editor should look like. Thanks for examining prior art and learning from it. I now have a few projects that now have a netbeans project folder and extra .form files.

That said, here are a few bumps I've run into so far...

Custom Component Creation
Rather than inline code in the XML form, initComponents should either take arguments or simply rely on field contents. Putting the code in XML makes it infeasible to automatically refactor. For example, I have a filename text field that provides a browsing action synched to the editability/enabled state of the text field. So to create the button, I need

new JButton(field.getBrowseAction())

I'd rather just create the component myself prior to initComponents(), and have initComponents() know that that field has already been initialized. Perhaps split between initializing and doing the layout?

Changing Component Names
If you edit the member field name for a component, you don't get a corresponding refactor of the code.

Alternate Components in Layout
I'm trying to figure out the best way to swap out two different panels. One has just a password, one has a filename and a password. I'd like to swap between the two and have the layout still work, but the layout assumes that the component hierarchy is static. I'll probably just set up a container panel with the appropriate resizing attributes, then add/remove the alternates as needed.

Strings
I should be able to substitute a string lookup for a hard-coded string, whether in labels or text fields or whatever. Granted, I can override whatever happens in initComponents, but then I'm maintaining the same string twice - once in my string/properties tables, and again in the UI designer.

Cross-Panel alignment
Sometimes I'd like to operate on a number of components as a group (resize the right edge, for instance) and this doesn't seem possible. It'd be nice if the alignment lines could be inferred and then themselves could be draggable. Putting things into sub-panels has the unfortunate side-effect that you can only align with other things in that panel.

Grid Size/Spacing
I'm surprised this one got through. I'm stuck with 10 pixel margins that I can't change. So my choices are 10 or 0 pixels. Ugh. There's a preference for Grid X/Grid Y, but it doesn't seem to have any effect.

Monday, April 17, 2006

Decorator/Overpainter Update

I've updated the decorator to handle most LAF backgrounds. You can now modify the background on just about anything that relies on a parent to paint its background.

The selection marquee is also no longer clipped to its parent component.



Now you might say "I can just paint on my glass pane" and you'd be right except that:
* Your glass pane eats all user input events once it's visible unless you explicitly redirect them
* You glass pane doesn't know anything about what's under it
* It always paints *above* everything else

The AbstractComponentDecorator is a bit more flexible in that you can target all or part of a specific component or a specific area of your window.

Saturday, April 08, 2006

Decorating/Overpainting Swing Components


Have you ever wanted to change the way an existing component appears, just a little, without having to go and rewrite it or subclass it?

The image at right demonstrates several implementations of an abstract base class which puts an arbitrary decoration over an existing component. This technique allows you to do any of the following:

  • Put labels on a scrolled component which stay fixed relative to the scroll pane rather than the scrolled component (useful for labeling horizontal lines on a panned display).
  • Put an icon badge anywhere on a component, such as a stop sign over invalid text in a text field
  • Put tooltips on a component that you didn't write (like adding annotations to someone else's work).
  • Dim the entire painted image to gray, or fade it into the background color to make the component look disabled.
  • Change the appearance of component's background (gradients, stripes, or polka-dots).
  • Apply animation effects to the component.
  • Marching ants/Rubber band selection rectangle (xor is so Seventies).
  • Highlight all or part of an existing component to indicate the effective target of a drop operation (changing the selection isn't always the best solution).

to see a few of these decorations in action.

Note that the code for each of the following decorations consists of the painting logic and optionally positioning logic, which is all you should be concerned about.

The implementation for drawing a badge:
class Warning extends AbstractComponentDecorator {
    private final int SIZE = 16;
    public Warning(JTextField f) {
        super(f);
    }
    /** Position the badge at the right-most edge. */
    public Rectangle getDecorationBounds() {
        Rectangle r = super.getDecorationBounds();
        Insets insets = getComponent().getInsets();
        r.x += r.width - SIZE - 1;
        r.y += (r.height - SIZE) / 2;
        if (insets != null) {
            r.x -= insets.right;
        }
        return r;
    }
    public void paint(Graphics graphics) {
        Rectangle r = getDecorationBounds();
        Graphics2D g = (Graphics2D)graphics;
        GeneralPath triangle = new GeneralPath();
        triangle.moveTo(r.x + SIZE/2, r.y);
        triangle.lineTo(r.x + SIZE-1, r.y + SIZE-1);
        triangle.lineTo(r.x, r.y + SIZE-1);
        triangle.closePath();
        g.setColor(Color.yellow);
        g.fill(triangle);
        g.setColor(Color.black);
        g.draw(triangle);
        g.drawLine(r.x + SIZE/2, r.y + 3, r.x + SIZE/2, r.y + SIZE*3/4 - 2);
        g.drawLine(r.x + SIZE/2, r.y + SIZE*3/4+1, r.x + SIZE/2, r.y + SIZE - 4);
    }
}
The implementation for dimming:

class Dimmer extends AbstractComponentDecorator {
    public Dimmer(JComponent target) {
        super(target);
    }
    /** Paint using a transparent version of the bg color. */
    public void paint(Graphics g) {
        Color bg = getComponent().getBackground();
        g.setColor(new Color(bg.getRed(), bg.getGreen(), bg.getBlue(), 200));
        Rectangle r = getDecorationBounds();
        g.fillRect(r.x, r.y, r.width, r.height);
    }
}
The implementation for semi-scrolling labels:

   private static final int SIZE = 1000;
   private static final int BLOCK = 20;
   private static final int LINE = 20;

   private static class Labeler extends AbstractComponentDecorator {
       public Labeler(JComponent target) {
           super(target);
       }
       /** Ensure the label stays at the left-most visible
        * edge of the component.
        */
       public Rectangle getDecorationBounds() {
           Rectangle r = super.getDecorationBounds();
           Rectangle visible = getComponent().getVisibleRect();
           if (r.x < visible.x)
              r.x = visible.x;
           return r;
       }
       public void paint(Graphics g) {
           Rectangle r = getDecorationBounds();
           for (int i=0;i < SIZE;i+= LINE) {
               g.drawString("label " + (i/LINE + 1),
                            r.x, r.y + i + g.getFontMetrics().getAscent() + 2);
           }
       }
   } 


This class started out as a way to highlight different drop targets in a tree or table, and a method for the Costello editor to highlight areas of a component to be captured as an image (without actually setting the target component's background). I've subsequently used it for other decorations as noted above.

The whole idea would be trivial if Swing provided a hook into a component's paint method. There is no such explicit hook, and no implicit ones either without getting into a lot of complexity (perhaps by writing your own repaint manager). Since writing a repaint manager seems totally orthogonal to decorating a single component, we take a different tack.

My first implementation was to simply add a sibling or child component into the hierarchy on top of the first (and indeed, that implementation is still in use by the Costello editor). Unfortunately, the normal component hierarchy doesn't really consider z ordering, so the results are not always consistent. You might get things painted properly 90% of the time, but occasionally have your decoration occluded by a scroll pane, or the fact that there's an extra component in the hierarchy causes the layout manager to occasionally freak out. So it's okay for a proof of concept, but kinda lame in practice.

There is, however, a Swing component that knows about z ordering. Most Swing components will reside somewhere below a JLayeredPane (see the javadoc for javax.swing.RootPaneContainer for details). This little component has the capability of painting things in layers, which is just the capability we're looking for. The JLayeredPane even has predefined layers and was explicitly designed for stacking components in a known Z order (it also has a sub-layer ordering referred to as "position", but this doesn't seem to work).

So the basic idea is that for any decoration, we establish a painting component as a child of the JLayeredPane which is positioned in a layer above the decorated component. The Swing painting mechanism will automatically ensure that our decoration gets painted whenever the decorated component is painted, and that the decoration happens after the target component is painted.

A few details to note:

  • The decorator must maintain proper position and size with respect to the target component. If the decoration is in the lower right corner of the component, it should stay there if the component grows.
  • The decorator must clip its painting according to how much of the target component is visible. If the component is partially obscured within a scroll pane, the decorator should be clipped in a similar fashion.
  • The decorator needs to track the target component's visibility. If the component is on a tabbed pane and a different tab is displayed, the decoration needs to be hidden.
  • Painting under the decorated component is a bit more tricky. The current implementation for decorating component backgrounds assumes the current LAF doesn't change how ComponentUI paints the default background (gtk/synth probably won't work well). I haven't really looked into this much.
  • It's possible to composite with the existing graphics (so you could, for instance, paint only where there's already a blue pixel), but this capability varies by platform and VM version.


Full source for the demo and AbstractComponentDecorator class.