Category Archives: development

Thumbnail Chooser

Whenever users can upload their own profile picture they should be able to choose a fixed sized thumbnail of it. Thinking of a news feed or protocol like list with user images in it, the images should all have the same size, they should be proportionally scaled and the user should select the section of the image to show.

Therefore, we implemented a Facebook like thumbnail chooser where the user can drag her image in a frame to adjust the visible section. We then create the thumbnail on the server (i.e., we first proportionally scale the original image and extract the selected aperture).

The chooser is initialized with the url of the previously selected thumbnail (if none exists we display a placeholder). After the user clicks on it we show a proportionally scaled version of the original image. The smaller dimension of the image (height or width) is scaled to fit the size of the frame (called thumbnailBox in the code) plus a given overlap. This is done in scaleToFit(Image, int):

private void scaleToFit(Image imageToScale, int overlap) {
    if (imageToScale.getWidth() < imageToScale.getHeight()) {
        int height = imageToScale.getHeight();
        double percent = (double) (fBoxWidth + overlap) / imageToScale.getWidth();
        imageToScale.setWidth(fBoxWidth + overlap + Unit.PX.getType());
        imageToScale.setHeight((height * percent) + Unit.PX.getType());
    } else {
        int width = imageToScale.getWidth();
        double percent = (double) (fBoxHeight + overlap) / imageToScale.getHeight();
        imageToScale.setHeight(fBoxHeight + overlap + Unit.PX.getType());
        imageToScale.setWidth((width * percent) + Unit.PX.getType());
    }
}

This method is called in a LoadHandler attached to the thumbnail

@UiHandler("thumbnail")
void onImageLoad(LoadEvent event) {
    // scheduler needed for Chrome, otherwise width/height of image is always 0 (see
    // http://code.google.com/p/google-web-toolkit/issues/detail?id=6848)
    Scheduler.get().scheduleDeferred(new ScheduledCommand() {
 
        @Override
        public void execute() {
            scaleToFit(thumbnail, fSourceLoaded ? fOverlap : 0);
        }
    });
}

that is invoked after changing the source of thumbnail:

@UiHandler("thumbnailBox")
void onMouseDownBox(MouseDownEvent event) {
    if (!fSourceLoaded) {
        fMover = new ImageMover();
        fSourceLoaded = true;
        thumbnail.setUrl(fSourceUrl);
    }
    fMover.setStartPoints(event.getClientX(), event.getClientY());
    fMouseUpRegistration = Event.addNativePreviewHandler(fMouseUpHandler);
    fPressed = true;
}

Here we first check whether the original image is already loaded or not. If not we set the new url (which causes a LoadEvent to be fired). When the user drags the image fMover adjusts the top and left CSS properties of the image. The movement is triggered upon receiving MouseMoveEvents on the thumbnailBox. As soon as the user releases the mouse button the movement should stop. This one was a bit trickier since listening to MouseUpEvent on the thumbnailBox is not sufficient. What if the mouse pointer is outside the box? We therefore add a NativePreviewHandler (fMouseUpHandler) and listen for all native Event.ONMOUSEUP events:

private Event.NativePreviewHandler fMouseUpHandler = new NativePreviewHandler() {
 
    @Override
    public void onPreviewNativeEvent(NativePreviewEvent event) {
        if (event.getTypeInt() == Event.ONMOUSEUP) {
            fPressed = false;
            fMouseUpRegistration.removeHandler();
        }
    }
};

Here’s the complete code:
ThumbnailChooser.ui.xml

<!DOCTYPE ui:UiBinder SYSTEM "http://dl.google.com/gwt/DTD/xhtml.ent">
<ui:UiBinder xmlns:ui="urn:ui:com.google.gwt.uibinder" xmlns:g="urn:import:com.google.gwt.user.client.ui">
    <ui:style>
        .box {
            border: solid 1px orange;
            cursor: move;
       	    float: left;
            overflow: hidden;
        }
 
        .box img {
            position: relative;
        }
    </ui:style>
 
    <g:FocusPanel ui:field="thumbnailBox" styleName="{style.box}">
        <g:Image ui:field="thumbnail" />
    </g:FocusPanel>
</ui:UiBinder>

ThumbnailChooser.java

import com.google.gwt.core.client.GWT;
import com.google.gwt.core.client.Scheduler;
import com.google.gwt.core.client.Scheduler.ScheduledCommand;
import com.google.gwt.dom.client.Style.Unit;
import com.google.gwt.event.dom.client.LoadEvent;
import com.google.gwt.event.dom.client.MouseDownEvent;
import com.google.gwt.event.dom.client.MouseMoveEvent;
import com.google.gwt.event.dom.client.MouseUpEvent;
import com.google.gwt.event.shared.HandlerRegistration;
import com.google.gwt.uibinder.client.UiBinder;
import com.google.gwt.uibinder.client.UiField;
import com.google.gwt.uibinder.client.UiHandler;
import com.google.gwt.user.client.Event;
import com.google.gwt.user.client.Event.NativePreviewEvent;
import com.google.gwt.user.client.Event.NativePreviewHandler;
import com.google.gwt.user.client.ui.Composite;
import com.google.gwt.user.client.ui.FocusPanel;
import com.google.gwt.user.client.ui.Image;
import com.google.gwt.user.client.ui.Widget;
 
/**
 * This widget allows for selecting a sized area (i.e., a thumbnail) from a given picture.
 * <p>
 * It draws a box with the size of the thumbnail and displays the image of which the thumbnail is to be made in it. The
 * user can now drag the picture with the mouse and place it in the box as she likes.
 * 
 * @author zubi
 * 
 */
public class ThumbnailChooser extends Composite {
 
    private static ThumbnailChooserUiBinder sUiBinder = GWT.create(ThumbnailChooserUiBinder.class);
 
    interface ThumbnailChooserUiBinder extends UiBinder<Widget, ThumbnailChooser> { }
 
    @UiField
    FocusPanel thumbnailBox;
    @UiField
    Image thumbnail;
    private final int fBoxHeight;
    private final int fBoxWidth;
    private final int fOverlap;
    private int fLeft;
    private int fTop;
    private String fThumbnailUrl;
    private String fSourceUrl;
    private boolean fSourceLoaded;
    private ImageMover fMover;
    private boolean fPressed;
    private HandlerRegistration fMouseUpRegistration;
    private Event.NativePreviewHandler fMouseUpHandler = new NativePreviewHandler() {
 
        @Override
        public void onPreviewNativeEvent(NativePreviewEvent event) {
            if (event.getTypeInt() == Event.ONMOUSEUP) {
                fPressed = false;
                fMouseUpRegistration.removeHandler();
            }
        }
    };
 
    /**
     * Creates new thumbnail chooser.
     * 
     * @param thumbnailHeight
     *            height of thumbnail
     * @param thumbnailWidth
     *            width of thumbnail
     * @param thumbnailUrl
     *            url of existing thumbnail
     * @param sourceUrl
     *            url of the image of which a thumbnail is to be selected
     * @param overlap
     *            the amount in pixels the source image overlaps the thumbnail box in its shorter dimension (the image
     *            gets scaled to just fit the thumbnail box plus the given overlap)
     */
    public ThumbnailChooser(int thumbnailHeight, int thumbnailWidth, String thumbnailUrl, String sourceUrl, int overlap) {
        initWidget(sUiBinder.createAndBindUi(this));
        fBoxHeight = thumbnailHeight;
        fBoxWidth = thumbnailWidth;
        fThumbnailUrl = thumbnailUrl;
        fSourceUrl = sourceUrl;
        fOverlap = overlap;
        init();
    }
 
    private void init() {
        thumbnailBox.setPixelSize(fBoxWidth, fBoxHeight);
        Image.prefetch(fSourceUrl);
        thumbnail.setUrl(fThumbnailUrl);
    }
 
    private void scaleToFit(Image imageToScale, int overlap) {
        if (imageToScale.getWidth() < imageToScale.getHeight()) {
            int height = imageToScale.getHeight();
            double percent = (double) (fBoxWidth + overlap) / imageToScale.getWidth();
            imageToScale.setWidth(fBoxWidth + overlap + Unit.PX.getType());
            imageToScale.setHeight((height * percent) + Unit.PX.getType());
        } else {
            int width = imageToScale.getWidth();
            double percent = (double) (fBoxHeight + overlap) / imageToScale.getHeight();
            imageToScale.setHeight(fBoxHeight + overlap + Unit.PX.getType());
            imageToScale.setWidth((width * percent) + Unit.PX.getType());
        }
    }
 
    @UiHandler("thumbnail")
    void onImageLoad(LoadEvent event) {
        // scheduler needed for Chrome, otherwise width/height of image is always 0 (see
        // http://code.google.com/p/google-web-toolkit/issues/detail?id=6848)
        Scheduler.get().scheduleDeferred(new ScheduledCommand() {
 
            @Override
            public void execute() {
                scaleToFit(thumbnail, fSourceLoaded ? fOverlap : 0);
            }
        });
    }
 
    @UiHandler("thumbnail")
    void onMouseDown(MouseDownEvent event) {
        // prevent dragging of image
        event.preventDefault();
    }
 
    @UiHandler("thumbnailBox")
    void onMouseDownBox(MouseDownEvent event) {
        if (!fSourceLoaded) {
            fMover = new ImageMover();
            fSourceLoaded = true;
            thumbnail.setUrl(fSourceUrl);
        }
        fMover.setStartPoints(event.getClientX(), event.getClientY());
        fMouseUpRegistration = Event.addNativePreviewHandler(fMouseUpHandler);
        fPressed = true;
    }
 
    @UiHandler("thumbnailBox")
    void onMouseMoveBox(MouseMoveEvent event) {
        if (fSourceLoaded && fPressed) {
            fMover.move(event.getClientX(), event.getClientY());
        }
    }
 
    @UiHandler("thumbnailBox")
    void onMouseUpBox(MouseUpEvent event) {
        fPressed = false;
    }
 
    /**
     * Returns width of the thumbnail box.
     * 
     * @return thumbnail width
     */
    public int getThumbnailWidth() {
        return fBoxWidth;
    }
 
    /**
     * Returns height of thumbnail box.
     * 
     * @return thumbnail height
     */
    public int getThumbnailHeight() {
        return fBoxHeight;
    }
 
    /**
     * Returns the new x coordinate of the upper left corner of the selected thumbnail. Add {@link #getThumbnailWidth()}
     * to get the new horizontal viewport.
     * 
     * @return x coordinate of the upper left corner of the viewport
     */
    public int getX() {
        return fLeft * -1;
    }
 
    /**
     * Returns the new y coordinate of the upper left corner of the selected thumbnail. Add
     * {@link #getThumbnailHeight()} to get the new vertical viewport.
     * 
     * @return y coordinate of the upper left corner of the viewport
     */
    public int getY() {
        return fTop * -1;
    }
 
    private class ImageMover {
 
        private int startX;
        private int startY;
 
        public void setStartPoints(int clientX, int clientY) {
            startX = clientX;
            startY = clientY;
        }
 
        public void move(int clientX, int clientY) {
            // horizontal movement
            int hDiff = startX - clientX;
            int left = getLeft();
            startX = clientX;
            if (hDiff >= 0) {
                fLeft = Math.max(left - hDiff, fBoxWidth - thumbnail.getWidth());
            } else {
                fLeft = Math.min(left + Math.abs(hDiff), 0);
            }
 
            // vertical movement
            int vDiff = startY - clientY;
            startY = clientY;
            int top = getTop();
            if (vDiff >= 0) {
                fTop = Math.max(top - vDiff, fBoxHeight - thumbnail.getHeight());
            } else {
                fTop = Math.min(top + Math.abs(vDiff), 0);
            }
            thumbnail.getElement().getStyle().setLeft(fLeft, Unit.PX);
            thumbnail.getElement().getStyle().setTop(fTop, Unit.PX);
        }
 
        private int getTop() {
            String top = thumbnail.getElement().getStyle().getTop();
            if (top.isEmpty()) {
                return 0;
            }
            return Integer.parseInt(top.replaceAll(Unit.PX.getType(), ""));
        }
 
        private int getLeft() {
            String left = thumbnail.getElement().getStyle().getLeft();
            if (left.isEmpty()) {
                return 0;
            }
            return Integer.parseInt(left.replaceAll(Unit.PX.getType(), ""));
        }
    }
}

Parse URL with GWT

We’ve implemented a simple rich text editor that supports basic text formatting like bold, italic, lists and so on. We also wanted to provide the functionality to enter URLs that automatically are truncated and afterwards transformed into anchor elements. For example the URL http://www.youtube.com/watch?v=T0X9BcBd-I0 should be transformed into <a href="http://www.youtube.com/watch?v=T0X9BcBd-I0">www.youtube.com</a>. This improves the reading as well as the look of an user post or comment.

Since we can’t use any of the classes of java.util.regex the solution below only relies on the regex methods of String. In our editor the parsing is done continuously while the user types. Meaning it is possible that toMarkup already contains an anchor element that was replaced previously. Therefore, we must not replace that URL again or more general, we must not parse any URL already contained in an anchor tag. The last part of PATTERN accounts for that situation.

/**
 * Converts URLs into <a href="url">short form of url</a> elements.
 * 
 * The shorter form of the URL will contain the subdomain, domain and top domain. Protocol (http(s) or
 * ftp) and path will be removed.
 * 
 * @author zubi
 * 
 */
public class LinkParser implements RichTextHTMLParser {
 
    private static final String BEFORE_DOMAIN = "\\b((https?|ftp)://)";
    private static final String PATH = "(:\\d+)?(/[-a-z0-9A-Z_:@&?=+,.!/~*'%#$]*)*";
    private static final String PATTERN =
            BEFORE_DOMAIN
                    + "?([a-z0-9](?:[-a-z0-9A-Z]*[a-z0-9])?\\.)+(com\\b|edu\\b|biz\\b|gov\\b|in(?:t|fo)\\b|mil\\b|net\\b|org\\b|[a-z][a-z]\\b)"
                    + PATH + "(?!((?!(?:<a )).)*?(?:</a>))";
 
    @Override
    public String getHTML(String toMarkup) {
        String[] splits = toMarkup.split(PATTERN, 2);
        String result = toMarkup;
        while (splits.length > 1) {
            result = replaceUrl(result, splits);
            splits = result.split(PATTERN);
        }
        if (result.matches(".+?" + PATTERN)) {
            int start = splits[0].length();
            result = result.replaceAll(PATTERN, getLink(result.substring(start)));
        }
        return result;
    }
 
    private String replaceUrl(String text, String[] parts) {
        int start = parts[0].length();
        int end = parts[0].length() + text.substring(parts[0].length()).lastIndexOf(parts[1]);
        String url = text.substring(start, end);
        return text.replaceFirst(PATTERN, getLink(url));
    }
 
    private String getLink(String url) {
        if (!url.matches(BEFORE_DOMAIN + ".*")) {
            url = "http://" + url;
        }
        return "<a target=\"_blank\" href=\"" + url + "\">" + getDomain(url) + "</a>";
    }
 
    private String getDomain(String url) {
        String domain = url.replaceAll(BEFORE_DOMAIN, "").replaceAll(PATH, "");
        return domain;
    }
}

Autogrow TextArea with GWT

This post is about a GWT only implementation of an autogrow textarea. That is, a textarea that automatically grows and shrinks vertically according to it’s content (best known of Facebook when writing comments or messages).

In our solution we calculate the needed height of the textarea upon every KeyUpEvent and KeyDownEvent. The KeyDownEvent is needed if a user keeps pressing a button. In this case only one KeyUpEvent is fired at the time when the key is released. Though the textarea won’t adjust its size during the pressing.

To calculate the size to fit the current content we use the following
shrink() and grow() methods:

private void grow() {
    while (getElement().getScrollHeight() > getElement().getClientHeight()) {
        setVisibleLines(getVisibleLines() + fGrowLines);
    }
}
 
private void shrink() {
    int rows = getVisibleLines();
    while (rows > fInitialLines) {
        setVisibleLines(--rows);
    }
}

Where fInitialLines is the minimum size (in lines) of the textarea and fGrowLines are the number of lines the textarea will grow. Whenever one of the mentioned key events occurs we run both methods to adjust the size.

One last thing we took account of was cutting and pasting when triggered via right mouse click or the menu. We therefore sink two more events: ONPASTE and ONCUT. Whereas ONPASTE is already supported by GWT we have to use JSNI to register for ONCUT events (there’s an open issue for it).

private native void registerOnCut(Element element) /*-{
    var that = this.@com.ambitz.everest.gwt.client.widget.autogrowtextarea.AutoGrowTextArea::fSizeHandler;
    element.oncut = $entry(function() {
    that.@com.ambitz.everest.gwt.client.widget.autogrowtextarea.AutoGrowTextArea.SizeHandler::shrink()();
    return false;
    });
}-*/;

Here’s the complete class:

import com.google.gwt.dom.client.Style.Overflow;
import com.google.gwt.event.dom.client.KeyPressEvent;
import com.google.gwt.event.dom.client.KeyPressHandler;
import com.google.gwt.event.dom.client.KeyUpEvent;
import com.google.gwt.event.dom.client.KeyUpHandler;
import com.google.gwt.user.client.Command;
import com.google.gwt.user.client.DOM;
import com.google.gwt.user.client.DeferredCommand;
import com.google.gwt.user.client.Element;
import com.google.gwt.user.client.Event;
import com.google.gwt.user.client.ui.TextArea;
 
/**
 * A {@link TextArea} that automatically grows and shrinks depending on its content.
 * 
 * @author zubi
 * 
 */
public class AutoGrowTextArea extends TextArea {
 
    private class SizeHandler implements KeyUpHandler, KeyPressHandler, BlurHandler {
 
        @Override
        public void onKeyUp(KeyUpEvent event) {
            size();
        }
 
        public void size() {
            shrink();
            grow();
        }
 
        private void grow() {
            while (getElement().getScrollHeight() > getElement().getClientHeight()) {
                setVisibleLines(getVisibleLines() + fGrowLines);
            }
        }
 
        private void shrink() {
            int rows = getVisibleLines();
            while (rows > fInitialLines) {
                setVisibleLines(--rows);
            }
        }
 
        @Override
        public void onKeyPress(KeyPressEvent event) {
            size();
        }
 
        @Override
        public void onBlur(BlurEvent event) {
            size();
        }
    }
 
    private int fInitialLines;
    private int fGrowLines;
    private SizeHandler fSizeHandler;
 
    /**
     * Creates new text area. Initial number of lines is set to 2, grow lines is set to 1 (see
     * {@link #AutoGrowTextArea(int, int)}.
     */
    public AutoGrowTextArea() {
        this(2, 1);
    }
 
    /**
     * Creates new text area.
     * 
     * @param initialLines
     *            how high in terms of visible lines the initial text box is (the height will never go below this
     *            number)
     * @param growLines
     *            how many lines the text box grows when content reaches the current last line
     */
    public AutoGrowTextArea(int initialLines, int growLines) {
        super();
        registerHandlers();
        adjustStyle();
        setVisibleLines(initialLines);
        fInitialLines = initialLines;
        fGrowLines = growLines;
    }
 
    private void adjustStyle() {
        getElement().getStyle().setOverflow(Overflow.HIDDEN);
        getElement().getStyle().setProperty("resize", "none");
    }
 
    private void registerHandlers() {
        fSizeHandler = new SizeHandler();
        addKeyUpHandler(fSizeHandler);
        addKeyPressHandler(fSizeHandler);
        addBlurHandler(fSizeHandler);
        sinkEvents(Event.ONPASTE);
        registerOnCut(getElement());
    }
 
    private native void registerOnCut(Element element) /*-{
        var that = this.@com.ambitz.everest.gwt.client.widget.autogrowtextarea.AutoGrowTextArea::fSizeHandler;
        element.oncut = $entry(function() {
        that.@com.ambitz.everest.gwt.client.widget.autogrowtextarea.AutoGrowTextArea.SizeHandler::shrink()();
        return false;
        });
    }-*/;
 
    @Override
    public void onBrowserEvent(Event event) {
        super.onBrowserEvent(event);
        switch (DOM.eventGetType(event)) {
            case Event.ONPASTE:
                Scheduler.get().scheduleDeferred(new ScheduledCommand() {
 
                    @Override
                    public void execute() {
                        fSizeHandler.size();
                    }
 
                });
                break;
        }
    }
}

Edit:
As mentioned by Mike (thanks for that!) you shouldn’t set a height on the textarea. Otherwise setVisibleLines() won’t work.