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.layout;
35
36 import static java.util.stream.Collectors.toList;
37
38 import info.magnolia.ui.vaadin.gwt.client.layout.lazylayout.connector.ThumbnailLayoutState;
39 import info.magnolia.ui.vaadin.gwt.client.layout.lazylayout.rpc.LazyLayoutClientRpc;
40 import info.magnolia.ui.vaadin.gwt.client.layout.lazylayout.rpc.LazyLayoutServerRpc;
41 import info.magnolia.ui.vaadin.gwt.client.layout.lazylayout.shared.ThumbnailData;
42 import info.magnolia.ui.vaadin.gwt.shared.Range;
43
44 import java.io.Serializable;
45 import java.util.ArrayList;
46 import java.util.Collection;
47 import java.util.HashSet;
48 import java.util.List;
49 import java.util.Set;
50
51 import org.slf4j.Logger;
52 import org.slf4j.LoggerFactory;
53
54 import com.google.common.collect.BiMap;
55 import com.google.common.collect.HashBiMap;
56 import com.vaadin.ui.AbstractComponent;
57 import com.vaadin.ui.IconGenerator;
58 import com.vaadin.ui.ItemCaptionGenerator;
59
60
61
62
63
64
65
66 public abstract class LazyLayout<T> extends AbstractComponent {
67
68 private static Logger log = LoggerFactory.getLogger(LazyThumbnailLayout.class);
69
70 private final List<ElementSelectionListener<T>> selectionListeners = new ArrayList<>();
71
72 private final List<ElementDblClickListener<T>> dblClickListeners = new ArrayList<>();
73
74 private final List<ElementRightClickListener<T>> rightClickListeners = new ArrayList<>();
75
76 private final Set<T> selectedItems = new HashSet<>();
77
78 private DataProviderKeyMapper mapper = new DataProviderKeyMapper();
79
80 public LazyLayout() {
81 LazyLayoutServerRpc serverRpc = getRpc(mapper);
82 registerRpc(serverRpc);
83 }
84
85 void handleSelectionAtIndex(int index, boolean isMultiple) {
86 if (isMultiple) {
87 getState().selection.toggleMultiSelection(index);
88 } else {
89 getState().selection.toggleSelection(index);
90 }
91 updateSelectedItems();
92 }
93
94 void fireSelectionChange() {
95 this.onElementsSelected(selectedItems);
96 }
97
98 private void updateSelectedItems() {
99 selectedItems.clear();
100 selectedItems.addAll(getState().selection.selectedIndices.stream()
101 .map(selectedItemIndex -> fetchItems(Range.between(0, Integer.MAX_VALUE)).get(selectedItemIndex)).collect(toList()));
102 }
103
104 protected abstract ItemCaptionGenerator<T> getItemCaptionGenerator();
105
106 protected abstract IconGenerator<T> getItemResourceGenerator();
107
108 protected abstract List<T> fetchItems(Range added);
109
110 private LazyLayoutServerRpc getRpc(DataProviderKeyMapper mapper) {
111 return new LazyLayoutServerRpcImpl(mapper, getRpc(), this);
112 }
113
114 public LazyLayoutClientRpc getRpc() {
115 return getRpcProxy(LazyLayoutClientRpc.class);
116 }
117
118 void onElementDoubleClicked(T item) {
119 for (final ElementDblClickListener<T> listener : dblClickListeners) {
120 listener.onElementDblClicked(item);
121 }
122 }
123
124 void onElementRightClicked(T item, int clickX, int clickY) {
125 for (final ElementRightClickListener<T> listener : rightClickListeners) {
126 listener.onElementRightClicked(item, clickX, clickY);
127 }
128 }
129
130 private void onElementsSelected(Set<T> items) {
131 for (final ElementSelectionListener<T> listener : selectionListeners) {
132 listener.onElementsSelected(items);
133 }
134 }
135
136 List<ThumbnailData> fetchElements(Range range) {
137 final List<ThumbnailData> elements = new ArrayList<>(range.length());
138 for (int i = range.getStart(); i < range.getEnd(); ++i) {
139 final T item = mapper.itemAtIndex(i);
140 String thumbnailId = mapper.getKey(item);
141 setResource(thumbnailId, getItemResourceGenerator().apply(item));
142 ThumbnailDataut/lazylayout/shared/ThumbnailData.html#ThumbnailData">ThumbnailData thumbnailData = new ThumbnailData(thumbnailId, true);
143 thumbnailData.setCaption(getItemCaptionGenerator().apply(item));
144 elements.add(thumbnailData);
145 }
146 return elements;
147 }
148
149 private void setElementAmount(int elementAmount) {
150 getState().elementAmount = Math.max(elementAmount, 0);
151 }
152
153 public void setElementSize(int width, int height) {
154 getState().size.height = height;
155 getState().size.width = width;
156 }
157
158 public void refresh() {
159 if (getState(false).elementAmount > 0) {
160 getState().resources.clear();
161 mapper.clearAll();
162 }
163
164 int itemCount = size();
165 setElementAmount(itemCount);
166 if (getState().offset > itemCount) {
167 getState().offset = 0;
168 }
169
170 updateSelectionIndices();
171 fireSelectionChange();
172 getRpc().refresh();
173 }
174
175 public abstract int size();
176
177 public void addElementSelectionListener(final ElementSelectionListener<T> listener) {
178 if (listener == null) {
179 throw new IllegalArgumentException("Selection listener cannot be null!");
180 }
181 this.selectionListeners.add(listener);
182 }
183
184 public void addDoubleClickListener(final ElementDblClickListener<T> listener) {
185 if (listener == null) {
186 throw new IllegalArgumentException("Double click listener cannot be null!");
187 }
188 this.dblClickListeners.add(listener);
189 }
190
191 public void addRightClickListener(final ElementRightClickListener<T> listener) {
192 if (listener == null) {
193 throw new IllegalArgumentException("Right click listener cannot be null!");
194 }
195 this.rightClickListeners.add(listener);
196 }
197
198 @Override
199 protected ThumbnailLayoutState getState() {
200 return (ThumbnailLayoutState) super.getState();
201 }
202
203 @Override
204 protected ThumbnailLayoutState getState(boolean markAsDirty) {
205 return (ThumbnailLayoutState) super.getState(markAsDirty);
206 }
207
208 public void setSelectedItem(T selectedItem) {
209 if (selectedItem == null) {
210 this.getState().selection.selectedIndices.clear();
211 } else {
212 this.getState().selection.toggleSelection(-1);
213 this.selectedItems.clear();
214 this.selectedItems.add(selectedItem);
215 }
216
217 updateSelectionIndices();
218 }
219
220 @Override
221 public void beforeClientResponse(boolean initial) {
222 super.beforeClientResponse(initial);
223
224 getState().isFirstUpdate &= initial;
225 }
226
227 protected void updateSelectionIndices() {
228 this.getState().selection.selectedIndices.clear();
229 if (!this.selectedItems.isEmpty()) {
230 this.selectedItems.stream().filter(mapper.itemToKey.keySet()::contains).forEach(item -> {
231 this.getState().selection.toggleMultiSelection(this.mapper.indexOf(item));
232 });
233 }
234 }
235
236
237
238
239
240
241 public class DataProviderKeyMapper implements Serializable {
242
243 private final BiMap<Integer, T> indexToItem = HashBiMap.create();
244
245 private final BiMap<T, String> itemToKey = HashBiMap.create();
246
247 private Range activeRange = Range.withLength(0, 0);
248
249 private long rollingIndex = 0;
250
251 private DataProviderKeyMapper() {
252 }
253
254 void setActiveRange(Range newActiveRange) {
255
256 final Range[] removed = activeRange.partitionWith(newActiveRange);
257 final Range[] added = newActiveRange.partitionWith(activeRange);
258
259 removeActiveElements(removed[0]);
260 removeActiveElements(removed[2]);
261 addActiveElements(added[0]);
262 addActiveElements(added[2]);
263
264 log.debug("Former active: {}, New Active: {}, idx-id: {}, id-key: {}. Removed: {} and {}, Added: {} and {}",
265 activeRange,
266 newActiveRange,
267 indexToItem.size(),
268 itemToKey.size(),
269 removed[0],
270 removed[2],
271 added[0],
272 added[2]);
273
274 activeRange = newActiveRange;
275 }
276
277 private void removeActiveElements(final Range deprecated) {
278 for (int i = deprecated.getStart(); i < deprecated.getEnd(); i++) {
279 final T item = indexToItem.get(i);
280
281 itemToKey.remove(item);
282 indexToItem.remove(i);
283 }
284 }
285
286 private void addActiveElements(Range added) {
287 if (added.isEmpty()) {
288 return;
289 }
290
291 List<T> newItems = fetchItems(added);
292 Integer index = added.getStart();
293 for (T items : newItems) {
294 if (!indexToItem.containsKey(index)) {
295 if (!itemToKey.containsKey(items)) {
296 itemToKey.put(items, nextKey());
297 }
298
299 indexToItem.forcePut(index, items);
300 }
301 index++;
302 }
303 }
304
305 private String nextKey() {
306 return String.valueOf(rollingIndex++);
307 }
308
309 public String getKey(T item) {
310 String key = itemToKey.get(item);
311 if (key == null) {
312 key = nextKey();
313 itemToKey.put(item, key);
314 }
315 return key;
316 }
317
318 public T getItem(String key) throws IllegalStateException {
319 T item = itemToKey.inverse().get(key);
320 if (item != null) {
321 return item;
322 } else {
323 throw new IllegalStateException("No item for key " + key + " found.");
324 }
325 }
326
327 public Collection<T> getItems(Collection<String> keys)
328 throws IllegalStateException {
329 if (keys == null) {
330 throw new IllegalArgumentException("keys may not be null");
331 }
332
333 final List<T> items = new ArrayList<>(keys.size());
334 for (String key : keys) {
335 items.add(getItem(key));
336 }
337 return items;
338 }
339
340 T itemAtIndex(int index) {
341 return indexToItem.get(index);
342 }
343
344 int indexOf(T item) {
345 return indexToItem.inverse().get(item);
346 }
347
348 void clearAll() {
349 indexToItem.clear();
350 itemToKey.clear();
351 rollingIndex = 0;
352 activeRange = Range.withLength(0, 0);
353 }
354 }
355
356
357
358
359
360 public interface ElementSelectionListener<T> {
361 void onElementsSelected(Set<T> items);
362 }
363
364
365
366
367
368 public interface ElementDblClickListener<T> {
369 void onElementDblClicked(T item);
370 }
371
372
373
374
375
376 public interface ElementRightClickListener<T> {
377 void onElementRightClicked(T item, int clickX, int clickY);
378 }
379 }