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 info.magnolia.ui.vaadin.gwt.client.layout.lazylayout.connector.ThumbnailLayoutState;
37 import info.magnolia.ui.vaadin.gwt.client.layout.lazylayout.rpc.LazyLayoutServerRpc;
38 import info.magnolia.ui.vaadin.gwt.client.layout.lazylayout.rpc.LazyLayoutClientRpc;
39 import info.magnolia.ui.vaadin.gwt.client.layout.lazylayout.shared.ThumbnailData;
40 import info.magnolia.ui.vaadin.gwt.shared.Range;
41 import info.magnolia.ui.vaadin.layout.data.PagingThumbnailContainer;
42 import info.magnolia.ui.vaadin.layout.data.ThumbnailContainer;
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.google.common.collect.Lists;
57 import com.vaadin.ui.AbstractComponent;
58 import com.vaadin.v7.data.Container;
59 import com.vaadin.v7.data.Container.Ordered;
60
61
62
63
64
65
66 public abstract class LazyLayout extends AbstractComponent implements Container.Viewer, Container.ItemSetChangeListener {
67
68 private static Logger log = LoggerFactory.getLogger(LazyLayout.class);
69
70 private final List<ElementSelectionListener> selectionListeners = new ArrayList<>();
71
72 private final List<ElementDblClickListener> dblClickListeners = new ArrayList<>();
73
74 private final List<ElementRightClickListener> rightClickListeners = new ArrayList<>();
75
76 private final Set<Object> selectedIds = new HashSet<>();
77
78 private DataProviderKeyMapper mapper = new DataProviderKeyMapper();
79
80 private ThumbnailContainer container;
81
82 private LazyLayoutClientRpc clientRpc;
83
84 private LazyLayoutServerRpc serverRpc;
85
86 public void handleSelectionAtIndex(int index, boolean isMultiple) {
87 if (isMultiple) {
88 getState().selection.toggleMultiSelection(index);
89 } else {
90 getState().selection.toggleSelection(index);
91 }
92
93 updateSelectedIds();
94 }
95
96 public void fireSelectionChange() {
97 this.onElementsSelected(selectedIds);
98 }
99
100 private void updateSelectedIds() {
101 selectedIds.clear();
102 selectedIds.addAll(Lists.transform(getState().selection.selectedIndices, input -> container.getIdByIndex(input.intValue())));
103 }
104
105 public LazyLayout() {
106 this.serverRpc = getRpc(mapper, clientRpc);
107 registerRpc(serverRpc);
108 this.clientRpc = getRpc();
109 }
110
111 abstract LazyLayoutServerRpc getRpc(DataProviderKeyMapper mapper, LazyLayoutClientRpc clientRpc);
112
113 abstract LazyLayoutClientRpc getRpc();
114
115 public void onElementDoubleClicked(Object itemId) {
116 for (final ElementDblClickListener listener : dblClickListeners) {
117 listener.onElementDblClicked(itemId);
118 }
119 }
120
121 public void onElementRightClicked(Object itemId, int clickX, int clickY) {
122 for (final ElementRightClickListener listener : rightClickListeners) {
123 listener.onElementRightClicked(itemId, clickX, clickY);
124 }
125 }
126
127 private void onElementsSelected(Set<Object> ids) {
128 for (final ElementSelectionListener listener : selectionListeners) {
129 listener.onElementsSelected(ids);
130 }
131 }
132
133 public abstract List<ThumbnailData> fetchElements(Range range, DataProviderKeyMapper mapper);
134
135 private void setElementAmount(int elementAmount) {
136 getState().elementAmount = Math.max(elementAmount, 0);
137 }
138
139 public void setElementSize(int width, int height) {
140 getState().size.height = height;
141 getState().size.width = width;
142 }
143
144 public int getElementWidth() {
145 return getState(false).size.width;
146 }
147
148 public int getElementHeight() {
149 return getState(false).size.height;
150 }
151
152 public void refresh() {
153 if (getState(false).elementAmount > 0) {
154 getState().resources.clear();
155 mapper.clearAll();
156 }
157
158 if (container != null) {
159 setElementAmount(container.size());
160
161 if (getState().offset > container.size()) {
162 getState().offset = 0;
163 }
164 }
165
166 synchroniseSelection();
167 getRpc().refresh();
168 }
169
170 public void addElementSelectionListener(final ElementSelectionListener listener) {
171 if (listener == null) {
172 throw new IllegalArgumentException("Selection listener cannot be null!");
173 }
174 this.selectionListeners.add(listener);
175 }
176
177 public void addDoubleClickListener(final ElementDblClickListener listener) {
178 if (listener == null) {
179 throw new IllegalArgumentException("Double click listener cannot be null!");
180 }
181 this.dblClickListeners.add(listener);
182 }
183
184 public void addRightClickListener(final ElementRightClickListener listener) {
185 if (listener == null) {
186 throw new IllegalArgumentException("Right click listener cannot be null!");
187 }
188 this.rightClickListeners.add(listener);
189 }
190
191 @Override
192 public void setContainerDataSource(Container newDataSource) {
193 if (!(newDataSource instanceof ThumbnailContainer)) {
194 throw new IllegalArgumentException("Container must implement info.magnolia.ui.vaadin.layout.data.ThumbnailContainer...");
195 }
196
197 if (this.container instanceof Container.ItemSetChangeNotifier) {
198 ((Container.ItemSetChangeNotifier) this.container).removeItemSetChangeListener(this);
199 }
200
201 this.container = (ThumbnailContainer) newDataSource;
202
203 if (this.container instanceof Container.ItemSetChangeNotifier) {
204 ((Container.ItemSetChangeNotifier) this.container).addItemSetChangeListener(this);
205 }
206
207 refresh();
208
209 }
210
211 @Override
212 public Ordered getContainerDataSource() {
213 return container;
214 }
215
216 @Override
217 protected ThumbnailLayoutState getState() {
218 return (ThumbnailLayoutState) super.getState();
219 }
220
221 @Override
222 protected ThumbnailLayoutState getState(boolean markAsDirty) {
223 return (ThumbnailLayoutState) super.getState(markAsDirty);
224 }
225
226 public void setSelectedItemId(Object selectedItemId) {
227 if (selectedItemId == null) {
228 this.getState().selection.selectedIndices.clear();
229 } else {
230 this.getState().selection.toggleSelection(-1);
231 this.getState().selection.toggleSelection(container.indexOfId(selectedItemId));
232 updateSelectedIds();
233 }
234 }
235
236 @Override
237 public void containerItemSetChange(Container.ItemSetChangeEvent event) {
238 refresh();
239 }
240
241 @Override
242 public void beforeClientResponse(boolean initial) {
243 super.beforeClientResponse(initial);
244
245 getState().isFirstUpdate &= initial;
246 }
247
248
249
250
251
252 private void synchroniseSelection() {
253 final List<Integer> formerSelectedIndices = getState().selection.selectedIndices;
254 getState().selection.toggleSelection(-1);
255
256 for (Object id : formerSelectedIndices) {
257 if (getContainerDataSource().containsId(id)) {
258 handleSelectionAtIndex(container.indexOfId(id), true);
259 }
260 }
261
262 updateSelectedIds();
263 fireSelectionChange();
264 }
265
266
267
268
269
270
271 public class DataProviderKeyMapper implements Serializable {
272
273 private final BiMap<Integer, Object> indexToItemId = HashBiMap.create();
274
275 private final BiMap<Object, String> itemIdToKey = HashBiMap.create();
276
277 private Range activeRange = Range.withLength(0, 0);
278
279 private long rollingIndex = 0;
280
281 private DataProviderKeyMapper() {
282 }
283
284 void setActiveRange(Range newActiveRange) {
285
286
287
288
289
290 if (container instanceof PagingThumbnailContainer) {
291 ((PagingThumbnailContainer) container).setPageSize(newActiveRange.length());
292 }
293
294 final Range[] removed = activeRange.partitionWith(newActiveRange);
295 final Range[] added = newActiveRange.partitionWith(activeRange);
296
297 removeActiveElements(removed[0]);
298 removeActiveElements(removed[2]);
299 addActiveElements(added[0]);
300 addActiveElements(added[2]);
301
302 log.debug("Former active: {}, New Active: {}, idx-id: {}, id-key: {}. Removed: {} and {}, Added: {} and {}",
303 activeRange,
304 newActiveRange,
305 indexToItemId.size(),
306 itemIdToKey.size(),
307 removed[0],
308 removed[2],
309 added[0],
310 added[2]);
311
312 activeRange = newActiveRange;
313
314 }
315
316 private void removeActiveElements(final Range deprecated) {
317 for (int i = deprecated.getStart(); i < deprecated.getEnd(); i++) {
318 final Object itemId = indexToItemId.get(i);
319
320 itemIdToKey.remove(itemId);
321 indexToItemId.remove(i);
322 }
323 }
324
325 private void addActiveElements(Range added) {
326 if (added.isEmpty()) {
327 return;
328 }
329
330 List<?> newItemIds = container.getItemIds(added.getStart(), added.length());
331 Integer index = added.getStart();
332 for (Object itemId : newItemIds) {
333 if (!indexToItemId.containsKey(index)) {
334 if (!itemIdToKey.containsKey(itemId)) {
335 itemIdToKey.put(itemId, nextKey());
336 }
337
338 indexToItemId.forcePut(index, itemId);
339 }
340 index++;
341 }
342 }
343
344 private String nextKey() {
345 return String.valueOf(rollingIndex++);
346 }
347
348 public String getKey(Object itemId) {
349 String key = itemIdToKey.get(itemId);
350 if (key == null) {
351 key = nextKey();
352 itemIdToKey.put(itemId, key);
353 }
354 return key;
355 }
356
357 public Object getItemId(String key) throws IllegalStateException {
358 Object itemId = itemIdToKey.inverse().get(key);
359 if (itemId != null) {
360 return itemId;
361 } else {
362 throw new IllegalStateException("No item id for key " + key + " found.");
363 }
364 }
365
366 public Collection<Object> getItemIds(Collection<String> keys)
367 throws IllegalStateException {
368 if (keys == null) {
369 throw new IllegalArgumentException("keys may not be null");
370 }
371
372 final List<Object> itemIds = new ArrayList<>(keys.size());
373 for (String key : keys) {
374 itemIds.add(getItemId(key));
375 }
376 return itemIds;
377 }
378
379 Object itemIdAtIndex(int index) {
380 return indexToItemId.get(index);
381 }
382
383 int indexOf(Object itemId) {
384 return indexToItemId.inverse().get(itemId);
385 }
386
387 public void clearAll() {
388 indexToItemId.clear();
389 itemIdToKey.clear();
390 rollingIndex = 0;
391 activeRange = Range.withLength(0, 0);
392 }
393 }
394
395
396
397
398 public interface ElementSelectionListener {
399 void onElementsSelected(Set<Object> ids);
400 }
401
402
403
404
405 public interface ElementDblClickListener {
406 void onElementDblClicked(Object itemId);
407 }
408
409
410
411
412 public interface ElementRightClickListener {
413 void onElementRightClicked(Object itemId, int clickX, int clickY);
414 }
415 }