info.magnolia.ui.vaadin.gwt.client.layout.thumbnaillayout.widget.EscalatorThumbnailsPanel.java Source code

Java tutorial

Introduction

Here is the source code for info.magnolia.ui.vaadin.gwt.client.layout.thumbnaillayout.widget.EscalatorThumbnailsPanel.java

Source

/**
 * This file Copyright (c) 2015 Magnolia International
 * Ltd.  (http://www.magnolia-cms.com). All rights reserved.
 *
 *
 * This file is dual-licensed under both the Magnolia
 * Network Agreement and the GNU General Public License.
 * You may elect to use one or the other of these licenses.
 *
 * This file is distributed in the hope that it will be
 * useful, but AS-IS and WITHOUT ANY WARRANTY; without even the
 * implied warranty of MERCHANTABILITY or FITNESS FOR A
 * PARTICULAR PURPOSE, TITLE, or NONINFRINGEMENT.
 * Redistribution, except as permitted by whichever of the GPL
 * or MNA you select, is prohibited.
 *
 * 1. For the GPL license (GPL), you can redistribute and/or
 * modify this file under the terms of the GNU General
 * Public License, Version 3, as published by the Free Software
 * Foundation.  You should have received a copy of the GNU
 * General Public License, Version 3 along with this program;
 * if not, write to the Free Software Foundation, Inc., 51
 * Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
 *
 * 2. For the Magnolia Network Agreement (MNA), this file
 * and the accompanying materials are made available under the
 * terms of the MNA which accompanies this distribution, and
 * is available at http://www.magnolia-cms.com/mna.html
 *
 * Any modifications to this file must keep this entire header
 * intact.
 *
 */
package info.magnolia.ui.vaadin.gwt.client.layout.thumbnaillayout.widget;

import info.magnolia.ui.vaadin.gwt.client.jquerywrapper.JQueryWrapper;
import info.magnolia.ui.vaadin.gwt.client.layout.thumbnaillayout.connector.LazyThumbnailLayoutConnector;
import info.magnolia.ui.vaadin.gwt.client.layout.thumbnaillayout.connector.ThumbnailLayoutState;
import info.magnolia.ui.vaadin.gwt.client.pinch.MagnoliaPinchRecognizer;
import info.magnolia.ui.vaadin.gwt.client.pinch.MagnoliaPinchStartEvent;
import info.magnolia.ui.vaadin.gwt.shared.Range;

import java.util.Deque;
import java.util.HashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Set;

import com.google.gwt.core.client.Scheduler;
import com.google.gwt.dom.client.DivElement;
import com.google.gwt.dom.client.Element;
import com.google.gwt.dom.client.ImageElement;
import com.google.gwt.dom.client.NativeEvent;
import com.google.gwt.dom.client.SpanElement;
import com.google.gwt.dom.client.Style;
import com.google.gwt.event.dom.client.ClickEvent;
import com.google.gwt.event.dom.client.ClickHandler;
import com.google.gwt.event.dom.client.ContextMenuEvent;
import com.google.gwt.event.dom.client.ContextMenuHandler;
import com.google.gwt.event.dom.client.ScrollEvent;
import com.google.gwt.event.dom.client.ScrollHandler;
import com.google.gwt.event.logical.shared.ValueChangeEvent;
import com.google.gwt.event.logical.shared.ValueChangeHandler;
import com.google.gwt.user.client.DOM;
import com.google.gwt.user.client.ui.FlowPanel;
import com.google.gwt.user.client.ui.ScrollPanel;
import com.googlecode.mgwt.dom.client.recognizer.pinch.UIObjectToOffsetProvider;
import com.googlecode.mgwt.dom.client.recognizer.tap.MultiTapEvent;
import com.googlecode.mgwt.dom.client.recognizer.tap.MultiTapHandler;
import com.googlecode.mgwt.dom.client.recognizer.tap.MultiTapRecognizer;
import com.googlecode.mgwt.ui.client.widget.touch.TouchDelegate;
import com.vaadin.client.Util;

/**
 * Implementation of a lazy thumbnail gallery widget.
 *
 * <p/>
 * Laziness is achieved by using a so-called 'escalator' pattern.
 * The real size of the scrollable area is achieved by combing the visible viewport which contains the currently displayed
 * thumbnails with the two spacer elements.
 *
 * <p/>
 * When scrolling occurs the spaces are adjusted to keep the viewport visible and the same thumbnail elements
 * are re-used to create a feeling of a viewport update - as some row moves out of the view it is moved in the DOM to the opposite edge
 * of the viewport (to top or bottom) like an escalator.
 */
public class EscalatorThumbnailsPanel extends FlowPanel {

    private static final String THUMBNAIL_LAYOUT_STYLE_NAME = "thumbnail-layout";
    private static final String THUMBNAIL_SCROLLER_STYLE_NAME = "thumbnail-scroller";
    public static final int MAX_PREFFERED_AMOUNT_OF_THUMBNAILS_IN_ROW = 8;

    /**
     * Listener interface for processing thumbnail click/tap events.
     */
    public interface Listener {

        void onThumbnailClicked(int index, boolean isMetaKeyPressed, boolean isShiftKeyPressed);

        void onThumbnailRightClicked(int index, int xPos, int yPos);

        void onThumbnailDoubleClicked(int index);

    }

    private Thumbnail thumbnailFlyweight;

    private final ScrollPanel scroller = new ScrollPanel();

    private final DivElement imageContainer = DivElement.as(DOM.createDiv());

    private final DivElement upperSpacer = DivElement.as(DOM.createDiv());

    private final DivElement lowerSpacer = DivElement.as(DOM.createDiv());

    private LazyThumbnailLayoutConnector.ThumbnailService thumbnailService;

    private Deque<Element> floatingThumbnails = new LinkedList<>();

    private int absoluteOffset = 0;

    private int thumbnailAmount;

    private int thumbnailsInRow;

    private int rowsInViewport;

    private ThumbnailsSizeKeeper size;

    private boolean isScrollProcessorLocked = false;

    private Listener listener;

    private final Slider thumbnailSizeSlider = new Slider();

    private final ScrollHandler scrollHandler = new ScrollHandler() {

        private int lastScrollTop = 0;

        @Override
        public void onScroll(ScrollEvent event) {
            if (isScrollProcessorLocked) {
                return;
            }
            int newScrollTop = event.getRelativeElement().getScrollTop();
            int delta = lastScrollTop - newScrollTop;
            escalate(newScrollTop, delta);
            lastScrollTop = newScrollTop;
        }
    };

    /**
     * On scroll - fix the spacer elements and replace the thumbnails that have gone out of view
     * with stubs.
     */
    private void escalate(int newScrollTop, int delta) {
        int lsHeight = lowerSpacer.getOffsetHeight();
        int usHeight = upperSpacer.getOffsetHeight();

        int thumbnailHeight = size.height();

        boolean moveOccurred = false;

        if (delta != 0) {
            /**
             * Scrolling down - moving thumbnail rows from top to bottom
             * in order to mimic an always visible viewport.
             */
            if (delta < 0) {
                while (newScrollTop - usHeight >= thumbnailHeight && lsHeight > 0) {
                    moveOccurred = true;

                    usHeight += thumbnailHeight;
                    lsHeight -= thumbnailHeight;

                    this.absoluteOffset = usHeight / size.height() * thumbnailsInRow;

                    // Tossing around stubs makes no sense in case there's no 'real' thumbnails in viewport
                    if (!areAllVisibleThumbnailsCleared()) {
                        releaseThumbnailRow(true);
                        addStubs(Range.withLength(imageContainer.getChildCount(), thumbnailsInRow));
                    }
                }
            } else {
                /**
                 * Scrolling up - moving thumbnail rows from bottom to top
                 */
                while (usHeight - newScrollTop > 0 && usHeight > 0) {
                    moveOccurred = true;

                    usHeight -= thumbnailHeight;
                    lsHeight += thumbnailHeight;

                    this.absoluteOffset = usHeight / size.height() * thumbnailsInRow;

                    // Tossing around stubs makes no sense in case there's no 'real' thumbnails in viewport
                    if (!areAllVisibleThumbnailsCleared()) {
                        releaseThumbnailRow(false);
                        addStubs(Range.between(0, thumbnailsInRow));
                    }
                }
            }
        }

        if (moveOccurred) {
            setSpacersHeight(usHeight, lsHeight);
            updateViewport();
        }
    }

    public EscalatorThumbnailsPanel(final Listener listener) {
        this.listener = listener;

        final TouchDelegate touchDelegate = new TouchDelegate(this);
        touchDelegate.addTouchHandler(
                new MagnoliaPinchRecognizer(touchDelegate, new UIObjectToOffsetProvider(scroller)));
        MultiTapRecognizer multitapRecognizer = new MultiTapRecognizer(touchDelegate, 1, 2);
        touchDelegate.addTouchHandler(multitapRecognizer);

        addHandler(new MagnoliaPinchStartEvent.Handler() {
            @Override
            public void onPinchStart(MagnoliaPinchStartEvent event) {
                /**
                 * TODO: Pinch does not work reliably yet, to be sorted out...
                 */
                //scale((float) event.getScaleFactor());
            }
        }, MagnoliaPinchStartEvent.TYPE);

        addDomHandler(new ClickHandler() {
            @Override
            public void onClick(ClickEvent event) {
                final NativeEvent nativeEvent = event.getNativeEvent();
                final Element element = findThumbnail(Element.as(nativeEvent.getEventTarget()));
                if (element != null) {
                    boolean isMetaKeyPressed = nativeEvent.getMetaKey();
                    boolean isShiftPressed = nativeEvent.getShiftKey();
                    EscalatorThumbnailsPanel.this.listener.onThumbnailClicked(getThumbnailIndex(element),
                            isMetaKeyPressed, isShiftPressed);
                }

            }
        }, ClickEvent.getType());

        addDomHandler(new ContextMenuHandler() {

            @Override
            public void onContextMenu(ContextMenuEvent event) {
                Element thumbnail = findThumbnail(Element.as(event.getNativeEvent().getEventTarget()));
                if (thumbnail != null) {
                    EscalatorThumbnailsPanel.this.listener.onThumbnailRightClicked(getThumbnailIndex(thumbnail),
                            event.getNativeEvent().getClientX(), event.getNativeEvent().getClientY());
                }

            }
        }, ContextMenuEvent.getType());

        addHandler(new MultiTapHandler() {
            @Override
            public void onMultiTap(MultiTapEvent event) {
                int x = event.getTouchStarts().get(0).get(0).getPageX();
                int y = event.getTouchStarts().get(0).get(0).getPageY();
                final Element thumbnail = findThumbnail(Util.getElementFromPoint(x, y));
                if (thumbnail != null) {
                    EscalatorThumbnailsPanel.this.listener.onThumbnailDoubleClicked(getThumbnailIndex(thumbnail));
                }
            }
        }, MultiTapEvent.getType());

        thumbnailSizeSlider.addValueChangeHandler(new ValueChangeHandler<Integer>() {
            @Override
            public void onValueChange(ValueChangeEvent<Integer> event) {
                scale(event.getValue() / 100f);
            }
        });

        add(thumbnailSizeSlider);

        this.size = new ThumbnailsSizeKeeper(imageContainer);

        addStyleName(THUMBNAIL_LAYOUT_STYLE_NAME);

        scroller.getElement().appendChild(imageContainer);
        scroller.addStyleName(THUMBNAIL_SCROLLER_STYLE_NAME);
        scroller.addScrollHandler(scrollHandler);

        scroller.getElement().insertFirst(upperSpacer);
        scroller.getElement().insertAfter(lowerSpacer, imageContainer);

        add(scroller);

        thumbnailFlyweight = new Thumbnail();
    }

    public final int getCurrentThumbnailOffset() {
        return absoluteOffset;
    }

    public int getCurrentlyDisplayedThumbnails() {
        return imageContainer.getChildCount();
    }

    public void initialize(int thumbnailAmount, int offset, ThumbnailLayoutState.ThumbnailSize size,
            float scaleRatio, boolean isFirstUpdateFromState) {
        this.imageContainer.removeAllChildren();

        this.thumbnailAmount = thumbnailAmount;
        this.size.updateAllThumbnailsSize(size.width, size.height);
        // Set initial scale ratio
        if (scaleRatio >= 0) {
            this.size.scale(scaleRatio);
        }

        // Calculate initial sizes
        resize();

        // If amount of thumbnails in a row exceeds the default threshold - scale them so that
        // there's not that many thumbnails in viewport
        if (isFirstUpdateFromState && thumbnailsInRow > MAX_PREFFERED_AMOUNT_OF_THUMBNAILS_IN_ROW) {
            int calculatedThumbnailWidth = imageContainer.getOffsetWidth()
                    / MAX_PREFFERED_AMOUNT_OF_THUMBNAILS_IN_ROW;
            scaleToWidth(calculatedThumbnailWidth);
        }

        thumbnailSizeSlider.setValue((int) (this.size.getScaleRatio() * 100), false);
        scroller.setVerticalScrollPosition((offset / thumbnailsInRow) * size.height);
    }

    public void setThumbnailService(LazyThumbnailLayoutConnector.ThumbnailService thumbnailService) {
        this.thumbnailService = thumbnailService;
    }

    public void setThumbnailAmount(int amount) {
        if (this.thumbnailAmount != amount) {
            this.thumbnailAmount = amount;
            resize();
        }
    }

    public void setThumbnailSize(int width, int height) {
        if (size.updateAllThumbnailsSize(width, height)) {
            resize();
        }
    }

    public Range getDisplayedRange() {
        return Range.between(absoluteIndex(0), absoluteIndex(imageContainer.getChildCount()));
    }

    public void resize() {
        int initialScrollTop = scroller.getVerticalScrollPosition();
        int viewportWidth = getScrollableWidth();
        int scrollerHeight = getScrollableHeight();
        int thumbnailHeight = size.height();

        if (scrollerHeight == 0 || viewportWidth == 0 || thumbnailHeight == 0) {
            return;
        }

        int thumbnailWidth = size.width();
        this.thumbnailsInRow = viewportWidth / thumbnailWidth;
        int totalRows = (int) Math.ceil((double) thumbnailAmount / thumbnailsInRow);
        this.rowsInViewport = Math.min((int) Math.ceil((double) scrollerHeight / thumbnailHeight) + 1, totalRows);

        final Range totalRange = Range.between(0, thumbnailAmount);

        Range offsetRange = Range.between(0, absoluteOffset);
        Range viewportActualRange = Range.withLength(absoluteOffset, getCurrentlyDisplayedThumbnails());
        Range remainingRange = totalRange.partitionWith(viewportActualRange)[2];

        boolean rangesAreInOrder = false;
        /**
         * Iterate until all calculations are aligned. According to the algorithm specifics there should be never more than two
         * iterations - the additional one could occur if there is not enough thumbnails in the viewport and we can't fill them
         * from the lower spacer - then we need to shift the upper spacer and take some thumbnails from there which means the change of
         * the offset and the upper spacer alignment.
         */
        while (!rangesAreInOrder) {
            /**
             * Re-calculate the offset first: the upper-spacer must be fully filled with 'virtual' thumbnails in order
             * to provide the correct value of the offset, so we need to see if the amount of thumbnails currently residing in
             * the offset range can be divided by the new amount of thumbnails per-row witout remainder. If there is a remainder -
             * it should be pushed into viewport and the offset should be adjusted.
             */
            int offsetRangeRemainder = offsetRange.length() % thumbnailsInRow;
            if (offsetRangeRemainder != 0) {
                addStubs(Range.withLength(relativeIndex(viewportActualRange.getStart()), offsetRangeRemainder));
                offsetRange = offsetRange.expand(0, -offsetRangeRemainder);
                viewportActualRange = viewportActualRange.expand(offsetRangeRemainder, 0);
                absoluteOffset -= offsetRangeRemainder;
            }

            /**
             * Update the viewport. We can be either in situation when there is not enough thumbnails in viewport (when we zoom out or
             * enlarge the viewport) or there is an overflow of thumbnails (when we zoom in).
             */
            Range viewportCalculatedRange = Range.withLength(absoluteOffset, rowsInViewport * thumbnailsInRow);

            /**
             * Let's see if the amount of thumbnails we have to display in viewport does not exceed the total amount.
             * {@code beyondTheTotalRange} variable tracks that overflow if any.
             */
            final Range[] calculatedVsTotalRange = viewportCalculatedRange.partitionWith(totalRange);
            final Range beyondTheTotalRange = calculatedVsTotalRange[2];

            /**
             * Let us meanwhile reduce the range of thumbnails to be displayed in viewport to those that are within available range.
             */
            viewportCalculatedRange = calculatedVsTotalRange[1];

            /**
             * In case we already display too many thumbnails - release the overflow.
             */
            if (viewportCalculatedRange.isSubsetOf(viewportActualRange)) {

                int overflow = viewportActualRange.partitionWith(viewportCalculatedRange)[2].length();
                releaseThumbnails(Range.withLength(imageContainer.getChildCount() - overflow, overflow));
                viewportActualRange = viewportActualRange.expand(0, -overflow);
                remainingRange = remainingRange.expand(overflow, 0);
                /**
                 * Else in case there're some thumbnails to be added - let's do that.
                 */
            } else if (viewportActualRange.isSubsetOf(viewportCalculatedRange)) {

                int lack = viewportCalculatedRange.partitionWith(viewportActualRange)[2].length();
                addStubs(Range.withLength(imageContainer.getChildCount(), lack));
                viewportActualRange = viewportActualRange.expand(0, lack);
                remainingRange = remainingRange.expand(-lack, 0);
            }

            /**
             * If the initial calculated range to be displayed goes beyond the total amount of thumbnails:
             * Fill viewport with the thumbnails that are displayed above by kind of "shifting" the viewport back.
             */
            if (!beyondTheTotalRange.isEmpty()) {
                int rowsToShift = (int) Math.floor((double) beyondTheTotalRange.length() / thumbnailsInRow);
                int toQueryFromOffset = rowsToShift * thumbnailsInRow;
                if (toQueryFromOffset > 0) {
                    absoluteOffset -= toQueryFromOffset;
                    offsetRange = offsetRange.expand(0, -toQueryFromOffset);
                    viewportActualRange = viewportActualRange.expand(toQueryFromOffset, 0);
                    addStubs(Range.between(0, toQueryFromOffset));
                }
            }

            // Since we might have compensated the lack of thumbnails in viewport from the offset
            // area - it might get out shape, we'd have to repeat the operation
            rangesAreInOrder = offsetRange.length() % thumbnailsInRow == 0;
        }

        /**
         * Update sized according to the calculations above, fix the scroll top based on the relation of the
         * former upper spacer height, to its new height.
         */
        int usH = upperSpacer.getOffsetHeight();

        int viewportHeight = rowsInViewport * thumbnailHeight;
        int upperSpacerHeight = (int) Math.ceil(offsetRange.length() / thumbnailsInRow) * thumbnailHeight;
        int lowerSpacerHeight = (int) Math.ceil(remainingRange.length() / thumbnailsInRow) * thumbnailHeight;

        // Set the sizes finally
        scroller.setHeight(scrollerHeight + "px");

        imageContainer.getStyle().setHeight(viewportHeight, Style.Unit.PX);
        imageContainer.getStyle().setWidth(viewportWidth, Style.Unit.PX);

        setSpacersHeight(upperSpacerHeight, lowerSpacerHeight);

        /**
         * Try to put the scroll top to where it used to be approximately.
         */
        isScrollProcessorLocked = true;
        scroller.setVerticalScrollPosition(initialScrollTop + upperSpacerHeight - usH);
        Scheduler.get().scheduleFinally(new Scheduler.RepeatingCommand() {
            @Override
            public boolean execute() {
                isScrollProcessorLocked = false;
                return false;
            }
        });

        updateViewport();
    }

    protected Element findThumbnail(Element element) {
        if (element != imageContainer && element != null && imageContainer.isOrHasChild(element)) {
            Element result = element;
            while (result.getParentElement() != imageContainer) {
                result = result.getParentElement();
            }
            return result;
        }
        return null;
    }

    protected int getThumbnailIndex(Element element) {
        return absoluteIndex(Util.getChildElementIndex(element));
    }

    protected void scaleToWidth(int width) {
        size.scaleToWidth(width);
        thumbnailService.onThumbnailsScaled(size.getScaleRatio());
        resize();
    }

    protected void scale(float ratio) {
        thumbnailService.onThumbnailsScaled(ratio);
        size.scale(ratio);
        resize();
    }

    private void addStubs(Range relativeRange) {
        int currentThumbnailOffset = getCurrentThumbnailOffset();
        final Range actualRange = relativeRange.expand(-currentThumbnailOffset, currentThumbnailOffset)
                .restrictTo(Range.between(0, thumbnailAmount));

        int relativeStartIndex = relativeIndex(actualRange.getStart());

        Element previousElement = relativeStartIndex == 0 ? null
                : Element.as(imageContainer.getChild(relativeStartIndex - 1));
        for (int i = 0; i < actualRange.length(); ++i) {
            final Element stub = floatingThumbnails.isEmpty() ? thumbnailFlyweight.createThumbnail()
                    : floatingThumbnails.pop();
            size.applySizeToThumbnail(stub);
            if (previousElement == null) {
                imageContainer.insertFirst(stub);
            } else {
                imageContainer.insertAfter(stub, previousElement);
            }
            previousElement = stub;
        }
    }

    private void updateViewport() {
        final Range visibleRange = Range.between(absoluteIndex(0), absoluteIndex(imageContainer.getChildCount()));
        if (areAllVisibleThumbnailsCleared()) {
            final Range availableRange = Range.between(0, thumbnailAmount);
            if (!visibleRange.isSubsetOf(availableRange)) {
                Range thumbnailsToRelease = visibleRange.partitionWith(availableRange)[2];
                if (!thumbnailsToRelease.isEmpty()) {
                    for (int i = 0; i < thumbnailsToRelease.length(); ++i) {
                        clearThumbnail(Element.as(imageContainer.getLastChild()));
                    }
                }
            }
        }
        this.thumbnailService
                .onViewportChanged(Range.between(absoluteIndex(0), absoluteIndex(imageContainer.getChildCount())));
    }

    public void updateImageSource(String url, int index) {
        thumbnailFlyweight.setImageSrc(url, Element.as(imageContainer.getChild(relativeIndex(index))));
    }

    private int relativeIndex(int index) {
        return index - getCurrentThumbnailOffset();
    }

    private int absoluteIndex(int relativeIndex) {
        return getCurrentThumbnailOffset() + relativeIndex;
    }

    private void releaseThumbnailRow(boolean inFront) {
        for (int i = 0; i < thumbnailsInRow; ++i) {
            final Element thumbnail = (Element) (inFront ? imageContainer.getFirstChild()
                    : imageContainer.getLastChild());
            clearThumbnail(thumbnail);
        }
    }

    private void releaseThumbnails(Range between) {
        final Set<Element> toRelease = new HashSet<>();
        for (int i = between.getStart(); i < between.getEnd(); ++i) {
            toRelease.add(Element.as(imageContainer.getChild(i)));
        }

        for (final Element thumbnail : toRelease) {
            clearThumbnail(thumbnail);
        }
    }

    private void clearThumbnail(Element thumbnail) {
        thumbnailFlyweight.clear(thumbnail);
        thumbnail.removeFromParent();
        floatingThumbnails.push(thumbnail);
    }

    public void updateIconFontStyle(String style, int index) {
        thumbnailFlyweight.setIconFontStyle(style, Element.as(imageContainer.getChild(relativeIndex(index))));
    }

    public void setSelectedThumbnailsViaIndices(List<Integer> indices) {
        clearSelection();
        for (int absIndex : indices) {
            Element.as(imageContainer.getChild(relativeIndex(absIndex))).addClassName("selected");
        }
    }

    private void setSpacersHeight(int usHeight, int lsHeight) {
        upperSpacer.getStyle().setHeight(Math.max(usHeight, 0), Style.Unit.PX);
        lowerSpacer.getStyle().setHeight(Math.max(lsHeight, 0), Style.Unit.PX);
    }

    private void clearSelection() {
        final JQueryWrapper selectedThumbnails = JQueryWrapper.select(DOM.asOld(imageContainer)).find(".selected");
        if (selectedThumbnails != null) {
            selectedThumbnails.removeClass("selected");
        }
    }

    private boolean areAllVisibleThumbnailsCleared() {
        return imageContainer.getChildCount() == JQueryWrapper.select(DOM.asOld(imageContainer)).find(".cleared")
                .size();
    }

    private int getScrollableHeight() {
        return getElement().getOffsetHeight() - thumbnailSizeSlider.getOffsetHeight();
    }

    private int getScrollableWidth() {
        return getOffsetWidth();
    }

    private static class Thumbnail {

        static final String ICON_STYLE_NAME = "icon";
        static final String THUMBNAIL_STYLE_NAME = "thumbnail";
        static final String THUMBNAIL_IMAGE_STYLE_NAME = "thumbnail-image";
        public static final String CLEARED_STYLE_NAME = "cleared";

        Element createThumbnail() {

            final DivElement thumbnail = DivElement.as(DOM.createDiv());
            thumbnail.addClassName(THUMBNAIL_STYLE_NAME);

            final SpanElement iconFontEl = SpanElement.as(DOM.createSpan());
            iconFontEl.addClassName(ICON_STYLE_NAME);
            Style style = iconFontEl.getStyle();
            style.setDisplay(Style.Display.NONE);
            style.setFontSize(24, Style.Unit.PX);
            style.setLineHeight(1, Style.Unit.PX);

            final ImageElement image = ImageElement.as(DOM.createImg());
            image.addClassName(THUMBNAIL_IMAGE_STYLE_NAME);
            image.getStyle().setDisplay(Style.Display.NONE);

            thumbnail.appendChild(image);
            thumbnail.appendChild(iconFontEl);

            return thumbnail;
        }

        void setImageSrc(String src, Element thumbnail) {
            final Element img = getImage(thumbnail);
            final Element icon = getIcon(thumbnail);

            img.getStyle().setDisplay(Style.Display.INLINE_BLOCK);
            icon.getStyle().setDisplay(Style.Display.NONE);

            img.setAttribute("src", src);
            thumbnail.removeClassName(CLEARED_STYLE_NAME);
        }

        void setIconFontStyle(String style, Element thumbnail) {
            final Element img = getImage(thumbnail);
            final Element icon = getIcon(thumbnail);

            img.getStyle().setDisplay(Style.Display.NONE);
            icon.getStyle().setDisplay(Style.Display.INLINE_BLOCK);

            icon.setClassName(ICON_STYLE_NAME);
            icon.addClassName(style);
            thumbnail.removeClassName(CLEARED_STYLE_NAME);
        }

        private Element getIcon(Element thumbnail) {
            return Element.as(thumbnail.getChild(1));
        }

        private Element getImage(Element thumbnail) {
            return Element.as(thumbnail.getChild(0));
        }

        public void clear(Element thumbnail) {
            Element icon = getIcon(thumbnail);
            icon.setClassName(ICON_STYLE_NAME);
            icon.getStyle().setDisplay(Style.Display.NONE);

            Element image = getImage(thumbnail);
            image.removeAttribute("src");
            image.getStyle().setDisplay(Style.Display.NONE);

            thumbnail.addClassName(CLEARED_STYLE_NAME);
        }
    }
}