Statistics
| Branch: | Tag: | Revision:

root / src / gr / grnet / pithos / web / client / PithosSelectionEventManager.java @ ebead1b5

History | View | Annotate | Download (18.9 kB)

1
/*
2
 * Copyright 2011 GRNET S.A. All rights reserved.
3
 *
4
 * Redistribution and use in source and binary forms, with or
5
 * without modification, are permitted provided that the following
6
 * conditions are met:
7
 *
8
 *   1. Redistributions of source code must retain the above
9
 *      copyright notice, this list of conditions and the following
10
 *      disclaimer.
11
 *
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.
16
 *
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.
29
 *
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.
34
 */
35
package gr.grnet.pithos.web.client;
36

    
37

    
38

    
39
import java.util.ArrayList;
40
import java.util.List;
41

    
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;
50

    
51
/**
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.
54
 * 
55
 * <p>
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.
60
 * </p>
61
 * 
62
 * @param <T> the data type of records in the list
63
 */
64
public class PithosSelectionEventManager<T> implements
65
    CellPreviewEvent.Handler<T> {
66

    
67
  /**
68
   * Implementation of {@link gr.grnet.pithos.web.client.PithosSelectionEventManager.EventTranslator} that only triggers selection when
69
   * any checkbox is selected.
70
   * 
71
   * @param <T> the data type
72
   */
73
  public static class CheckboxEventTranslator<T> implements EventTranslator<T> {
74

    
75
    /**
76
     * The column index of the checkbox. Other columns are ignored.
77
     */
78
    private final int column;
79

    
80
    /**
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.
83
     */
84
    public CheckboxEventTranslator() {
85
      this(-1);
86
    }
87

    
88
    /**
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.
91
     * 
92
     * @param column the column index, or -1 for all columns
93
     */
94
    public CheckboxEventTranslator(int column) {
95
      this.column = column;
96
    }
97

    
98
    @Override
99
        public boolean clearCurrentSelection(CellPreviewEvent<T> event) {
100
      return false;
101
    }
102

    
103
    @Override
104
        public SelectAction translateSelectionEvent(CellPreviewEvent<T> event) {
105
      // Handle the 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;
111
        }
112

    
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(
120
                event.getValue()));
121
            return SelectAction.TOGGLE;
122
          }
123
        }
124
        return SelectAction.IGNORE;
125
      }
126

    
127
      // For keyboard events, do the default action.
128
      return SelectAction.DEFAULT;
129
    }
130
  }
131

    
132
  /**
133
   * Translates {@link CellPreviewEvent}s into {@link SelectAction}s.
134
   */
135
  public static interface EventTranslator<T> {
136
    /**
137
     * Check whether a user selection event should clear all currently selected
138
     * values.
139
     * 
140
     * @param event the {@link CellPreviewEvent} to translate
141
     */
142
    boolean clearCurrentSelection(CellPreviewEvent<T> event);
143

    
144
    /**
145
     * Translate the user selection event into a {@link SelectAction}.
146
     * 
147
     * @param event the {@link CellPreviewEvent} to translate
148
     */
149
    SelectAction translateSelectionEvent(CellPreviewEvent<T> event);
150
  }
151

    
152
  /**
153
   * The action that controls how selection is handled.
154
   */
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.
161
  }
162

    
163
  /**
164
   * Construct a new {@link PithosSelectionEventManager} that triggers
165
   * selection when any checkbox in any column is clicked.
166
   * 
167
   * @param <T> the data type of the display
168
   * @return a {@link PithosSelectionEventManager} instance
169
   */
170
  public static <T> PithosSelectionEventManager<T> createCheckboxManager() {
171
    return new PithosSelectionEventManager<T>(new CheckboxEventTranslator<T>());
172
  }
173

    
174
  /**
175
   * Construct a new {@link PithosSelectionEventManager} that triggers
176
   * selection when a checkbox in the specified column is clicked.
177
   * 
178
   * @param <T> the data type of the display
179
   * @param column the column to handle
180
   * @return a {@link PithosSelectionEventManager} instance
181
   */
182
  public static <T> PithosSelectionEventManager<T> createCheckboxManager(
183
      int column) {
184
    return new PithosSelectionEventManager<T>(new CheckboxEventTranslator<T>(
185
        column));
186
  }
187

    
188
  /**
189
   * Create a new {@link PithosSelectionEventManager} using the specified
190
   * {@link EventTranslator} to control which {@link SelectAction} to take for
191
   * each event.
192
   * 
193
   * @param <T> the data type of the display
194
   * @param translator the {@link EventTranslator} to use
195
   * @return a {@link PithosSelectionEventManager} instance
196
   */
197
  public static <T> PithosSelectionEventManager<T> createCustomManager(
198
      EventTranslator<T> translator) {
199
    return new PithosSelectionEventManager<T>(translator);
200
  }
201

    
202
  /**
203
   * Create a new {@link PithosSelectionEventManager} that handles selection
204
   * via user interactions.
205
   * 
206
   * @param <T> the data type of the display
207
   * @return a new {@link PithosSelectionEventManager} instance
208
   */
209
  public static <T> PithosSelectionEventManager<T> createDefaultManager() {
210
    return new PithosSelectionEventManager<T>(null);
211
  }
212

    
213
  /**
214
   * The last {@link HasData} that was handled.
215
   */
216
  private HasData<T> lastDisplay;
217

    
218
  /**
219
   * The last page start.
220
   */
221
  private int lastPageStart;
222

    
223
  /**
224
   * The last selected row index.
225
   */
226
  private int lastSelectedIndex = -1;
227

    
228
  /**
229
   * A boolean indicating that the last shift selection was additive.
230
   */
231
  private boolean shiftAdditive;
232

    
233
  /**
234
   * The last place where the user clicked without holding shift. Multi
235
   * selections that use the shift key are rooted at the anchor.
236
   */
237
  private int shiftAnchor = -1;
238

    
239
  /**
240
   * The {@link EventTranslator} that controls how selection is handled.
241
   */
242
  private final EventTranslator<T> translator;
243

    
244
  /**
245
   * Construct a new {@link PithosSelectionEventManager} using the specified
246
   * {@link EventTranslator} to control which {@link SelectAction} to take for
247
   * each event.
248
   * 
249
   * @param translator the {@link EventTranslator} to use
250
   */
251
  protected PithosSelectionEventManager(EventTranslator<T> translator) {
252
    this.translator = translator;
253
  }
254

    
255
  /**
256
   * Update the selection model based on a user selection event.
257
   * 
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
264
   */
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) {
271
      switch (action) {
272
        case IGNORE:
273
          // Ignore selection.
274
          return;
275
        case SELECT:
276
          addToSelection = true;
277
          break;
278
        case DESELECT:
279
          addToSelection = false;
280
          break;
281
        case TOGGLE:
282
          addToSelection = !selectionModel.isSelected(rowValue);
283
          break;
284
        case DEFAULT:
285
          break;
286
      }
287
    }
288

    
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) {
293
      /*
294
       * Get the new shift bounds based on the existing shift anchor and the
295
       * selected row.
296
       */
297
      int start = Math.min(shiftAnchor, row); // Inclusive.
298
      int end = Math.max(shiftAnchor, row); // Inclusive.
299

    
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);
308
      } else {
309
        // Remember if we are adding or removing rows.
310
        shiftAdditive = addToSelection;
311
      }
312

    
313
      // Update the last selected row, but do not move the shift anchor.
314
      lastSelectedIndex = row;
315

    
316
      // Select the range.
317
      setRangeSelection(selectionModel, display, new Range(start, end - start
318
          + 1), shiftAdditive, clearOthers);
319
    } else {
320
      /*
321
       * If we are not selecting a range, save the last row and set the shift
322
       * anchor.
323
       */
324
      lastDisplay = display;
325
      lastPageStart = pageStart;
326
      lastSelectedIndex = row;
327
      shiftAnchor = row;
328
      selectOne(selectionModel, rowValue, addToSelection, clearOthers);
329
    }
330
  }
331

    
332
  @Override
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()) {
336
      return;
337
    }
338

    
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) {
343
      return;
344
    }
345

    
346
    // Check for user defined actions.
347
    SelectAction action = (translator == null) ? SelectAction.DEFAULT
348
        : translator.translateSelectionEvent(event);
349

    
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);
355
    } else {
356
      // Use the standard handler.
357
      handleSelectionEvent(event, action, selectionModel);
358
    }
359
  }
360

    
361
  /**
362
   * Removes all items from the selection.
363
   * 
364
   * @param selectionModel the {@link MultiSelectionModel} to clear
365
   */
366
  protected void clearSelection(MultiSelectionModel<? super T> selectionModel) {
367
    selectionModel.clear();
368
  }
369

    
370
  /**
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.
375
   * 
376
   * @param event the {@link CellPreviewEvent} that triggered selection
377
   * @param action the action to handle
378
   * @param selectionModel the {@link SelectionModel} to update
379
   */
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;
386
    if(rightclick){
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;
393
        }
394
        //if the row is selected then do nothing
395
        if(selectionModel.isSelected(event.getValue())){
396
                return;
397
        }
398
        doMultiSelection(selectionModel, event.getDisplay(), event.getIndex(),
399
            event.getValue(), action1, shift, clearOthers);
400
    }
401
    else if ("click".equals(type)) {
402
      /*
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.
406
       */
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;
413
      }
414
      doMultiSelection(selectionModel, event.getDisplay(), event.getIndex(),
415
          event.getValue(), action1, shift, clearOthers);
416
      if(ctrlOrMeta){
417
              event.setCanceled(true);
418
      }
419
    } else if ("keyup".equals(type)) {
420
      int keyCode = nativeEvent.getKeyCode();
421
      if (keyCode == 32) {
422
        /*
423
         * Update selection when the space bar is pressed. The spacebar always
424
         * toggles selection, regardless of whether the control key is pressed.
425
         */
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;
431
        }
432
        doMultiSelection(selectionModel, event.getDisplay(), event.getIndex(),
433
            event.getValue(), action1, shift, clearOthers);
434
      }
435
    }
436
  }
437

    
438
  /**
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.
442
   * 
443
   * @param event the {@link CellPreviewEvent} that triggered selection
444
   * @param action the action to handle
445
   * @param selectionModel the {@link SelectionModel} to update
446
   */
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) {
452
      switch (action) {
453
        case IGNORE:
454
          return;
455
        case SELECT:
456
          selectionModel.setSelected(value, true);
457
          return;
458
        case DESELECT:
459
          selectionModel.setSelected(value, false);
460
          return;
461
        case TOGGLE:
462
          selectionModel.setSelected(value, !selectionModel.isSelected(value));
463
          return;
464
                case DEFAULT:
465
                        break;
466
      }
467
    }
468

    
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));
476
      } else {
477
        // Select on click.
478
        selectionModel.setSelected(value, true);
479
      }
480
    } else if ("keyup".equals(type)) {
481
      // Toggle selection on space.
482
      int keyCode = nativeEvent.getKeyCode();
483
      if (keyCode == 32) {
484
        selectionModel.setSelected(value, !selectionModel.isSelected(value));
485
      }
486
    }
487
  }
488

    
489
  /**
490
   * Selects the given item, optionally clearing any prior selection.
491
   * 
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
496
   */
497
  protected void selectOne(MultiSelectionModel<? super T> selectionModel,
498
      T target, boolean selected, boolean clearOthers) {
499
    if (clearOthers) {
500
      clearSelection(selectionModel);
501
    }
502
    selectionModel.setSelected(target, selected);
503
  }
504

    
505
  /**
506
   * Select or deselect a range of row indexes, optionally deselecting all other
507
   * values.
508
   * 
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
514
   */
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()));
524
        }
525
    // Clear all other values.
526
    if (clearOthers) {
527
      clearSelection(selectionModel);
528
    }
529

    
530
    // Update the state of the values.
531
    for (T value : toUpdate) {
532
      selectionModel.setSelected(value, addToSelection);
533
    }
534
  }
535
}
536