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