2 * Copyright 2011-2012 GRNET S.A. All rights reserved.
4 * Redistribution and use in source and binary forms, with or
5 * without modification, are permitted provided that the following
8 * 1. Redistributions of source code must retain the above
9 * copyright notice, this list of conditions and the following
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.
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.
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.
35 package gr.grnet.pithos.web.client;
39 import java.util.ArrayList;
40 import java.util.List;
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;
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.
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.
62 * @param <T> the data type of records in the list
64 public class PithosSelectionEventManager<T> implements
65 CellPreviewEvent.Handler<T> {
68 * Implementation of {@link gr.grnet.pithos.web.client.PithosSelectionEventManager.EventTranslator} that only triggers selection when
69 * any checkbox is selected.
71 * @param <T> the data type
73 public static class CheckboxEventTranslator<T> implements EventTranslator<T> {
76 * The column index of the checkbox. Other columns are ignored.
78 private final int column;
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.
84 public CheckboxEventTranslator() {
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.
92 * @param column the column index, or -1 for all columns
94 public CheckboxEventTranslator(int column) {
99 public boolean clearCurrentSelection(CellPreviewEvent<T> event) {
104 public SelectAction translateSelectionEvent(CellPreviewEvent<T> 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;
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(
121 return SelectAction.TOGGLE;
124 return SelectAction.IGNORE;
127 // For keyboard events, do the default action.
128 return SelectAction.DEFAULT;
133 * Translates {@link CellPreviewEvent}s into {@link SelectAction}s.
135 public static interface EventTranslator<T> {
137 * Check whether a user selection event should clear all currently selected
140 * @param event the {@link CellPreviewEvent} to translate
142 boolean clearCurrentSelection(CellPreviewEvent<T> event);
145 * Translate the user selection event into a {@link SelectAction}.
147 * @param event the {@link CellPreviewEvent} to translate
149 SelectAction translateSelectionEvent(CellPreviewEvent<T> event);
153 * The action that controls how selection is handled.
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.
164 * Construct a new {@link PithosSelectionEventManager} that triggers
165 * selection when any checkbox in any column is clicked.
167 * @param <T> the data type of the display
168 * @return a {@link PithosSelectionEventManager} instance
170 public static <T> PithosSelectionEventManager<T> createCheckboxManager() {
171 return new PithosSelectionEventManager<T>(new CheckboxEventTranslator<T>());
175 * Construct a new {@link PithosSelectionEventManager} that triggers
176 * selection when a checkbox in the specified column is clicked.
178 * @param <T> the data type of the display
179 * @param column the column to handle
180 * @return a {@link PithosSelectionEventManager} instance
182 public static <T> PithosSelectionEventManager<T> createCheckboxManager(
184 return new PithosSelectionEventManager<T>(new CheckboxEventTranslator<T>(
189 * Create a new {@link PithosSelectionEventManager} using the specified
190 * {@link EventTranslator} to control which {@link SelectAction} to take for
193 * @param <T> the data type of the display
194 * @param translator the {@link EventTranslator} to use
195 * @return a {@link PithosSelectionEventManager} instance
197 public static <T> PithosSelectionEventManager<T> createCustomManager(
198 EventTranslator<T> translator) {
199 return new PithosSelectionEventManager<T>(translator);
203 * Create a new {@link PithosSelectionEventManager} that handles selection
204 * via user interactions.
206 * @param <T> the data type of the display
207 * @return a new {@link PithosSelectionEventManager} instance
209 public static <T> PithosSelectionEventManager<T> createDefaultManager() {
210 return new PithosSelectionEventManager<T>(null);
214 * The last {@link HasData} that was handled.
216 private HasData<T> lastDisplay;
219 * The last page start.
221 private int lastPageStart;
224 * The last selected row index.
226 private int lastSelectedIndex = -1;
229 * A boolean indicating that the last shift selection was additive.
231 private boolean shiftAdditive;
234 * The last place where the user clicked without holding shift. Multi
235 * selections that use the shift key are rooted at the anchor.
237 private int shiftAnchor = -1;
240 * The {@link EventTranslator} that controls how selection is handled.
242 private final EventTranslator<T> translator;
245 * Construct a new {@link PithosSelectionEventManager} using the specified
246 * {@link EventTranslator} to control which {@link SelectAction} to take for
249 * @param translator the {@link EventTranslator} to use
251 protected PithosSelectionEventManager(EventTranslator<T> translator) {
252 this.translator = translator;
256 * Update the selection model based on a user selection event.
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
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) {
276 addToSelection = true;
279 addToSelection = false;
282 addToSelection = !selectionModel.isSelected(rowValue);
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) {
294 * Get the new shift bounds based on the existing shift anchor and the
297 int start = Math.min(shiftAnchor, row); // Inclusive.
298 int end = Math.max(shiftAnchor, row); // Inclusive.
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);
309 // Remember if we are adding or removing rows.
310 shiftAdditive = addToSelection;
313 // Update the last selected row, but do not move the shift anchor.
314 lastSelectedIndex = row;
317 setRangeSelection(selectionModel, display, new Range(start, end - start
318 + 1), shiftAdditive, clearOthers);
321 * If we are not selecting a range, save the last row and set the shift
324 lastDisplay = display;
325 lastPageStart = pageStart;
326 lastSelectedIndex = row;
328 selectOne(selectionModel, rowValue, addToSelection, clearOthers);
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()) {
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) {
346 // Check for user defined actions.
347 SelectAction action = (translator == null) ? SelectAction.DEFAULT
348 : translator.translateSelectionEvent(event);
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);
356 // Use the standard handler.
357 handleSelectionEvent(event, action, selectionModel);
362 * Removes all items from the selection.
364 * @param selectionModel the {@link MultiSelectionModel} to clear
366 protected void clearSelection(MultiSelectionModel<? super T> selectionModel) {
367 selectionModel.clear();
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.
376 * @param event the {@link CellPreviewEvent} that triggered selection
377 * @param action the action to handle
378 * @param selectionModel the {@link SelectionModel} to update
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;
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;
394 //if the row is selected then do nothing
395 if(selectionModel.isSelected(event.getValue())){
398 doMultiSelection(selectionModel, event.getDisplay(), event.getIndex(),
399 event.getValue(), action1, shift, clearOthers);
401 else if ("click".equals(type)) {
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.
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;
414 doMultiSelection(selectionModel, event.getDisplay(), event.getIndex(),
415 event.getValue(), action1, shift, clearOthers);
417 event.setCanceled(true);
419 } else if ("keyup".equals(type)) {
420 int keyCode = nativeEvent.getKeyCode();
423 * Update selection when the space bar is pressed. The spacebar always
424 * toggles selection, regardless of whether the control key is pressed.
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;
432 doMultiSelection(selectionModel, event.getDisplay(), event.getIndex(),
433 event.getValue(), action1, shift, clearOthers);
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.
443 * @param event the {@link CellPreviewEvent} that triggered selection
444 * @param action the action to handle
445 * @param selectionModel the {@link SelectionModel} to update
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) {
456 selectionModel.setSelected(value, true);
459 selectionModel.setSelected(value, false);
462 selectionModel.setSelected(value, !selectionModel.isSelected(value));
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));
478 selectionModel.setSelected(value, true);
480 } else if ("keyup".equals(type)) {
481 // Toggle selection on space.
482 int keyCode = nativeEvent.getKeyCode();
484 selectionModel.setSelected(value, !selectionModel.isSelected(value));
490 * Selects the given item, optionally clearing any prior selection.
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
497 protected void selectOne(MultiSelectionModel<? super T> selectionModel,
498 T target, boolean selected, boolean clearOthers) {
500 clearSelection(selectionModel);
502 selectionModel.setSelected(target, selected);
506 * Select or deselect a range of row indexes, optionally deselecting all other
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
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()));
525 // Clear all other values.
527 clearSelection(selectionModel);
530 // Update the state of the values.
531 for (T value : toUpdate) {
532 selectionModel.setSelected(value, addToSelection);