Fixed various warnings
[pithos-web-client] / src / gr / grnet / pithos / web / client / PithosSelectionEventManager.java
1 /*
2  * Copyright 2011 GRNET S.A. All rights reserved.
3  *
4  * Redistribution and use in source and binary forms, with or
5  * without modification, are permitted provided that the following
6  * conditions are met:
7  *
8  *   1. Redistributions of source code must retain the above
9  *      copyright notice, this list of conditions and the following
10  *      disclaimer.
11  *
12  *   2. Redistributions in binary form must reproduce the above
13  *      copyright notice, this list of conditions and the following
14  *      disclaimer in the documentation and/or other materials
15  *      provided with the distribution.
16  *
17  * THIS SOFTWARE IS PROVIDED BY GRNET S.A. ``AS IS'' AND ANY EXPRESS
18  * OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
19  * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
20  * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL GRNET S.A OR
21  * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
22  * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
23  * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF
24  * USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED
25  * AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
26  * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
27  * ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
28  * POSSIBILITY OF SUCH DAMAGE.
29  *
30  * The views and conclusions contained in the software and
31  * documentation are those of the authors and should not be
32  * interpreted as representing official policies, either expressed
33  * or implied, of GRNET S.A.
34  */
35 package gr.grnet.pithos.web.client;
36
37
38
39 import java.util.ArrayList;
40 import java.util.List;
41
42 import com.google.gwt.dom.client.Element;
43 import com.google.gwt.dom.client.InputElement;
44 import com.google.gwt.dom.client.NativeEvent;
45 import com.google.gwt.view.client.CellPreviewEvent;
46 import com.google.gwt.view.client.HasData;
47 import com.google.gwt.view.client.MultiSelectionModel;
48 import com.google.gwt.view.client.Range;
49 import com.google.gwt.view.client.SelectionModel;
50
51 /**
52  * An implementation of {@link com.google.gwt.view.client.CellPreviewEvent.Handler} that adds selection
53  * support via the spacebar and mouse clicks and handles the control key.
54  * 
55  * <p>
56  * If the {@link HasData} source of the selection event uses a
57  * {@link MultiSelectionModel}, this manager additionally provides support for
58  * shift key to select a range of values. For all other {@link SelectionModel}s,
59  * only the control key is supported.
60  * </p>
61  * 
62  * @param <T> the data type of records in the list
63  */
64 public class PithosSelectionEventManager<T> implements
65     CellPreviewEvent.Handler<T> {
66
67   /**
68    * Implementation of {@link gr.grnet.pithos.web.client.PithosSelectionEventManager.EventTranslator} that only triggers selection when
69    * any checkbox is selected.
70    * 
71    * @param <T> the data type
72    */
73   public static class CheckboxEventTranslator<T> implements EventTranslator<T> {
74
75     /**
76      * The column index of the checkbox. Other columns are ignored.
77      */
78     private final int column;
79
80     /**
81      * Construct a new {@link gr.grnet.pithos.web.client.PithosSelectionEventManager.CheckboxEventTranslator} that will trigger
82      * selection when any checkbox in any column is selected.
83      */
84     public CheckboxEventTranslator() {
85       this(-1);
86     }
87
88     /**
89      * Construct a new {@link gr.grnet.pithos.web.client.PithosSelectionEventManager.CheckboxEventTranslator} that will trigger
90      * selection when a checkbox in the specified column is selected.
91      * 
92      * @param column the column index, or -1 for all columns
93      */
94     public CheckboxEventTranslator(int column) {
95       this.column = column;
96     }
97
98     @Override
99         public boolean clearCurrentSelection(CellPreviewEvent<T> event) {
100       return false;
101     }
102
103     @Override
104         public SelectAction translateSelectionEvent(CellPreviewEvent<T> event) {
105       // Handle the event.
106       NativeEvent nativeEvent = event.getNativeEvent();
107       if ("click".equals(nativeEvent.getType())) {
108         // Ignore if the event didn't occur in the correct column.
109         if (column > -1 && column != event.getColumn()) {
110           return SelectAction.IGNORE;
111         }
112
113         // Determine if we clicked on a checkbox.
114         Element target = nativeEvent.getEventTarget().cast();
115         if ("input".equals(target.getTagName().toLowerCase())) {
116           final InputElement input = target.cast();
117           if ("checkbox".equals(input.getType().toLowerCase())) {
118             // Synchronize the checkbox with the current selection state.
119             input.setChecked(event.getDisplay().getSelectionModel().isSelected(
120                 event.getValue()));
121             return SelectAction.TOGGLE;
122           }
123         }
124         return SelectAction.IGNORE;
125       }
126
127       // For keyboard events, do the default action.
128       return SelectAction.DEFAULT;
129     }
130   }
131
132   /**
133    * Translates {@link CellPreviewEvent}s into {@link SelectAction}s.
134    */
135   public static interface EventTranslator<T> {
136     /**
137      * Check whether a user selection event should clear all currently selected
138      * values.
139      * 
140      * @param event the {@link CellPreviewEvent} to translate
141      */
142     boolean clearCurrentSelection(CellPreviewEvent<T> event);
143
144     /**
145      * Translate the user selection event into a {@link SelectAction}.
146      * 
147      * @param event the {@link CellPreviewEvent} to translate
148      */
149     SelectAction translateSelectionEvent(CellPreviewEvent<T> event);
150   }
151
152   /**
153    * The action that controls how selection is handled.
154    */
155   public static enum SelectAction {
156     DEFAULT, // Perform the default action.
157     SELECT, // Select the value.
158     DESELECT, // Deselect the value.
159     TOGGLE, // Toggle the selected state of the value.
160     IGNORE; // Ignore the event.
161   }
162
163   /**
164    * Construct a new {@link PithosSelectionEventManager} that triggers
165    * selection when any checkbox in any column is clicked.
166    * 
167    * @param <T> the data type of the display
168    * @return a {@link PithosSelectionEventManager} instance
169    */
170   public static <T> PithosSelectionEventManager<T> createCheckboxManager() {
171     return new PithosSelectionEventManager<T>(new CheckboxEventTranslator<T>());
172   }
173
174   /**
175    * Construct a new {@link PithosSelectionEventManager} that triggers
176    * selection when a checkbox in the specified column is clicked.
177    * 
178    * @param <T> the data type of the display
179    * @param column the column to handle
180    * @return a {@link PithosSelectionEventManager} instance
181    */
182   public static <T> PithosSelectionEventManager<T> createCheckboxManager(
183       int column) {
184     return new PithosSelectionEventManager<T>(new CheckboxEventTranslator<T>(
185         column));
186   }
187
188   /**
189    * Create a new {@link PithosSelectionEventManager} using the specified
190    * {@link EventTranslator} to control which {@link SelectAction} to take for
191    * each event.
192    * 
193    * @param <T> the data type of the display
194    * @param translator the {@link EventTranslator} to use
195    * @return a {@link PithosSelectionEventManager} instance
196    */
197   public static <T> PithosSelectionEventManager<T> createCustomManager(
198       EventTranslator<T> translator) {
199     return new PithosSelectionEventManager<T>(translator);
200   }
201
202   /**
203    * Create a new {@link PithosSelectionEventManager} that handles selection
204    * via user interactions.
205    * 
206    * @param <T> the data type of the display
207    * @return a new {@link PithosSelectionEventManager} instance
208    */
209   public static <T> PithosSelectionEventManager<T> createDefaultManager() {
210     return new PithosSelectionEventManager<T>(null);
211   }
212
213   /**
214    * The last {@link HasData} that was handled.
215    */
216   private HasData<T> lastDisplay;
217
218   /**
219    * The last page start.
220    */
221   private int lastPageStart;
222
223   /**
224    * The last selected row index.
225    */
226   private int lastSelectedIndex = -1;
227
228   /**
229    * A boolean indicating that the last shift selection was additive.
230    */
231   private boolean shiftAdditive;
232
233   /**
234    * The last place where the user clicked without holding shift. Multi
235    * selections that use the shift key are rooted at the anchor.
236    */
237   private int shiftAnchor = -1;
238
239   /**
240    * The {@link EventTranslator} that controls how selection is handled.
241    */
242   private final EventTranslator<T> translator;
243
244   /**
245    * Construct a new {@link PithosSelectionEventManager} using the specified
246    * {@link EventTranslator} to control which {@link SelectAction} to take for
247    * each event.
248    * 
249    * @param translator the {@link EventTranslator} to use
250    */
251   protected PithosSelectionEventManager(EventTranslator<T> translator) {
252     this.translator = translator;
253   }
254
255   /**
256    * Update the selection model based on a user selection event.
257    * 
258    * @param selectionModel the selection model to update
259    * @param row the selected row index relative to the page start
260    * @param rowValue the selected row value
261    * @param action the {@link SelectAction} to apply
262    * @param selectRange true to select the range from the last selected row
263    * @param clearOthers true to clear the current selection
264    */
265   public void doMultiSelection(MultiSelectionModel<? super T> selectionModel,
266       HasData<T> display, int row, T rowValue, SelectAction action,
267       boolean selectRange, boolean clearOthers) {
268     // Determine if we will add or remove selection.
269     boolean addToSelection = true;
270     if (action != null) {
271       switch (action) {
272         case IGNORE:
273           // Ignore selection.
274           return;
275         case SELECT:
276           addToSelection = true;
277           break;
278         case DESELECT:
279           addToSelection = false;
280           break;
281         case TOGGLE:
282           addToSelection = !selectionModel.isSelected(rowValue);
283           break;
284         case DEFAULT:
285           break;
286       }
287     }
288
289     // Determine which rows will be newly selected.
290     int pageStart = display.getVisibleRange().getStart();
291     if (selectRange && pageStart == lastPageStart && lastSelectedIndex > -1
292         && shiftAnchor > -1 && display == lastDisplay) {
293       /*
294        * Get the new shift bounds based on the existing shift anchor and the
295        * selected row.
296        */
297       int start = Math.min(shiftAnchor, row); // Inclusive.
298       int end = Math.max(shiftAnchor, row); // Inclusive.
299
300       if (lastSelectedIndex < start) {
301         // Revert previous selection if the user reselects a smaller range.
302         setRangeSelection(selectionModel, display, new Range(lastSelectedIndex,
303             start - lastSelectedIndex), !shiftAdditive, false);
304       } else if (lastSelectedIndex > end) {
305         // Revert previous selection if the user reselects a smaller range.
306         setRangeSelection(selectionModel, display, new Range(end + 1,
307             lastSelectedIndex - end), !shiftAdditive, false);
308       } else {
309         // Remember if we are adding or removing rows.
310         shiftAdditive = addToSelection;
311       }
312
313       // Update the last selected row, but do not move the shift anchor.
314       lastSelectedIndex = row;
315
316       // Select the range.
317       setRangeSelection(selectionModel, display, new Range(start, end - start
318           + 1), shiftAdditive, clearOthers);
319     } else {
320       /*
321        * If we are not selecting a range, save the last row and set the shift
322        * anchor.
323        */
324       lastDisplay = display;
325       lastPageStart = pageStart;
326       lastSelectedIndex = row;
327       shiftAnchor = row;
328       selectOne(selectionModel, rowValue, addToSelection, clearOthers);
329     }
330   }
331
332   @Override
333 public void onCellPreview(CellPreviewEvent<T> event) {
334     // Early exit if selection is already handled or we are editing.
335     if (event.isCellEditing() || event.isSelectionHandled()) {
336       return;
337     }
338
339     // Early exit if we do not have a SelectionModel.
340     HasData<T> display = event.getDisplay();
341     SelectionModel<? super T> selectionModel = display.getSelectionModel();
342     if (selectionModel == null) {
343       return;
344     }
345
346     // Check for user defined actions.
347     SelectAction action = (translator == null) ? SelectAction.DEFAULT
348         : translator.translateSelectionEvent(event);
349
350     // Handle the event based on the SelectionModel type.
351     if (selectionModel instanceof MultiSelectionModel) {
352       // Add shift key support for MultiSelectionModel.
353       handleMultiSelectionEvent(event, action,
354           (MultiSelectionModel<? super T>) selectionModel);
355     } else {
356       // Use the standard handler.
357       handleSelectionEvent(event, action, selectionModel);
358     }
359   }
360
361   /**
362    * Removes all items from the selection.
363    * 
364    * @param selectionModel the {@link MultiSelectionModel} to clear
365    */
366   protected void clearSelection(MultiSelectionModel<? super T> selectionModel) {
367     selectionModel.clear();
368   }
369
370   /**
371    * Handle an event that could cause a value to be selected for a
372    * {@link MultiSelectionModel}. This overloaded method adds support for both
373    * the control and shift keys. If the shift key is held down, all rows between
374    * the previous selected row and the current row are selected.
375    * 
376    * @param event the {@link CellPreviewEvent} that triggered selection
377    * @param action the action to handle
378    * @param selectionModel the {@link SelectionModel} to update
379    */
380   protected void handleMultiSelectionEvent(CellPreviewEvent<T> event,
381       SelectAction action, MultiSelectionModel<? super T> selectionModel) {
382     NativeEvent nativeEvent = event.getNativeEvent();
383     String type = nativeEvent.getType();
384     boolean rightclick = "mousedown".equals(type) && nativeEvent.getButton()==NativeEvent.BUTTON_RIGHT;
385     SelectAction action1 = action;
386     if(rightclick){
387         boolean shift = nativeEvent.getShiftKey();
388         boolean ctrlOrMeta = nativeEvent.getCtrlKey() || nativeEvent.getMetaKey();
389         boolean clearOthers = (translator == null) ? !ctrlOrMeta
390             : translator.clearCurrentSelection(event);
391         if (action == null || action == SelectAction.DEFAULT) {
392           action1 = ctrlOrMeta ? SelectAction.TOGGLE : SelectAction.SELECT;
393         }
394         //if the row is selected then do nothing
395         if(selectionModel.isSelected(event.getValue())){
396                 return;
397         }
398         doMultiSelection(selectionModel, event.getDisplay(), event.getIndex(),
399             event.getValue(), action1, shift, clearOthers);
400     }
401     else if ("click".equals(type)) {
402       /*
403        * Update selection on click. Selection is toggled only if the user
404        * presses the ctrl key. If the user does not press the control key,
405        * selection is additive.
406        */
407       boolean shift = nativeEvent.getShiftKey();
408       boolean ctrlOrMeta = nativeEvent.getCtrlKey() || nativeEvent.getMetaKey();
409       boolean clearOthers = (translator == null) ? !ctrlOrMeta
410           : translator.clearCurrentSelection(event);
411       if (action == null || action == SelectAction.DEFAULT) {
412         action1 = ctrlOrMeta ? SelectAction.TOGGLE : SelectAction.SELECT;
413       }
414       doMultiSelection(selectionModel, event.getDisplay(), event.getIndex(),
415           event.getValue(), action1, shift, clearOthers);
416       if(ctrlOrMeta){
417           event.setCanceled(true);
418       }
419     } else if ("keyup".equals(type)) {
420       int keyCode = nativeEvent.getKeyCode();
421       if (keyCode == 32) {
422         /*
423          * Update selection when the space bar is pressed. The spacebar always
424          * toggles selection, regardless of whether the control key is pressed.
425          */
426         boolean shift = nativeEvent.getShiftKey();
427         boolean clearOthers = (translator == null) ? false
428             : translator.clearCurrentSelection(event);
429         if (action == null || action == SelectAction.DEFAULT) {
430           action1 = SelectAction.TOGGLE;
431         }
432         doMultiSelection(selectionModel, event.getDisplay(), event.getIndex(),
433             event.getValue(), action1, shift, clearOthers);
434       }
435     }
436   }
437
438   /**
439    * Handle an event that could cause a value to be selected. This method works
440    * for any {@link SelectionModel}. Pressing the space bar or ctrl+click will
441    * toggle the selection state. Clicking selects the row if it is not selected.
442    * 
443    * @param event the {@link CellPreviewEvent} that triggered selection
444    * @param action the action to handle
445    * @param selectionModel the {@link SelectionModel} to update
446    */
447   protected void handleSelectionEvent(CellPreviewEvent<T> event,
448       SelectAction action, SelectionModel<? super T> selectionModel) {
449     // Handle selection overrides.
450     T value = event.getValue();
451     if (action != null) {
452       switch (action) {
453         case IGNORE:
454           return;
455         case SELECT:
456           selectionModel.setSelected(value, true);
457           return;
458         case DESELECT:
459           selectionModel.setSelected(value, false);
460           return;
461         case TOGGLE:
462           selectionModel.setSelected(value, !selectionModel.isSelected(value));
463           return;
464                 case DEFAULT:
465                         break;
466       }
467     }
468
469     // Handle default selection.
470     NativeEvent nativeEvent = event.getNativeEvent();
471     String type = nativeEvent.getType();
472     if ("click".equals(type)) {
473       if (nativeEvent.getCtrlKey() || nativeEvent.getMetaKey()) {
474         // Toggle selection on ctrl+click.
475         selectionModel.setSelected(value, !selectionModel.isSelected(value));
476       } else {
477         // Select on click.
478         selectionModel.setSelected(value, true);
479       }
480     } else if ("keyup".equals(type)) {
481       // Toggle selection on space.
482       int keyCode = nativeEvent.getKeyCode();
483       if (keyCode == 32) {
484         selectionModel.setSelected(value, !selectionModel.isSelected(value));
485       }
486     }
487   }
488
489   /**
490    * Selects the given item, optionally clearing any prior selection.
491    * 
492    * @param selectionModel the {@link MultiSelectionModel} to update
493    * @param target the item to select
494    * @param selected true to select, false to deselect
495    * @param clearOthers true to clear all other selected items
496    */
497   protected void selectOne(MultiSelectionModel<? super T> selectionModel,
498       T target, boolean selected, boolean clearOthers) {
499     if (clearOthers) {
500       clearSelection(selectionModel);
501     }
502     selectionModel.setSelected(target, selected);
503   }
504
505   /**
506    * Select or deselect a range of row indexes, optionally deselecting all other
507    * values.
508    * 
509    * @param selectionModel the {@link MultiSelectionModel} to update
510    * @param display the {@link HasData} source of the selection event
511    * @param range the {@link Range} of rows to select or deselect
512    * @param addToSelection true to select, false to deselect the range
513    * @param clearOthers true to deselect rows not in the range
514    */
515   protected void setRangeSelection(
516       MultiSelectionModel<? super T> selectionModel, HasData<T> display,
517       Range range, boolean addToSelection, boolean clearOthers) {
518     // Get the list of values to select.
519     List<T> toUpdate = new ArrayList<T>();
520     int start = range.getStart();
521     int end = start + range.getLength();
522     for (int i = start; i < end ; i++) {
523              toUpdate.add(display.getVisibleItem(i-display.getVisibleRange().getStart()));
524         }
525     // Clear all other values.
526     if (clearOthers) {
527       clearSelection(selectionModel);
528     }
529
530     // Update the state of the values.
531     for (T value : toUpdate) {
532       selectionModel.setSelected(value, addToSelection);
533     }
534   }
535 }
536