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.ebs.gss.client;
27 import java.util.ArrayList;
28 import java.util.List;
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;
43 * An implementation of {@link CellPreviewEvent.Handler} that adds selection
44 * support via the spacebar and mouse clicks and handles the control key.
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.
53 * @param <T> the data type of records in the list
55 public class GSSSelectionEventManager<T> implements
56 CellPreviewEvent.Handler<T> {
59 * Implementation of {@link EventTranslator} that only triggers selection when
60 * any checkbox is selected.
62 * @param <T> the data type
64 public static class CheckboxEventTranslator<T> implements EventTranslator<T> {
67 * The column index of the checkbox. Other columns are ignored.
69 private final int column;
72 * Construct a new {@link CheckboxEventTranslator} that will trigger
73 * selection when any checkbox in any column is selected.
75 public CheckboxEventTranslator() {
80 * Construct a new {@link CheckboxEventTranslator} that will trigger
81 * selection when a checkbox in the specified column is selected.
83 * @param column the column index, or -1 for all columns
85 public CheckboxEventTranslator(int column) {
89 public boolean clearCurrentSelection(CellPreviewEvent<T> event) {
93 public SelectAction translateSelectionEvent(CellPreviewEvent<T> 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;
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(
110 return SelectAction.TOGGLE;
113 return SelectAction.IGNORE;
116 // For keyboard events, do the default action.
117 return SelectAction.DEFAULT;
122 * Translates {@link CellPreviewEvent}s into {@link SelectAction}s.
124 public static interface EventTranslator<T> {
126 * Check whether a user selection event should clear all currently selected
129 * @param event the {@link CellPreviewEvent} to translate
131 boolean clearCurrentSelection(CellPreviewEvent<T> event);
134 * Translate the user selection event into a {@link SelectAction}.
136 * @param event the {@link CellPreviewEvent} to translate
138 SelectAction translateSelectionEvent(CellPreviewEvent<T> event);
142 * The action that controls how selection is handled.
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.
153 * Construct a new {@link GSSSelectionEventManager} that triggers
154 * selection when any checkbox in any column is clicked.
156 * @param <T> the data type of the display
157 * @return a {@link GSSSelectionEventManager} instance
159 public static <T> GSSSelectionEventManager<T> createCheckboxManager() {
160 return new GSSSelectionEventManager<T>(new CheckboxEventTranslator<T>());
164 * Construct a new {@link GSSSelectionEventManager} that triggers
165 * selection when a checkbox in the specified column is clicked.
167 * @param <T> the data type of the display
168 * @param column the column to handle
169 * @return a {@link GSSSelectionEventManager} instance
171 public static <T> GSSSelectionEventManager<T> createCheckboxManager(
173 return new GSSSelectionEventManager<T>(new CheckboxEventTranslator<T>(
178 * Create a new {@link GSSSelectionEventManager} using the specified
179 * {@link EventTranslator} to control which {@link SelectAction} to take for
182 * @param <T> the data type of the display
183 * @param translator the {@link EventTranslator} to use
184 * @return a {@link GSSSelectionEventManager} instance
186 public static <T> GSSSelectionEventManager<T> createCustomManager(
187 EventTranslator<T> translator) {
188 return new GSSSelectionEventManager<T>(translator);
192 * Create a new {@link GSSSelectionEventManager} that handles selection
193 * via user interactions.
195 * @param <T> the data type of the display
196 * @return a new {@link GSSSelectionEventManager} instance
198 public static <T> GSSSelectionEventManager<T> createDefaultManager() {
199 return new GSSSelectionEventManager<T>(null);
203 * The last {@link HasData} that was handled.
205 private HasData<T> lastDisplay;
208 * The last page start.
210 private int lastPageStart;
213 * The last selected row index.
215 private int lastSelectedIndex = -1;
218 * A boolean indicating that the last shift selection was additive.
220 private boolean shiftAdditive;
223 * The last place where the user clicked without holding shift. Multi
224 * selections that use the shift key are rooted at the anchor.
226 private int shiftAnchor = -1;
229 * The {@link EventTranslator} that controls how selection is handled.
231 private final EventTranslator<T> translator;
234 * Construct a new {@link GSSSelectionEventManager} using the specified
235 * {@link EventTranslator} to control which {@link SelectAction} to take for
238 * @param translator the {@link EventTranslator} to use
240 protected GSSSelectionEventManager(EventTranslator<T> translator) {
241 this.translator = translator;
245 * Update the selection model based on a user selection event.
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
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) {
265 addToSelection = true;
268 addToSelection = false;
271 addToSelection = !selectionModel.isSelected(rowValue);
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) {
281 * Get the new shift bounds based on the existing shift anchor and the
284 int start = Math.min(shiftAnchor, row); // Inclusive.
285 int end = Math.max(shiftAnchor, row); // Inclusive.
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);
296 // Remember if we are adding or removing rows.
297 shiftAdditive = addToSelection;
300 // Update the last selected row, but do not move the shift anchor.
301 lastSelectedIndex = row;
304 setRangeSelection(selectionModel, display, new Range(start, end - start
305 + 1), shiftAdditive, clearOthers);
308 * If we are not selecting a range, save the last row and set the shift
311 lastDisplay = display;
312 lastPageStart = pageStart;
313 lastSelectedIndex = row;
315 selectOne(selectionModel, rowValue, addToSelection, clearOthers);
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()) {
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) {
332 // Check for user defined actions.
333 SelectAction action = (translator == null) ? SelectAction.DEFAULT
334 : translator.translateSelectionEvent(event);
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);
342 // Use the standard handler.
343 handleSelectionEvent(event, action, selectionModel);
348 * Removes all items from the selection.
350 * @param selectionModel the {@link MultiSelectionModel} to clear
352 protected void clearSelection(MultiSelectionModel<? super T> selectionModel) {
353 selectionModel.clear();
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.
362 * @param event the {@link CellPreviewEvent} that triggered selection
363 * @param action the action to handle
364 * @param selectionModel the {@link SelectionModel} to update
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);
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;
380 //if the row is selected then do nothing
381 if(selectionModel.isSelected(event.getValue())){
384 doMultiSelection(selectionModel, event.getDisplay(), event.getIndex(),
385 event.getValue(), action, shift, clearOthers);
387 else if ("click".equals(type)) {
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.
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;
400 doMultiSelection(selectionModel, event.getDisplay(), event.getIndex(),
401 event.getValue(), action, shift, clearOthers);
402 } else if ("keyup".equals(type)) {
403 int keyCode = nativeEvent.getKeyCode();
406 * Update selection when the space bar is pressed. The spacebar always
407 * toggles selection, regardless of whether the control key is pressed.
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;
415 doMultiSelection(selectionModel, event.getDisplay(), event.getIndex(),
416 event.getValue(), action, shift, clearOthers);
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.
426 * @param event the {@link CellPreviewEvent} that triggered selection
427 * @param action the action to handle
428 * @param selectionModel the {@link SelectionModel} to update
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) {
439 selectionModel.setSelected(value, true);
442 selectionModel.setSelected(value, false);
445 selectionModel.setSelected(value, !selectionModel.isSelected(value));
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));
459 selectionModel.setSelected(value, true);
461 } else if ("keyup".equals(type)) {
462 // Toggle selection on space.
463 int keyCode = nativeEvent.getKeyCode();
465 selectionModel.setSelected(value, !selectionModel.isSelected(value));
471 * Selects the given item, optionally clearing any prior selection.
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
478 protected void selectOne(MultiSelectionModel<? super T> selectionModel,
479 T target, boolean selected, boolean clearOthers) {
481 clearSelection(selectionModel);
483 selectionModel.setSelected(target, selected);
487 * Select or deselect a range of row indexes, optionally deselecting all other
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
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));
508 // Clear all other values.
510 clearSelection(selectionModel);
513 // Update the state of the values.
514 for (T value : toUpdate) {
515 selectionModel.setSelected(value, addToSelection);