1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
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.NativeEvent;
54 import com.google.gwt.dom.client.Style;
55 import com.google.gwt.event.dom.client.ClickEvent;
56 import com.google.gwt.event.dom.client.ContextMenuEvent;
57 import com.google.gwt.event.dom.client.ScrollEvent;
58 import com.google.gwt.event.dom.client.ScrollHandler;
59 import com.google.gwt.user.client.DOM;
60 import com.google.gwt.user.client.ui.FlowPanel;
61 import com.google.gwt.user.client.ui.ScrollPanel;
62 import com.googlecode.mgwt.dom.client.recognizer.pinch.UIObjectToOffsetProvider;
63 import com.googlecode.mgwt.dom.client.recognizer.tap.MultiTapEvent;
64 import com.googlecode.mgwt.dom.client.recognizer.tap.MultiTapRecognizer;
65 import com.googlecode.mgwt.ui.client.widget.touch.TouchDelegate;
66 import com.vaadin.client.Util;
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83 public abstract class EscalatorPanel extends FlowPanel {
84
85 private static final String THUMBNAIL_LAYOUT_STYLE_NAME = "thumbnail-layout";
86 private static final String THUMBNAIL_SCROLLER_STYLE_NAME = "thumbnail-scroller";
87 private static final String THUMBNAIL_VIEWPORT_STYLE_NAME = "thumbnail-viewport";
88 private static final String THUMBNAIL_PLACEHOLDER_STYLE_NAME = "placeholder";
89 private static final String THUMBNAIL_STYLE_NAME = "thumbnail";
90
91
92
93
94 public interface Listener {
95
96 void onThumbnailClicked(int index, boolean isMetaKeyPressed, boolean isShiftKeyPressed);
97
98 void onThumbnailRightClicked(int index, int xPos, int yPos);
99
100 void onThumbnailDoubleClicked(int index);
101
102 }
103
104 private Flyweight flyweight;
105
106 private final ScrollPanel scroller = new ScrollPanel();
107
108 private final DivElement imageContainer = DivElement.as(DOM.createDiv());
109
110 private final DivElement upperSpacer = DivElement.as(DOM.createDiv());
111
112 private final DivElement lowerSpacer = DivElement.as(DOM.createDiv());
113
114 private ThumbnailService thumbnailService;
115
116 private Deque<Element> floatingThumbnails = new LinkedList<>();
117
118 private int absoluteOffset = 0;
119
120 private int thumbnailAmount;
121
122 private int thumbnailsInRow;
123
124 private int rowsInViewport;
125
126 private ThumbnailsSizeKeeper size;
127
128 private boolean isScrollProcessorLocked = false;
129
130 private Listener listener;
131
132 private final Slider thumbnailSizeSlider = new Slider();
133
134 private final ScrollHandler scrollHandler = new ScrollHandler() {
135
136 private int lastScrollTop = 0;
137
138 @Override
139 public void onScroll(ScrollEvent event) {
140 if (isScrollProcessorLocked) {
141 return;
142 }
143 int newScrollTop = event.getRelativeElement().getScrollTop();
144 int delta = lastScrollTop - newScrollTop;
145 escalate(newScrollTop, delta);
146 lastScrollTop = newScrollTop;
147 }
148 };
149
150
151
152
153
154 private void escalate(int newScrollTop, int delta) {
155 int lsHeight = lowerSpacer.getOffsetHeight();
156 int usHeight = upperSpacer.getOffsetHeight();
157
158 int thumbnailHeight = size.height();
159
160 boolean moveOccurred = false;
161
162 if (delta != 0) {
163
164
165
166
167 if (delta < 0) {
168 while (newScrollTop - usHeight >= thumbnailHeight && lsHeight > 0) {
169 moveOccurred = true;
170
171 usHeight += thumbnailHeight;
172 lsHeight -= thumbnailHeight;
173
174 this.absoluteOffset = usHeight / size.height() * thumbnailsInRow;
175
176
177 if (!areAllVisibleThumbnailsCleared()) {
178 releaseThumbnailRow(true);
179 addStubs(Range.withLength(getCurrentlyDisplayedThumbnails(), thumbnailsInRow));
180 }
181 }
182 } else {
183
184
185
186 while (usHeight - newScrollTop > 0 && usHeight > 0) {
187 moveOccurred = true;
188
189 usHeight -= thumbnailHeight;
190 lsHeight += thumbnailHeight;
191
192 this.absoluteOffset = usHeight / size.height() * thumbnailsInRow;
193
194
195 if (!areAllVisibleThumbnailsCleared()) {
196 releaseThumbnailRow(false);
197 addStubs(Range.between(0, thumbnailsInRow));
198 }
199 }
200 }
201 }
202
203 if (moveOccurred) {
204 setSpacersHeight(usHeight, lsHeight);
205 resize();
206 }
207 }
208
209 public EscalatorPanel(final Listener listener, Flyweight flyweight) {
210 this.listener = listener;
211
212 final TouchDelegate touchDelegate = new TouchDelegate(this);
213 touchDelegate.addTouchHandler(new MagnoliaPinchRecognizer(touchDelegate, new UIObjectToOffsetProvider(scroller)));
214 MultiTapRecognizer multitapRecognizer = new MultiTapRecognizer(touchDelegate, 1, 2);
215 touchDelegate.addTouchHandler(multitapRecognizer);
216
217 addHandler(event -> {
218
219
220
221
222 }, MagnoliaPinchStartEvent.TYPE);
223
224 addDomHandler(event -> {
225 final NativeEvent nativeEvent = event.getNativeEvent();
226 final Element element = findThumbnail(Element.as(nativeEvent.getEventTarget()));
227 if (element != null) {
228 boolean isMetaKeyPressed = nativeEvent.getMetaKey();
229 boolean isShiftPressed = nativeEvent.getShiftKey();
230 EscalatorPanel.this.listener.onThumbnailClicked(
231 getThumbnailIndex(element),
232 isMetaKeyPressed,
233 isShiftPressed);
234 }
235 }, ClickEvent.getType());
236
237 addDomHandler(event -> {
238 Element thumbnail = findThumbnail(Element.as(event.getNativeEvent().getEventTarget()));
239 if (thumbnail != null) {
240 EscalatorPanel.this.listener.onThumbnailRightClicked(
241 getThumbnailIndex(thumbnail),
242 event.getNativeEvent().getClientX(),
243 event.getNativeEvent().getClientY());
244 }
245 }, ContextMenuEvent.getType());
246
247 addHandler(event -> {
248 int x = event.getTouchStarts().get(0).get(0).getPageX();
249 int y = event.getTouchStarts().get(0).get(0).getPageY();
250 final Element thumbnail = findThumbnail(Util.getElementFromPoint(x, y));
251 if (thumbnail != null) {
252 EscalatorPanel.this.listener.onThumbnailDoubleClicked(getThumbnailIndex(thumbnail));
253 }
254 }, MultiTapEvent.getType());
255
256 thumbnailSizeSlider.addValueChangeHandler(event -> scale(event.getValue().floatValue() / 100f));
257
258 add(thumbnailSizeSlider);
259
260 this.size = new ThumbnailsSizeKeeper(imageContainer);
261
262 addStyleName(THUMBNAIL_LAYOUT_STYLE_NAME);
263 imageContainer.addClassName(THUMBNAIL_VIEWPORT_STYLE_NAME);
264
265 scroller.getElement().appendChild(imageContainer);
266 scroller.addStyleName(THUMBNAIL_SCROLLER_STYLE_NAME);
267 scroller.addScrollHandler(scrollHandler);
268
269 scroller.getElement().insertFirst(upperSpacer);
270 scroller.getElement().insertAfter(lowerSpacer, imageContainer);
271
272 add(scroller);
273
274 this.flyweight = flyweight;
275 }
276
277 public final int getCurrentThumbnailOffset() {
278 return absoluteOffset;
279 }
280
281 public int getCurrentlyDisplayedThumbnails() {
282 return imageContainer.getChildCount() - getPlaceholderCount();
283 }
284
285 public void initialize(int thumbnailAmount, int offset, ThumbnailLayoutState.ThumbnailSize size, float scaleRatio, boolean isFirstUpdateFromState) {
286 this.imageContainer.removeAllChildren();
287
288 this.thumbnailAmount = thumbnailAmount;
289 this.size.updateAllElementsSize(size.width, size.height);
290
291 if (scaleRatio > 0) {
292 this.size.scale(scaleRatio);
293 } else {
294 this.size.scale(0.8f);
295 }
296
297
298 resize();
299
300 thumbnailSizeSlider.setValue(this.size.getScaleRatio() * 100d, false);
301 scroller.setVerticalScrollPosition((offset / thumbnailsInRow) * size.height);
302 }
303
304 public void setThumbnailService(ThumbnailService thumbnailService) {
305 this.thumbnailService = thumbnailService;
306 }
307
308 public void setThumbnailAmount(int amount) {
309 if (this.thumbnailAmount != amount) {
310 this.thumbnailAmount = amount;
311 resize();
312 }
313 }
314
315 public void setElementSize(int width, int height) {
316 if (size.updateAllElementsSize(width, height)) {
317 resize();
318 }
319 }
320
321 public Range getDisplayedRange() {
322 return Range.between(absoluteIndex(0), absoluteIndex(getCurrentlyDisplayedThumbnails()));
323 }
324
325 public void resize() {
326 int initialScrollTop = scroller.getVerticalScrollPosition();
327 int viewportWidth = getScrollableWidth();
328 int scrollerHeight = getScrollableHeight();
329 int thumbnailHeight = size.height();
330
331 if (scrollerHeight == 0 || viewportWidth == 0 || thumbnailHeight == 0) {
332 return;
333 }
334
335 int thumbnailWidth = size.width();
336 this.thumbnailsInRow = (int) Math.round((double) viewportWidth / thumbnailWidth);
337 int totalRows = (int) Math.ceil((double) thumbnailAmount / thumbnailsInRow);
338
339 int scaledWidth = viewportWidth / thumbnailsInRow - 1;
340 size.scaleToWidth(scaledWidth);
341 thumbnailHeight = size.height();
342
343 this.rowsInViewport = Math.min((int) Math.ceil((double) scrollerHeight / thumbnailHeight) + 1, totalRows);
344
345 final Range totalRange = Range.between(0, thumbnailAmount);
346
347 Range offsetRange = Range.between(0, absoluteOffset);
348 Range viewportActualRange = Range.withLength(absoluteOffset, getCurrentlyDisplayedThumbnails());
349 Range remainingRange = totalRange.partitionWith(viewportActualRange)[2];
350
351 removePlaceholders();
352
353 boolean rangesAreInOrder = false;
354
355
356
357
358
359
360 while (!rangesAreInOrder) {
361
362
363
364
365
366
367 int offsetRangeRemainder = offsetRange.length() % thumbnailsInRow;
368 if (offsetRangeRemainder != 0) {
369 addStubs(Range.withLength(relativeIndex(viewportActualRange.getStart()), offsetRangeRemainder));
370 offsetRange = offsetRange.expand(0, -offsetRangeRemainder);
371 viewportActualRange = viewportActualRange.expand(offsetRangeRemainder, 0);
372 absoluteOffset -= offsetRangeRemainder;
373 }
374
375
376
377
378
379 Range viewportCalculatedRange = Range.withLength(absoluteOffset, rowsInViewport * thumbnailsInRow);
380
381
382
383
384
385 final Range[] calculatedVsTotalRange = viewportCalculatedRange.partitionWith(totalRange);
386 final Range beyondTheTotalRange = calculatedVsTotalRange[2];
387
388
389
390
391 viewportCalculatedRange = calculatedVsTotalRange[1];
392
393
394
395
396 if (viewportCalculatedRange.isSubsetOf(viewportActualRange)) {
397
398 int overflow = viewportActualRange.partitionWith(viewportCalculatedRange)[2].length();
399 releaseThumbnails(Range.withLength(getCurrentlyDisplayedThumbnails() - overflow, overflow));
400 viewportActualRange = viewportActualRange.expand(0, -overflow);
401 remainingRange = remainingRange.expand(overflow, 0);
402
403
404
405 } else if (viewportActualRange.isSubsetOf(viewportCalculatedRange)) {
406
407 int lack = viewportCalculatedRange.partitionWith(viewportActualRange)[2].length();
408 addStubs(Range.withLength(getCurrentlyDisplayedThumbnails(), lack));
409 viewportActualRange = viewportActualRange.expand(0, lack);
410 remainingRange = remainingRange.expand(-lack, 0);
411 }
412
413
414
415
416
417 if (!beyondTheTotalRange.isEmpty()) {
418 int rowsToShift = (int) Math.floor((double) beyondTheTotalRange.length() / thumbnailsInRow);
419 int toQueryFromOffset = rowsToShift * thumbnailsInRow;
420 if (toQueryFromOffset > 0) {
421 absoluteOffset -= toQueryFromOffset;
422 offsetRange = offsetRange.expand(0, -toQueryFromOffset);
423 viewportActualRange = viewportActualRange.expand(toQueryFromOffset, 0);
424 addStubs(Range.between(0, toQueryFromOffset));
425 }
426 }
427
428
429
430 rangesAreInOrder = offsetRange.length() % thumbnailsInRow == 0;
431 }
432
433
434 int remainder = getCurrentlyDisplayedThumbnails() % thumbnailsInRow;
435 if (remainder > 0) {
436 IntStream.range(0, thumbnailsInRow - remainder).forEach($ ->
437 imageContainer.insertAfter(createThumbnailPlaceholder(size.width()), imageContainer.getLastChild()));
438 }
439
440
441
442
443
444 int usH = upperSpacer.getOffsetHeight();
445
446 int viewportHeight = rowsInViewport * thumbnailHeight;
447 int upperSpacerHeight = (int) Math.ceil(offsetRange.length() / thumbnailsInRow) * thumbnailHeight;
448 int lowerSpacerHeight = (int) Math.ceil(remainingRange.length() / thumbnailsInRow) * thumbnailHeight;
449
450
451 scroller.setHeight(scrollerHeight + "px");
452 scroller.getElement().getStyle().setOverflowX(Style.Overflow.HIDDEN);
453
454 imageContainer.getStyle().setWidth(viewportWidth, Style.Unit.PX);
455 imageContainer.getStyle().setHeight(viewportHeight, Style.Unit.PX);
456
457 setSpacersHeight(upperSpacerHeight, lowerSpacerHeight);
458
459
460
461
462 isScrollProcessorLocked = true;
463 scroller.setVerticalScrollPosition(initialScrollTop + upperSpacerHeight - usH);
464 Scheduler.get().scheduleFinally(() -> {
465 isScrollProcessorLocked = false;
466 return false;
467 });
468
469 updateViewport();
470 }
471
472 protected Element findThumbnail(Element element) {
473 if (element != imageContainer && element != null && imageContainer.isOrHasChild(element)) {
474 Element result = element;
475 while (result.getParentElement() != imageContainer) {
476 result = result.getParentElement();
477 }
478 return result;
479 }
480 return null;
481 }
482
483 protected int getThumbnailIndex(Element element) {
484 return absoluteIndex(Util.getChildElementIndex(element));
485 }
486
487 protected void scale(float ratio) {
488 thumbnailService.onThumbnailsScaled(ratio);
489 size.scale(ratio);
490 resize();
491 }
492
493 private void addStubs(Range relativeRange) {
494 int currentThumbnailOffset = getCurrentThumbnailOffset();
495 final Range actualRange = relativeRange.expand(-currentThumbnailOffset, currentThumbnailOffset).restrictTo(Range.between(0, thumbnailAmount));
496
497 int relativeStartIndex = relativeIndex(actualRange.getStart());
498
499 Element previousElement = relativeStartIndex == 0 ? null : Element.as(imageContainer.getChild(relativeStartIndex - 1));
500 for (int i = 0; i < actualRange.length(); ++i) {
501 final Element stub = floatingThumbnails.isEmpty() ? flyweight.createThumbnail() : floatingThumbnails.pop();
502 size.applySizeToElement(stub);
503 if (previousElement == null) {
504 imageContainer.insertFirst(stub);
505 } else {
506 imageContainer.insertAfter(stub, previousElement);
507 }
508 previousElement = stub;
509 }
510 }
511
512 private void updateViewport() {
513 final Range visibleRange = Range.between(absoluteIndex(0), absoluteIndex(getCurrentlyDisplayedThumbnails()));
514 if (areAllVisibleThumbnailsCleared()) {
515 final Range availableRange = Range.between(0, thumbnailAmount);
516 if (!visibleRange.isSubsetOf(availableRange)) {
517 Range thumbnailsToRelease = visibleRange.partitionWith(availableRange)[2];
518 if (!thumbnailsToRelease.isEmpty()) {
519 for (int i = 0; i < thumbnailsToRelease.length(); ++i) {
520 clearThumbnail(Element.as(imageContainer.getLastChild()));
521 }
522 }
523 }
524 }
525 this.thumbnailService.onViewportChanged(Range.between(absoluteIndex(0), absoluteIndex(getCurrentlyDisplayedThumbnails())));
526 }
527
528 public void updateImageSource(String url, int index) {
529 flyweight.setImageSrc(url, Element.as(imageContainer.getChild(relativeIndex(index))));
530 }
531
532 public int relativeIndex(int index) {
533 return index - getCurrentThumbnailOffset();
534 }
535
536 private int absoluteIndex(int relativeIndex) {
537 return getCurrentThumbnailOffset() + relativeIndex;
538 }
539
540 private void releaseThumbnailRow(boolean inFront) {
541 for (int i = 0; i < thumbnailsInRow; ++i) {
542 final Element thumbnail = (Element) (inFront ? imageContainer.getFirstChild() : imageContainer.getLastChild());
543 clearThumbnail(thumbnail);
544 }
545 }
546
547 private void releaseThumbnails(Range between) {
548 final Set<Element> toRelease = new HashSet<>();
549 for (int i = between.getStart(); i < between.getEnd(); ++i) {
550 toRelease.add(Element.as(imageContainer.getChild(i)));
551 }
552
553 for (final Element thumbnail : toRelease) {
554 clearThumbnail(thumbnail);
555 }
556 }
557
558 private Element createThumbnailPlaceholder(int width) {
559 final DivElement thumbnail = DivElement.as(DOM.createDiv());
560 thumbnail.addClassName(THUMBNAIL_STYLE_NAME);
561 thumbnail.addClassName(THUMBNAIL_PLACEHOLDER_STYLE_NAME);
562 thumbnail.getStyle().setWidth(width, Style.Unit.PX);
563 thumbnail.getStyle().setHeight(0, Style.Unit.PX);
564 thumbnail.getStyle().setDisplay(Style.Display.INLINE_BLOCK);
565 thumbnail.getStyle().setVisibility(Style.Visibility.HIDDEN);
566
567 return thumbnail;
568 }
569
570 private void clearThumbnail(Element thumbnail) {
571 if (!thumbnail.hasClassName(THUMBNAIL_PLACEHOLDER_STYLE_NAME)) {
572 flyweight.clear(thumbnail);
573 floatingThumbnails.push(thumbnail);
574 }
575 thumbnail.removeFromParent();
576 }
577
578 public void updateIconFontStyle(String style, int index) {
579 flyweight.setIconFontStyle(style, Element.as(imageContainer.getChild(relativeIndex(index))));
580 }
581
582 public void setSelectedThumbnailsViaIndices(List<Integer> indices) {
583 clearSelection();
584 for (int absIndex : indices) {
585 Element.as(imageContainer.getChild(relativeIndex(absIndex))).addClassName("selected");
586 }
587 }
588
589 private void setSpacersHeight(int usHeight, int lsHeight) {
590 upperSpacer.getStyle().setHeight(Math.max(usHeight, 0), Style.Unit.PX);
591 lowerSpacer.getStyle().setHeight(Math.max(lsHeight, 0), Style.Unit.PX);
592 }
593
594 private void clearSelection() {
595 final JQueryWrapper selectedThumbnails = JQueryWrapper.select(DOM.asOld(imageContainer)).find(".selected");
596 if (selectedThumbnails != null) {
597 selectedThumbnails.removeClass("selected");
598 }
599 }
600
601 private void removePlaceholders() {
602 if (imageContainer.getChildCount() > 0) {
603 while (imageContainer.getLastChild().getChildCount() == 0) {
604 clearThumbnail((Element) imageContainer.getLastChild());
605 }
606 }
607 }
608
609 private boolean areAllVisibleThumbnailsCleared() {
610 return getCurrentlyDisplayedThumbnails() == JQueryWrapper.select(DOM.asOld(imageContainer)).find(".cleared").size();
611 }
612
613 private int getPlaceholderCount() {
614 return JQueryWrapper.select(DOM.asOld(imageContainer)).find("." + THUMBNAIL_PLACEHOLDER_STYLE_NAME).size();
615 }
616
617 private int getScrollableHeight() {
618 return getElement().getOffsetHeight() - thumbnailSizeSlider.getOffsetHeight();
619 }
620
621 private int getScrollableWidth() {
622 return getOffsetWidth();
623 }
624
625
626
627
628 static abstract class Flyweight {
629
630 abstract Element createThumbnail();
631
632 abstract void setImageSrc(String src, Element thumbnail);
633
634 abstract void setIconFontStyle(String style, Element thumbnail);
635
636 abstract Element getImage(Element thumbnail);
637
638 public abstract void clear(Element thumbnail);
639
640 abstract void setCaption(String newCaption, Element thumbnail);
641 }
642
643 public DivElement getImageContainer() {
644 return imageContainer;
645 }
646 }