Statistics
| Branch: | Tag: | Revision:

root / src / org / gss_project / gss / web / client / GSSSelectionEventManager.java @ 83c3bc8e

History | View | Annotate | Download (17.8 kB)

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 org.gss_project.gss.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