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.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   * Lazy layout of asset 'elements'. Either thumbnails (legacy implementation, 5.x UI), or cards (6.0+).
63   *
64   * References to 'thumbnail' in method or class names might remain.
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      * Since the item set changed - the indices in the state might now point to the different items.
250      * Since we know which items to select via {@code selectedIds}, we can update the indices in state as well.
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      * Maps item ids, indices and client-side keys to each other.
268      * Highly inspired by Vaadin analogous class used in Grid component implementation
269      * (introduced in Vaadin 7.4).
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              * First update container's page size if needed - in order to avoid multiple queries to
288              * the datasource.
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      * Listener interface for element selection.
397      */
398     public interface ElementSelectionListener {
399         void onElementsSelected(Set<Object> ids);
400     }
401 
402     /**
403      * Listener for element double clicks.
404      */
405     public interface ElementDblClickListener {
406         void onElementDblClicked(Object itemId);
407     }
408 
409     /**
410      * Listener for element right clicks.
411      */
412     public interface ElementRightClickListener {
413         void onElementRightClicked(Object itemId, int clickX, int clickY);
414     }
415 }