Implementing a Color Picker Dialog With Canvas and GWT

Google Web Toolkit is a powerful framework that can be used to do a variety of things. In this article I'll be showing what GWT's JavaScript overlay types are, what they can be used for, and I'll be showing a concrete example that uses them.

To follow the code examples it is best if you have the GWT plugin installed in Eclipse, which simplifies programming and makes it easy to do debugging if necessary.

I'll be showing how to use the <canvas> element from within GWT. Then I'll be using that to construct a color picker complete with mouse handling. And finally, the color picker is put into a dialog that can be reused

Update: canvas Element Now Directly Supported By GWT
The GWT team has integrated <canvas> support into the recently-released version 2.2.0. Converting your code should be pretty straightforward since they're using the same approaches I did. See the API documentation for futher details.

What are JavaScript Overlay Types?

GWT allows you to embed native hand-written JavaScript code in your Java code. For example, the following would pop up an alert box:

public native void showAlert() /*-{
    alert("hi there");
}-*/;

While this is pretty nifty and can be used to do powerful stuff, it doesn't mean you should. The main disadvantage is that it complicates debugging. While for the rest of your code you can use your IDE's debugger, this doesn't apply to JavaScript code. In general, you should try to avoid using JavaScript code as much as you can. Additionally, when using the GWT compiler to generate the actual JavaScript code running on the browser, it tries really really hard to optimize the code to make it run as fast as possible. Hand-written code does interfere badly with that goal.

Luckily, there is a workaround for all of that. And you even get to use pure Java code along with code-completion in your IDE, Javadocs and all that stuff. Enter GWT JavaScript overlay types.

GWT's overlay types are just that - they are overlays for native JavaScript objects. For example, JavaScript arrays or DOM elements are native JavaScript objects, and you are able to overlay a native Java type on top of that.

You may wonder what overhead this feature brings with it. There is none! In fact, overlay types do not generate any JavaScript code when using the GWT compiler. You get all the neat stuff with only a few minor downsides.

Using the <canvas> Element

Let's work on a real life implementation of an overlay type. We want to use the <canvas> element supported by modern browsers (*cough* no MSIE *cough*) so that we're able to draw stuff on the page from within Java code. First we start with a regular custom widget that wraps the <canvas> element:

package com.subshell.gwt.canvas.client.canvas;
public Canvas extends Composite {
    private Element canvas;
    public Canvas() {
        HTML html = new HTML("<canvas></canvas>");
        initWidget(html);
        canvas = html.getElement().getFirstChildElement();
    }
}

Nothing spectacular so far. Now let's add support for the getContext() method of <canvas>:

public Canvas extends Composite {
    public native RenderingContext getContext() /*-{
        return this.@com.subshell.gwt.canvas.client.canvas.Canvas::canvas.getContext("2d");
    }-*/;
}

This is a native JavaScript method. As you can see, it is easy to reference members of the enclosing Java type from inside the JavaScript code. In this case, I've referenced the <canvas> element defined in the constructor of the class to invoke the getContext() method on it.

The return type of the method is RenderingContext, which is where we'll be using an overlay type so that it is possible to use the actual native JavaScript rendering context object inside our Java code:

import com.google.gwt.core.client.JavaScriptObject;
public RenderingContext extends JavaScriptObject {
    // must be protected
    protected RenderingContext() {}
}

That's basically it. To use an overlay type for a native JavaScript object you need to extend JavaScriptObject and provide a protected no-args constructor.

Now we're able to add support for more functionality of the <canvas> element. (If you're familiar with using <canvas> you should recognize the following.) Note that all methods must be final.

public RenderingContext extends JavaScriptObject {
    public final native void setFillStyle(String style) /*-{
        this.fillStyle = fillStyle;
    }-*/;
    public final native void fillRect(int x, int y, int width, int height) /*-{
        this.fillRect(x, y, width, height);
    }-*/;
}

Note that this in the native JavaScript code refers to the actual native JavaScript object, in this case the rendering context returned by <canvas>.getContext().

Putting It Together

The code above enables us to set a fill color and to fill a rectangle. Let's see it!

public class CanvasEntryPoint implements EntryPoint {
    public void onModuleLoad() {
        Canvas canvas = new Canvas();
        RootPanel.get("main").add(canvas);
        RenderingContext ctx = canvas.getContext();
        ctx.setFillStyle("#ff0000");
        ctx.fillRect(10, 10, 20, 20);
    }
}

This should draw a red rectangle onto your screen:

Canvas Widget

Adding support for more <canvas> functionality is pretty straightforward and expands on using overlay types. Please see the complete code example that allows you to use paths and linear gradients. In addition, I've added code for drawing hue and saturation/lightness gradients to help complete the promised color picker:

 

Canvas Widget

Mouse Handling and Custom Events

In this second part I'll expand on the code previously provided to add mouse handling, which will add functionality to our color picker to actually become useful. We'll also have a look at implementing custom events.

GWT Event Handling

In GWT there are your regular event handlers for each type of event produced by the browser: Mouse down, mouse up, mouse move, key down, etc. In addition, you can always create your own higher-level events and associated listeners.

If you have been doing any user interface coding recently, you may be familiar with the following technique:

widget.addListener(new MouseDownListener() {
    public void mouseDown(MouseDownEvent event) {
        // do stuff here
    }
});

In GWT, it simply works the same, except that there is nothing similar to a removeListener() method. Instead, GWT returns a HandlerRegistration object that you can use to remove handlers. (Note that in GWT event handlers are called "handlers", not "listeners".)

Adding Mouse Handling

Expanding on the code I've provided in the previous part of this series, let's add some mouse handling so that HuePicker and SaturationLightnessPicker become actually useful:

public class HuePicker extends Composite {
    public HuePicker() {
        // ...
        canvas.addMouseDownHandler(new MouseDownHandler() {
            public void onMouseDown(MouseDownEvent event) {
                handleY = event.getRelativeY(canvas.getElement());
                drawGradient();
                mouseDown = true;
            }
        });
        canvas.addMouseMoveHandler(new MouseMoveHandler() {
            public void onMouseMove(MouseMoveEvent event) {
                if (mouseDown) {
                    handleY = event.getRelativeY(canvas.getElement());
                    drawGradient();
                }
            }
        });
        canvas.addMouseUpHandler(new MouseUpHandler() {
            public void onMouseUp(MouseUpEvent event) {
                mouseDown = false;
            }
        });
        canvas.addMouseOutHandler(new MouseOutHandler() {
            public void onMouseOut(MouseOutEvent event) {
                mouseDown = false;
            }
        });
    }
}

As you can see we respond to mouse clicking and dragging by remembering the y coordinate (relative to the <canvas> element.) All of this is rather straightforward.

Custom Events

While handling regular mouse events is pretty useful, our HuePicker can do more. From the outside, we're not really interested in whether the hue changing is a result of mouse clicks or drags. The only thing we're interested in is the hue change itself.

That's where custom events come into play. Whenever the hue changes its value, we want HuePicker to fire a new HueChangedEvent that other components may choose to handle. Let's start with the handler interface which should look quite familiar:

public interface IHueChangedHandler {
    void hueChanged(HueChangedEvent event);
}

Next, we need the HueChangedEvent. It looks a bit complicated, but in the end, it will integrate nicely with GWT's own event handling mechanisms, freeing us from implementing our own. The interesting part is the dispatch() method that GWT will invoke to dispatch the event to a particular handler.

public class HueChangedEvent extends GwtEvent<IHueChangedHandler> {
    private static GwtEvent.Type<IHueChangedHandler> TYPE;
    private int hue;
    
    HueChangedEvent(int hue) {
        this.hue = hue;
    }
    
    public static GwtEvent.Type<IHueChangedHandler> getType() {
        if (TYPE == null) {
            TYPE = new Type<IHueChangedHandler>();
        }
        return TYPE;
    }
    
    public GwtEvent.Type<IHueChangedHandler> getAssociatedType() {
        return TYPE;
    }
    protected void dispatch(IHueChangedHandler handler) {
        handler.hueChanged(this);
    }
    
    public int getHue() {
        return hue;
    }
}

Now we have everything in place to start using the new event:

public class HuePicker extends Composite {
    public HandlerRegistration addHueChangedHandler(IHueChangedHandler handler) {
        return addHandler(handler, HueChangedEvent.getType());
    }
    
    private void fireHueChanged(int hue) {
        fireEvent(new HueChangedEvent(hue));
    }
}

Whenever the hue changes, we can now invoke fireHueChanged() to notify each handler of the change. Note that addHueChangedHandler() and fireHueChanged() invoke GWT's event handling methods. We don't need to do everything ourselves.

In the code attached to this article I've done something similar to SaturationLightnessPicker - it fires a ColorChangedEvent instead. With this in place, it is now possible to have both working together with just a few lines of code:

huePicker.addHueChangedHandler(new IHueChangedHandler() {
    public void hueChanged(HueChangedEvent event) {
        slPicker.setHue(event.getHue());
    }
}); 

This is what it may look like in the end:

Canvas Widget

Also, please note that updating the gradients in the color pickers may be quite slow. This can be sped up considerably by using image caching which can be done with regular <canvas> features. I will not be presenting how to do that in this series, though.

GWT DialogBox

In this last part our color picker will be put into a dialog box so that it can be reused.

Creating a dialog box in GWT is pretty straightforward. In its most simplest form it can be used like this:

DialogBox dlg = new DialogBox();
dlg.add(new Label("Hello World!"));
dlg.center();

This method is not pretty useful, though, because there aren't any buttons to click which means the dialog box will never go away. You also can't simply add a button because that would throw an exception:

DialogBox dlg = new DialogBox();
dlg.add(new Label("Hello World!"));
dlg.add(new Button("OK")); // throws exception!
dlg.center();

The exception is thrown because DialogBox may only have exactly one child widget ever. So to add more children, you need panels.

This is the dialog code in a working form:

final DialogBox dlg = new DialogBox();
VerticalPanel panel = new VerticalPanel();
panel.add(new Label("Hello World!"));
panel.add(new Button("OK", new ClickHandler() {
    public void onClick(ClickEvent event) {
        dlg.hide();
    }
}));
dlg.add(panel);
dlg.center();

As you can imagine, this code will get long and unwieldy rather quickly as you add more widgets such as labels, buttons, panels, etc. Sooner or later you will want to use subclasses of DialogBox here.

Another thing to note about DialogBox is that it has a method to add a CloseHandler. Obviously, this handler is notified when the dialog is closed. In that case, a CloseEvent is sent to the handler. However, there is no way to know exactly why the dialog was closed: Has the "OK" button been clicked? Has it been canceled? This is because GWT's DialogBox doesn't add any buttons by itself.

Custom Dialogs

As a workaround for these problems I've written an abstract class that inherits from DialogBox. It adds a few features that incorporate ideas from JFace's Dialog class:

  •  There are dedicated areas for dialog contents and dialog buttons. No need to fiddle with panels to separate the two.
  • When subclassing it you only need to override createDialogArea() and buttonClicked() to make it work. The "OK" and "Cancel" buttons are created by default. (You may alter the default behaviour by overriding createButtonsForButtonBar() or createButtonBar().)
  • I've added a custom close event that knows if the dialog has been canceled or not.


Please check the full source code to see how to use it.

Putting It All Together

Going back to our color picker, it is now rather easy to put it into a dialog box. Most of the code that has been in CanvasEntryPoint is moved into a new ColorPickerDialog class. The following method then opens the new dialog, and when "OK" is clicked, it will put the new color into a text box:

private void pickColor() {
    final ColorPickerDialog dlg = new ColorPickerDialog();
    dlg.setColor(colorText.getText());
    dlg.addDialogClosedHandler(new IDialogClosedHandler() {
        public void dialogClosed(DialogClosedEvent event) {
            if (!event.isCanceled()) {
                setColor(dlg.getColor());
            }
        }
    });
    dlg.center();
}

This is a screenshot showing what the result might look like:

Canvas Widget

Download the source if you'd like to experiment with it. Also please feel free to ask questions in the comments section.

Image Manipulation with HTML5 Canvas - Part I

For our upcoming website relaunch, I did some interesting experiments with the HTML5 canvas element. more

Image Manipulation with HTML5 Canvas - Part II

more

Create Info Graphics with Sophora

more

Sophora CMS: A New Take on Content Management

Fast. Powerful. Flexible. Easy to use. Comfortable. We built our CMS Sophora with these key challenges in mind. more

Maik Schreiber

Maik Schreiber

2010-10-05 • 03:49 上午

Java