View Javadoc
1   /**
2    * This file Copyright (c) 2018 Magnolia International
3    * Ltd.  (http://www.magnolia-cms.com). All rights reserved.
4    *
5    *
6    * This file is dual-licensed under both the Magnolia
7    * Network Agreement and the GNU General Public License.
8    * You may elect to use one or the other of these licenses.
9    *
10   * This file is distributed in the hope that it will be
11   * useful, but AS-IS and WITHOUT ANY WARRANTY; without even the
12   * implied warranty of MERCHANTABILITY or FITNESS FOR A
13   * PARTICULAR PURPOSE, TITLE, or NONINFRINGEMENT.
14   * Redistribution, except as permitted by whichever of the GPL
15   * or MNA you select, is prohibited.
16   *
17   * 1. For the GPL license (GPL), you can redistribute and/or
18   * modify this file under the terms of the GNU General
19   * Public License, Version 3, as published by the Free Software
20   * Foundation.  You should have received a copy of the GNU
21   * General Public License, Version 3 along with this program;
22   * if not, write to the Free Software Foundation, Inc., 51
23   * Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
24   *
25   * 2. For the Magnolia Network Agreement (MNA), this file
26   * and the accompanying materials are made available under the
27   * terms of the MNA which accompanies this distribution, and
28   * is available at http://www.magnolia-cms.com/mna.html
29   *
30   * Any modifications to this file must keep this entire header
31   * intact.
32   *
33   */
34  package info.magnolia.ui.vaadin.gwt.client.layout.lazylayout.widget;
35  
36  import info.magnolia.ui.vaadin.gwt.client.jquerywrapper.JQueryWrapper;
37  import info.magnolia.ui.vaadin.gwt.client.layout.lazylayout.connector.ThumbnailLayoutState;
38  import info.magnolia.ui.vaadin.gwt.client.layout.lazylayout.connector.ThumbnailService;
39  import info.magnolia.ui.vaadin.gwt.client.pinch.MagnoliaPinchRecognizer;
40  import info.magnolia.ui.vaadin.gwt.client.pinch.MagnoliaPinchStartEvent;
41  import info.magnolia.ui.vaadin.gwt.shared.Range;
42  
43  import java.util.Deque;
44  import java.util.HashSet;
45  import java.util.LinkedList;
46  import java.util.List;
47  import java.util.Set;
48  import java.util.stream.IntStream;
49  
50  import com.google.gwt.core.client.Scheduler;
51  import com.google.gwt.dom.client.DivElement;
52  import com.google.gwt.dom.client.Element;
53  import com.google.gwt.dom.client.ImageElement;
54  import com.google.gwt.dom.client.NativeEvent;
55  import com.google.gwt.dom.client.SpanElement;
56  import com.google.gwt.dom.client.Style;
57  import com.google.gwt.event.dom.client.ClickEvent;
58  import com.google.gwt.event.dom.client.ContextMenuEvent;
59  import com.google.gwt.event.dom.client.ScrollEvent;
60  import com.google.gwt.event.dom.client.ScrollHandler;
61  import com.google.gwt.user.client.DOM;
62  import com.google.gwt.user.client.ui.FlowPanel;
63  import com.google.gwt.user.client.ui.ScrollPanel;
64  import com.googlecode.mgwt.dom.client.recognizer.pinch.UIObjectToOffsetProvider;
65  import com.googlecode.mgwt.dom.client.recognizer.tap.MultiTapEvent;
66  import com.googlecode.mgwt.dom.client.recognizer.tap.MultiTapRecognizer;
67  import com.googlecode.mgwt.ui.client.widget.touch.TouchDelegate;
68  import com.vaadin.client.Util;
69  
70  /**
71   * Implementation of a lazy thumbnail gallery widget.
72   *
73   * <p/>
74   * Laziness is achieved by using a so-called 'escalator' pattern.
75   * The real size of the scrollable area is achieved by combing the visible viewport which contains the currently displayed
76   * thumbnails with the two spacer elements.
77   *
78   * <p/>
79   * When scrolling occurs the spaces are adjusted to keep the viewport visible and the same thumbnail elements
80   * 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
81   * of the viewport (to top or bottom) like an escalator.
82   *
83   */
84  public class EscalatorPanel extends FlowPanel {
85  
86      private static final String THUMBNAIL_LAYOUT_STYLE_NAME = "thumbnail-layout";
87      private static final String THUMBNAIL_SCROLLER_STYLE_NAME = "thumbnail-scroller";
88      private static final String THUMBNAIL_VIEWPORT_STYLE_NAME = "thumbnail-viewport";
89      private static final String THUMBNAIL_PLACEHOLDER_STYLE_NAME = "placeholder";
90      private static final String THUMBNAIL_STYLE_NAME = "thumbnail";
91  
92      /**
93       * Listener interface for processing thumbnail click/tap events.
94       */
95      public interface Listener {
96  
97          void onThumbnailClicked(int index, boolean isMetaKeyPressed, boolean isShiftKeyPressed);
98  
99          void onThumbnailRightClicked(int index, int xPos, int yPos);
100 
101         void onThumbnailDoubleClicked(int index);
102 
103     }
104 
105     private Flyweight flyweight;
106 
107     private final ScrollPanel scroller = new ScrollPanel();
108 
109     private final DivElement imageContainer = DivElement.as(DOM.createDiv());
110 
111     private final DivElement upperSpacer = DivElement.as(DOM.createDiv());
112 
113     private final DivElement lowerSpacer = DivElement.as(DOM.createDiv());
114 
115     private ThumbnailService thumbnailService;
116 
117     private Deque<Element> floatingThumbnails = new LinkedList<>();
118 
119     private int absoluteOffset = 0;
120 
121     private int thumbnailAmount;
122 
123     private int thumbnailsInRow;
124 
125     private int rowsInViewport;
126 
127     private ThumbnailsSizeKeeper size;
128 
129     private boolean isScrollProcessorLocked = false;
130 
131     private Listener listener;
132 
133     private final Slidert/client/layout/lazylayout/widget/Slider.html#Slider">Slider thumbnailSizeSlider = new Slider();
134 
135     private final ScrollHandler scrollHandler = new ScrollHandler() {
136 
137         private int lastScrollTop = 0;
138 
139         @Override
140         public void onScroll(ScrollEvent event) {
141             if (isScrollProcessorLocked) {
142                 return;
143             }
144             int newScrollTop = event.getRelativeElement().getScrollTop();
145             int delta = lastScrollTop - newScrollTop;
146             escalate(newScrollTop, delta);
147             lastScrollTop = newScrollTop;
148         }
149     };
150 
151     /**
152      * On scroll - fix the spacer elements and replace the thumbnails that have gone out of view
153      * with stubs.
154      */
155     private void escalate(int newScrollTop, int delta) {
156         int lsHeight = lowerSpacer.getOffsetHeight();
157         int usHeight = upperSpacer.getOffsetHeight();
158 
159         int thumbnailHeight = size.height();
160 
161         boolean moveOccurred = false;
162 
163         if (delta != 0) {
164             /**
165              * Scrolling down - moving thumbnail rows from top to bottom
166              * in order to mimic an always visible viewport.
167              */
168             if (delta < 0) {
169                 while (newScrollTop - usHeight >= thumbnailHeight && lsHeight > 0) {
170                     moveOccurred = true;
171 
172                     usHeight += thumbnailHeight;
173                     lsHeight -= thumbnailHeight;
174 
175                     this.absoluteOffset = usHeight / size.height() * thumbnailsInRow;
176 
177                     // Tossing around stubs makes no sense in case there's no 'real' thumbnails in viewport
178                     if (!areAllVisibleThumbnailsCleared()) {
179                         releaseThumbnailRow(true);
180                         addStubs(Range.withLength(getCurrentlyDisplayedThumbnails(), thumbnailsInRow));
181                     }
182                 }
183             } else {
184                 /**
185                  * Scrolling up - moving thumbnail rows from bottom to top
186                  */
187                 while (usHeight - newScrollTop > 0 && usHeight > 0) {
188                     moveOccurred = true;
189 
190                     usHeight -= thumbnailHeight;
191                     lsHeight += thumbnailHeight;
192 
193                     this.absoluteOffset = usHeight / size.height() * thumbnailsInRow;
194 
195                     // Tossing around stubs makes no sense in case there's no 'real' thumbnails in viewport
196                     if (!areAllVisibleThumbnailsCleared()) {
197                         releaseThumbnailRow(false);
198                         addStubs(Range.between(0, thumbnailsInRow));
199                     }
200                 }
201             }
202         }
203 
204         if (moveOccurred) {
205             setSpacersHeight(usHeight, lsHeight);
206             resize();
207         }
208     }
209 
210     public EscalatorPanel(final Listener listener, Flyweight flyweight) {
211         this.listener = listener;
212         this.flyweight = flyweight;
213 
214         final TouchDelegatent/widget/touch/TouchDelegate.html#TouchDelegate">TouchDelegate touchDelegate = new TouchDelegate(this);
215         touchDelegate.addTouchHandler(new MagnoliaPinchRecognizer(touchDelegate, new UIObjectToOffsetProvider(scroller)));
216         MultiTapRecognizerizer/tap/MultiTapRecognizer.html#MultiTapRecognizer">MultiTapRecognizer multitapRecognizer = new MultiTapRecognizer(touchDelegate, 1, 2);
217         touchDelegate.addTouchHandler(multitapRecognizer);
218 
219         addHandler(event -> {
220             /**
221              * TODO: Pinch does not work reliably yet, to be sorted out...
222              */
223             //scale((float) event.getScaleFactor());
224         }, MagnoliaPinchStartEvent.TYPE);
225 
226         addDomHandler(event -> {
227             final NativeEvent nativeEvent = event.getNativeEvent();
228             final Element element = findThumbnail(Element.as(nativeEvent.getEventTarget()));
229             if (element != null) {
230                 boolean isMetaKeyPressed = nativeEvent.getMetaKey();
231                 boolean isShiftPressed = nativeEvent.getShiftKey();
232                 EscalatorPanel.this.listener.onThumbnailClicked(
233                         getThumbnailIndex(element),
234                         isMetaKeyPressed,
235                         isShiftPressed);
236             }
237         }, ClickEvent.getType());
238 
239         addDomHandler(event -> {
240             Element thumbnail = findThumbnail(Element.as(event.getNativeEvent().getEventTarget()));
241             if (thumbnail != null) {
242                 EscalatorPanel.this.listener.onThumbnailRightClicked(
243                         getThumbnailIndex(thumbnail),
244                         event.getNativeEvent().getClientX(),
245                         event.getNativeEvent().getClientY());
246             }
247         }, ContextMenuEvent.getType());
248 
249         addHandler(event -> {
250             int x = event.getTouchStarts().get(0).get(0).getPageX();
251             int y = event.getTouchStarts().get(0).get(0).getPageY();
252             final Element thumbnail = findThumbnail(Util.getElementFromPoint(x, y));
253             if (thumbnail != null) {
254                 EscalatorPanel.this.listener.onThumbnailDoubleClicked(getThumbnailIndex(thumbnail));
255             }
256         }, MultiTapEvent.getType());
257 
258         thumbnailSizeSlider.addValueChangeHandler(event -> scale(event.getValue().floatValue() / 100f));
259 
260         add(thumbnailSizeSlider);
261 
262         this.size = new ThumbnailsSizeKeeper(imageContainer);
263 
264         addStyleName(THUMBNAIL_LAYOUT_STYLE_NAME);
265         imageContainer.addClassName(THUMBNAIL_VIEWPORT_STYLE_NAME);
266 
267         scroller.getElement().appendChild(imageContainer);
268         scroller.addStyleName(THUMBNAIL_SCROLLER_STYLE_NAME);
269         scroller.addScrollHandler(scrollHandler);
270 
271         scroller.getElement().insertFirst(upperSpacer);
272         scroller.getElement().insertAfter(lowerSpacer, imageContainer);
273 
274         add(scroller);
275 
276     }
277 
278     public EscalatorPanel(Listener listener) {
279         this(listener, new Flyweight());
280     }
281 
282     public final int getCurrentThumbnailOffset() {
283         return absoluteOffset;
284     }
285 
286     public int getCurrentlyDisplayedThumbnails() {
287         return imageContainer.getChildCount() - getPlaceholderCount();
288     }
289 
290     public void initialize(int thumbnailAmount, int offset, ThumbnailLayoutState.ThumbnailSize size, float scaleRatio, boolean isFirstUpdateFromState) {
291         this.imageContainer.removeAllChildren();
292 
293         this.thumbnailAmount = thumbnailAmount;
294         this.size.updateAllElementsSize(size.width, size.height);
295         // Set initial scale ratio
296         if (scaleRatio > 0) {
297             this.size.scale(scaleRatio);
298         } else {
299             this.size.scale(0.8f);
300         }
301 
302         // Calculate initial sizes
303         resize();
304 
305         thumbnailSizeSlider.setValue(this.size.getScaleRatio() * 100d, false);
306         scroller.setVerticalScrollPosition((offset / thumbnailsInRow) * size.height);
307     }
308 
309     public void setThumbnailService(ThumbnailService thumbnailService) {
310         this.thumbnailService = thumbnailService;
311     }
312 
313     public void setThumbnailAmount(int amount) {
314         if (this.thumbnailAmount != amount) {
315             this.thumbnailAmount = amount;
316             resize();
317         }
318     }
319 
320     public void setElementSize(int width, int height) {
321         if (size.updateAllElementsSize(width, height)) {
322             resize();
323         }
324     }
325 
326     public Range getDisplayedRange() {
327         return Range.between(absoluteIndex(0), absoluteIndex(getCurrentlyDisplayedThumbnails()));
328     }
329 
330     public void resize() {
331         int initialScrollTop = scroller.getVerticalScrollPosition();
332         int viewportWidth = getScrollableWidth();
333         int scrollerHeight = getScrollableHeight();
334         int thumbnailHeight = size.height();
335 
336         if (scrollerHeight == 0 || viewportWidth == 0 || thumbnailHeight == 0) {
337             return;
338         }
339 
340         int thumbnailWidth = size.width();
341         this.thumbnailsInRow = (int) Math.round((double) viewportWidth / thumbnailWidth);
342         int totalRows = (int) Math.ceil((double) thumbnailAmount / thumbnailsInRow);
343 
344         int scaledWidth = viewportWidth / thumbnailsInRow - 1;
345         size.scaleToWidth(scaledWidth);
346         thumbnailHeight = size.height();
347 
348         this.rowsInViewport = Math.min((int) Math.ceil((double) scrollerHeight / thumbnailHeight) + 1, totalRows);
349 
350         final Range totalRange = Range.between(0, thumbnailAmount);
351 
352         Range offsetRange = Range.between(0, absoluteOffset);
353         Range viewportActualRange = Range.withLength(absoluteOffset, getCurrentlyDisplayedThumbnails());
354         Range remainingRange = totalRange.partitionWith(viewportActualRange)[2];
355 
356         removePlaceholders();
357 
358         boolean rangesAreInOrder = false;
359         /**
360          * Iterate until all calculations are aligned. According to the algorithm specifics there should be never more than two
361          * iterations - the additional one could occur if there is not enough thumbnails in the viewport and we can't fill them
362          * from the lower spacer - then we need to shift the upper spacer and take some thumbnails from there which means the change of
363          * the offset and the upper spacer alignment.
364          */
365         while (!rangesAreInOrder) {
366             /**
367              * Re-calculate the offset first: the upper-spacer must be fully filled with 'virtual' thumbnails in order
368              * to provide the correct value of the offset, so we need to see if the amount of thumbnails currently residing in
369              * the offset range can be divided by the new amount of thumbnails per-row witout remainder. If there is a remainder -
370              * it should be pushed into viewport and the offset should be adjusted.
371              */
372             int offsetRangeRemainder = offsetRange.length() % thumbnailsInRow;
373             if (offsetRangeRemainder != 0) {
374                 addStubs(Range.withLength(relativeIndex(viewportActualRange.getStart()), offsetRangeRemainder));
375                 offsetRange = offsetRange.expand(0, -offsetRangeRemainder);
376                 viewportActualRange = viewportActualRange.expand(offsetRangeRemainder, 0);
377                 absoluteOffset -= offsetRangeRemainder;
378             }
379 
380             /**
381              * Update the viewport. We can be either in situation when there is not enough thumbnails in viewport (when we zoom out or
382              * enlarge the viewport) or there is an overflow of thumbnails (when we zoom in).
383              */
384             Range viewportCalculatedRange = Range.withLength(absoluteOffset, rowsInViewport * thumbnailsInRow);
385 
386             /**
387              * Let's see if the amount of thumbnails we have to display in viewport does not exceed the total amount.
388              * {@code beyondTheTotalRange} variable tracks that overflow if any.
389              */
390             final Range[] calculatedVsTotalRange = viewportCalculatedRange.partitionWith(totalRange);
391             final Range beyondTheTotalRange = calculatedVsTotalRange[2];
392 
393             /**
394              * Let us meanwhile reduce the range of thumbnails to be displayed in viewport to those that are within available range.
395              */
396             viewportCalculatedRange = calculatedVsTotalRange[1];
397 
398             /**
399              * In case we already display too many thumbnails - release the overflow.
400              */
401             if (viewportCalculatedRange.isSubsetOf(viewportActualRange)) {
402 
403                 int overflow = viewportActualRange.partitionWith(viewportCalculatedRange)[2].length();
404                 releaseThumbnails(Range.withLength(getCurrentlyDisplayedThumbnails() - overflow, overflow));
405                 viewportActualRange = viewportActualRange.expand(0, -overflow);
406                 remainingRange = remainingRange.expand(overflow, 0);
407                 /**
408                  * Else in case there're some thumbnails to be added - let's do that.
409                  */
410             } else if (viewportActualRange.isSubsetOf(viewportCalculatedRange)) {
411 
412                 int lack = viewportCalculatedRange.partitionWith(viewportActualRange)[2].length();
413                 addStubs(Range.withLength(getCurrentlyDisplayedThumbnails(), lack));
414                 viewportActualRange = viewportActualRange.expand(0, lack);
415                 remainingRange = remainingRange.expand(-lack, 0);
416             }
417 
418             /**
419              * If the initial calculated range to be displayed goes beyond the total amount of thumbnails:
420              * Fill viewport with the thumbnails that are displayed above by kind of "shifting" the viewport back.
421              */
422             if (!beyondTheTotalRange.isEmpty()) {
423                 int rowsToShift = (int) Math.floor((double) beyondTheTotalRange.length() / thumbnailsInRow);
424                 int toQueryFromOffset = rowsToShift * thumbnailsInRow;
425                 if (toQueryFromOffset > 0) {
426                     absoluteOffset -= toQueryFromOffset;
427                     offsetRange = offsetRange.expand(0, -toQueryFromOffset);
428                     viewportActualRange = viewportActualRange.expand(toQueryFromOffset, 0);
429                     addStubs(Range.between(0, toQueryFromOffset));
430                 }
431             }
432 
433             // Since we might have compensated the lack of thumbnails in viewport from the offset
434             // area - it might get out shape, we'd have to repeat the operation
435             rangesAreInOrder = offsetRange.length() % thumbnailsInRow == 0;
436         }
437 
438         // when the last line contains less thumbnails, fill it with invisible placeholders so that flex stretches it equally
439         int remainder = getCurrentlyDisplayedThumbnails() % thumbnailsInRow;
440         if (remainder > 0) {
441             IntStream.range(0, thumbnailsInRow - remainder).forEach($ ->
442                     imageContainer.insertAfter(createThumbnailPlaceholder(size.width()), imageContainer.getLastChild()));
443         }
444 
445         /**
446          * Update sized according to the calculations above, fix the scroll top based on the relation of the
447          * former upper spacer height, to its new height.
448          */
449         int usH = upperSpacer.getOffsetHeight();
450 
451         int viewportHeight = rowsInViewport * thumbnailHeight;
452         int upperSpacerHeight = (int) Math.ceil(offsetRange.length() / thumbnailsInRow) * thumbnailHeight;
453         int lowerSpacerHeight = (int) Math.ceil(remainingRange.length() / thumbnailsInRow) * thumbnailHeight;
454 
455         // Set the sizes finally
456         scroller.setHeight(scrollerHeight + "px");
457         scroller.getElement().getStyle().setOverflowX(Style.Overflow.HIDDEN);
458 
459         imageContainer.getStyle().setWidth(viewportWidth, Style.Unit.PX);
460         imageContainer.getStyle().setHeight(viewportHeight, Style.Unit.PX);
461 
462         setSpacersHeight(upperSpacerHeight, lowerSpacerHeight);
463 
464         /**
465          * Try to put the scroll top to where it used to be approximately.
466          */
467         isScrollProcessorLocked = true;
468         scroller.setVerticalScrollPosition(initialScrollTop + upperSpacerHeight - usH);
469         Scheduler.get().scheduleFinally(() -> {
470             isScrollProcessorLocked = false;
471             return false;
472         });
473 
474         updateViewport();
475     }
476 
477     protected Element findThumbnail(Element element) {
478         if (element != imageContainer && element != null && imageContainer.isOrHasChild(element)) {
479             Element result = element;
480             while (result.getParentElement() != imageContainer) {
481                 result = result.getParentElement();
482             }
483             return result;
484         }
485         return null;
486     }
487 
488     public void updateImageCaption(String caption, int index) {
489         flyweight.setCaption(caption, Element.as(getImageContainer().getChild(relativeIndex(index))));
490     }
491 
492     protected int getThumbnailIndex(Element element) {
493         return absoluteIndex(Util.getChildElementIndex(element));
494     }
495 
496     protected void scale(float ratio) {
497         thumbnailService.onThumbnailsScaled(ratio);
498         size.scale(ratio);
499         resize();
500     }
501 
502     private void addStubs(Range relativeRange) {
503         int currentThumbnailOffset = getCurrentThumbnailOffset();
504         final Range actualRange = relativeRange.expand(-currentThumbnailOffset, currentThumbnailOffset).restrictTo(Range.between(0, thumbnailAmount));
505 
506         int relativeStartIndex = relativeIndex(actualRange.getStart());
507 
508         Element previousElement = relativeStartIndex == 0 ? null : Element.as(imageContainer.getChild(relativeStartIndex - 1));
509         for (int i = 0; i < actualRange.length(); ++i) {
510             final Element stub = floatingThumbnails.isEmpty() ? flyweight.createThumbnail() : floatingThumbnails.pop();
511             size.applySizeToElement(stub);
512             if (previousElement == null) {
513                 imageContainer.insertFirst(stub);
514             } else {
515                 imageContainer.insertAfter(stub, previousElement);
516             }
517             previousElement = stub;
518         }
519     }
520 
521     private void updateViewport() {
522         final Range visibleRange = Range.between(absoluteIndex(0), absoluteIndex(getCurrentlyDisplayedThumbnails()));
523         if (areAllVisibleThumbnailsCleared()) {
524             final Range availableRange = Range.between(0, thumbnailAmount);
525             if (!visibleRange.isSubsetOf(availableRange)) {
526                 Range thumbnailsToRelease = visibleRange.partitionWith(availableRange)[2];
527                 if (!thumbnailsToRelease.isEmpty()) {
528                     for (int i = 0; i < thumbnailsToRelease.length(); ++i) {
529                         clearThumbnail(Element.as(imageContainer.getLastChild()));
530                     }
531                 }
532             }
533         }
534         this.thumbnailService.onViewportChanged(Range.between(absoluteIndex(0), absoluteIndex(getCurrentlyDisplayedThumbnails())));
535     }
536 
537     public void updateImageSource(String url, int index) {
538         flyweight.setImageSrc(url, Element.as(imageContainer.getChild(relativeIndex(index))));
539     }
540 
541     public int relativeIndex(int index) {
542         return index - getCurrentThumbnailOffset();
543     }
544 
545     private int absoluteIndex(int relativeIndex) {
546         return getCurrentThumbnailOffset() + relativeIndex;
547     }
548 
549     private void releaseThumbnailRow(boolean inFront) {
550         for (int i = 0; i < thumbnailsInRow; ++i) {
551             final Element thumbnail = (Element) (inFront ? imageContainer.getFirstChild() : imageContainer.getLastChild());
552             clearThumbnail(thumbnail);
553         }
554     }
555 
556     private void releaseThumbnails(Range between) {
557         final Set<Element> toRelease = new HashSet<>();
558         for (int i = between.getStart(); i < between.getEnd(); ++i) {
559             toRelease.add(Element.as(imageContainer.getChild(i)));
560         }
561 
562         for (final Element thumbnail : toRelease) {
563             clearThumbnail(thumbnail);
564         }
565     }
566 
567     private Element createThumbnailPlaceholder(int width) {
568         final DivElement thumbnail = DivElement.as(DOM.createDiv());
569         thumbnail.addClassName(THUMBNAIL_STYLE_NAME);
570         thumbnail.addClassName(THUMBNAIL_PLACEHOLDER_STYLE_NAME);
571         thumbnail.getStyle().setWidth(width, Style.Unit.PX);
572         thumbnail.getStyle().setHeight(0, Style.Unit.PX);
573         thumbnail.getStyle().setDisplay(Style.Display.INLINE_BLOCK);
574         thumbnail.getStyle().setVisibility(Style.Visibility.HIDDEN);
575 
576         return thumbnail;
577     }
578 
579     private void clearThumbnail(Element thumbnail) {
580         if (!thumbnail.hasClassName(THUMBNAIL_PLACEHOLDER_STYLE_NAME)) {
581             flyweight.clear(thumbnail);
582             floatingThumbnails.push(thumbnail);
583         }
584         thumbnail.removeFromParent();
585     }
586 
587     public void updateIconFontStyle(String style, int index) {
588         flyweight.setIconFontStyle(style, Element.as(imageContainer.getChild(relativeIndex(index))));
589     }
590 
591     public void setSelectedThumbnailsViaIndices(List<Integer> indices) {
592         clearSelection();
593         for (int absIndex : indices) {
594             Element.as(imageContainer.getChild(relativeIndex(absIndex))).addClassName("selected");
595         }
596     }
597 
598     private void setSpacersHeight(int usHeight, int lsHeight) {
599         upperSpacer.getStyle().setHeight(Math.max(usHeight, 0), Style.Unit.PX);
600         lowerSpacer.getStyle().setHeight(Math.max(lsHeight, 0), Style.Unit.PX);
601     }
602 
603     private void clearSelection() {
604         final JQueryWrapper selectedThumbnails = JQueryWrapper.select(DOM.asOld(imageContainer)).find(".selected");
605         if (selectedThumbnails != null) {
606             selectedThumbnails.removeClass("selected");
607         }
608     }
609 
610     private void removePlaceholders() {
611         if (imageContainer.getChildCount() > 0) {
612             while (imageContainer.getLastChild().getChildCount() == 0) {
613                 clearThumbnail((Element) imageContainer.getLastChild());
614             }
615         }
616     }
617 
618     private boolean areAllVisibleThumbnailsCleared() {
619         return getCurrentlyDisplayedThumbnails() == JQueryWrapper.select(DOM.asOld(imageContainer)).find(".cleared").size();
620     }
621 
622     private int getPlaceholderCount() {
623         return JQueryWrapper.select(DOM.asOld(imageContainer)).find("." + THUMBNAIL_PLACEHOLDER_STYLE_NAME).size();
624     }
625 
626     private int getScrollableHeight() {
627         return getElement().getOffsetHeight() - thumbnailSizeSlider.getOffsetHeight();
628     }
629 
630     private int getScrollableWidth() {
631         return getOffsetWidth();
632     }
633 
634     /**
635      * Describes the panel's children.
636      */
637     static class Flyweight {
638 
639         static final String THUMBNAIL_STYLE_NAME = "thumbnail";
640         static final String THUMBNAIL_IMAGE_STYLE_NAME = "thumbnail-image";
641         public static final String CLEARED_STYLE_NAME = "cleared";
642 
643         Element createThumbnail() {
644             final DivElement thumbnail = DivElement.as(DOM.createDiv());
645             thumbnail.addClassName(THUMBNAIL_STYLE_NAME);
646             thumbnail.getStyle().setDisplay(Style.Display.INLINE_BLOCK);
647 
648             final SpanElement caption = SpanElement.as(DOM.createSpan());
649             caption.getStyle().setDisplay(Style.Display.NONE);
650 
651             final ImageElement image = ImageElement.as(DOM.createImg());
652             image.addClassName(THUMBNAIL_IMAGE_STYLE_NAME);
653             image.getStyle().setDisplay(Style.Display.NONE);
654 
655             thumbnail.appendChild(image);
656             thumbnail.appendChild(caption);
657 
658             return thumbnail;
659         }
660 
661         void setImageSrc(String src, Element thumbnail) {
662             final Element img = getImage(thumbnail);
663             final Element caption = getCaption(thumbnail);
664 
665             img.getStyle().setDisplay(Style.Display.BLOCK);
666             caption.getStyle().setDisplay(Style.Display.BLOCK);
667 
668             img.setAttribute("src", src);
669             thumbnail.removeClassName(CLEARED_STYLE_NAME);
670         }
671 
672         void setIconFontStyle(String style, Element thumbnail) {
673             // no-op
674         }
675 
676         private Element getCaption(Element thumbnail) {
677             return Element.as(thumbnail.getChild(1));
678         }
679 
680         Element getImage(Element thumbnail) {
681             return Element.as(thumbnail.getChild(0));
682         }
683 
684         public void clear(Element thumbnail) {
685             Element caption = getCaption(thumbnail);
686             caption.setInnerText("");
687             caption.getStyle().setDisplay(Style.Display.NONE);
688 
689             Element image = getImage(thumbnail);
690             image.removeAttribute("src");
691             image.getStyle().setDisplay(Style.Display.NONE);
692 
693             thumbnail.addClassName(CLEARED_STYLE_NAME);
694         }
695 
696         void setCaption(String newCaption, Element thumbnail) {
697             final Element caption = getCaption(thumbnail);
698             caption.setInnerHTML(newCaption);
699             caption.getStyle().setDisplay(Style.Display.BLOCK); // ?
700 
701             final Element img = getImage(thumbnail);
702             img.getStyle().setDisplay(Style.Display.BLOCK); // ?
703 
704             thumbnail.removeClassName(CLEARED_STYLE_NAME);
705         }
706     }
707 
708     public DivElement getImageContainer() {
709         return imageContainer;
710     }
711 }