2 * Copyright 2011 Electronic Business Systems Ltd.
4 * This file is part of GSS.
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.
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.
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/>.
19 package gr.grnet.pithos.web.client;
27 import java.util.ArrayList;
28 import java.util.List;
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;
40 * An implementation of {@link CellPreviewEvent.Handler} that adds selection
41 * support via the spacebar and mouse clicks and handles the control key.
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.
50 * @param <T> the data type of records in the list
52 public class GSSSelectionEventManager<T> implements
53 CellPreviewEvent.Handler<T> {
56 * Implementation of {@link EventTranslator} that only triggers selection when
57 * any checkbox is selected.
59 * @param <T> the data type
61 public static class CheckboxEventTranslator<T> implements EventTranslator<T> {
64 * The column index of the checkbox. Other columns are ignored.
66 private final int column;
69 * Construct a new {@link CheckboxEventTranslator} that will trigger
70 * selection when any checkbox in any column is selected.
72 public CheckboxEventTranslator() {
77 * Construct a new {@link CheckboxEventTranslator} that will trigger
78 * selection when a checkbox in the specified column is selected.
80 * @param column the column index, or -1 for all columns
82 public CheckboxEventTranslator(int column) {
86 public boolean clearCurrentSelection(CellPreviewEvent<T> event) {
90 public SelectAction translateSelectionEvent(CellPreviewEvent<T> 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;
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(
107 return SelectAction.TOGGLE;
110 return SelectAction.IGNORE;
113 // For keyboard events, do the default action.
114 return SelectAction.DEFAULT;
119 * Translates {@link CellPreviewEvent}s into {@link SelectAction}s.
121 public static interface EventTranslator<T> {
123 * Check whether a user selection event should clear all currently selected
126 * @param event the {@link CellPreviewEvent} to translate
128 boolean clearCurrentSelection(CellPreviewEvent<T> event);
131 * Translate the user selection event into a {@link SelectAction}.
133 * @param event the {@link CellPreviewEvent} to translate
135 SelectAction translateSelectionEvent(CellPreviewEvent<T> event);
139 * The action that controls how selection is handled.
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.
150 * Construct a new {@link GSSSelectionEventManager} that triggers
151 * selection when any checkbox in any column is clicked.
153 * @param <T> the data type of the display
154 * @return a {@link GSSSelectionEventManager} instance
156 public static <T> GSSSelectionEventManager<T> createCheckboxManager() {
157 return new GSSSelectionEventManager<T>(new CheckboxEventTranslator<T>());
161 * Construct a new {@link GSSSelectionEventManager} that triggers
162 * selection when a checkbox in the specified column is clicked.
164 * @param <T> the data type of the display
165 * @param column the column to handle
166 * @return a {@link GSSSelectionEventManager} instance
168 public static <T> GSSSelectionEventManager<T> createCheckboxManager(
170 return new GSSSelectionEventManager<T>(new CheckboxEventTranslator<T>(
175 * Create a new {@link GSSSelectionEventManager} using the specified
176 * {@link EventTranslator} to control which {@link SelectAction} to take for
179 * @param <T> the data type of the display
180 * @param translator the {@link EventTranslator} to use
181 * @return a {@link GSSSelectionEventManager} instance
183 public static <T> GSSSelectionEventManager<T> createCustomManager(
184 EventTranslator<T> translator) {
185 return new GSSSelectionEventManager<T>(translator);
189 * Create a new {@link GSSSelectionEventManager} that handles selection
190 * via user interactions.
192 * @param <T> the data type of the display
193 * @return a new {@link GSSSelectionEventManager} instance
195 public static <T> GSSSelectionEventManager<T> createDefaultManager() {
196 return new GSSSelectionEventManager<T>(null);
200 * The last {@link HasData} that was handled.
202 private HasData<T> lastDisplay;
205 * The last page start.
207 private int lastPageStart;
210 * The last selected row index.
212 private int lastSelectedIndex = -1;
215 * A boolean indicating that the last shift selection was additive.
217 private boolean shiftAdditive;
220 * The last place where the user clicked without holding shift. Multi
221 * selections that use the shift key are rooted at the anchor.
223 private int shiftAnchor = -1;
226 * The {@link EventTranslator} that controls how selection is handled.
228 private final EventTranslator<T> translator;
231 * Construct a new {@link GSSSelectionEventManager} using the specified
232 * {@link EventTranslator} to control which {@link SelectAction} to take for
235 * @param translator the {@link EventTranslator} to use
237 protected GSSSelectionEventManager(EventTranslator<T> translator) {
238 this.translator = translator;
242 * Update the selection model based on a user selection event.
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
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) {
262 addToSelection = true;
265 addToSelection = false;
268 addToSelection = !selectionModel.isSelected(rowValue);
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) {
278 * Get the new shift bounds based on the existing shift anchor and the
281 int start = Math.min(shiftAnchor, row); // Inclusive.
282 int end = Math.max(shiftAnchor, row); // Inclusive.
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);
293 // Remember if we are adding or removing rows.
294 shiftAdditive = addToSelection;
297 // Update the last selected row, but do not move the shift anchor.
298 lastSelectedIndex = row;
301 setRangeSelection(selectionModel, display, new Range(start, end - start
302 + 1), shiftAdditive, clearOthers);
305 * If we are not selecting a range, save the last row and set the shift
308 lastDisplay = display;
309 lastPageStart = pageStart;
310 lastSelectedIndex = row;
312 selectOne(selectionModel, rowValue, addToSelection, clearOthers);
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()) {
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) {
329 // Check for user defined actions.
330 SelectAction action = (translator == null) ? SelectAction.DEFAULT
331 : translator.translateSelectionEvent(event);
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);
339 // Use the standard handler.
340 handleSelectionEvent(event, action, selectionModel);
345 * Removes all items from the selection.
347 * @param selectionModel the {@link MultiSelectionModel} to clear
349 protected void clearSelection(MultiSelectionModel<? super T> selectionModel) {
350 selectionModel.clear();
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.
359 * @param event the {@link CellPreviewEvent} that triggered selection
360 * @param action the action to handle
361 * @param selectionModel the {@link SelectionModel} to update
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;
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;
376 //if the row is selected then do nothing
377 if(selectionModel.isSelected(event.getValue())){
380 doMultiSelection(selectionModel, event.getDisplay(), event.getIndex(),
381 event.getValue(), action, shift, clearOthers);
383 else if ("click".equals(type)) {
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.
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;
396 doMultiSelection(selectionModel, event.getDisplay(), event.getIndex(),
397 event.getValue(), action, shift, clearOthers);
399 event.setCanceled(true);
401 } else if ("keyup".equals(type)) {
402 int keyCode = nativeEvent.getKeyCode();
405 * Update selection when the space bar is pressed. The spacebar always
406 * toggles selection, regardless of whether the control key is pressed.
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;
414 doMultiSelection(selectionModel, event.getDisplay(), event.getIndex(),
415 event.getValue(), action, shift, clearOthers);
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.
425 * @param event the {@link CellPreviewEvent} that triggered selection
426 * @param action the action to handle
427 * @param selectionModel the {@link SelectionModel} to update
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) {
438 selectionModel.setSelected(value, true);
441 selectionModel.setSelected(value, false);
444 selectionModel.setSelected(value, !selectionModel.isSelected(value));
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));
458 selectionModel.setSelected(value, true);
460 } else if ("keyup".equals(type)) {
461 // Toggle selection on space.
462 int keyCode = nativeEvent.getKeyCode();
464 selectionModel.setSelected(value, !selectionModel.isSelected(value));
470 * Selects the given item, optionally clearing any prior selection.
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
477 protected void selectOne(MultiSelectionModel<? super T> selectionModel,
478 T target, boolean selected, boolean clearOthers) {
480 clearSelection(selectionModel);
482 selectionModel.setSelected(target, selected);
486 * Select or deselect a range of row indexes, optionally deselecting all other
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
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()));
506 // Clear all other values.
508 clearSelection(selectionModel);
511 // Update the state of the values.
512 for (T value : toUpdate) {
513 selectionModel.setSelected(value, addToSelection);