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