View Javadoc
1   /**
2    * This file Copyright (c) 2019 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 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   * Base implementation of thumbnail gallery view.
62   *
63   * @param <T>
64   *     data type backing up the thumbnails
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      * Maps item ids, indices and client-side keys to each other.
238      * Highly inspired by Vaadin analogous class used in Grid component implementation
239      * (introduced in Vaadin 7.4).
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      * Listener interface for element selection.
358      * @param <T> item type.
359      */
360     public interface ElementSelectionListener<T> {
361         void onElementsSelected(Set<T> items);
362     }
363 
364     /**
365      * Listener for element double clicks.
366      * @param <T> item type.
367      */
368     public interface ElementDblClickListener<T> {
369         void onElementDblClicked(T item);
370     }
371 
372     /**
373      * Listener for element right clicks.
374      * @param <T> item type.
375      */
376     public interface ElementRightClickListener<T> {
377         void onElementRightClicked(T item, int clickX, int clickY);
378     }
379 }