Statistics
| Branch: | Tag: | Revision:

root / web_client / src / gr / grnet / pithos / web / client / GSSSelectionEventManager.java @ 58777026

History | View | Annotate | Download (18.6 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 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 GSSSelectionEventManager<T> implements
65
    CellPreviewEvent.Handler<T> {
66

    
67
  /**
68
   * Implementation of {@link 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 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 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
    public boolean clearCurrentSelection(CellPreviewEvent<T> event) {
99
      return false;
100
    }
101

    
102
    public SelectAction translateSelectionEvent(CellPreviewEvent<T> event) {
103
      // Handle the event.
104
      NativeEvent nativeEvent = event.getNativeEvent();
105
      if ("click".equals(nativeEvent.getType())) {
106
        // Ignore if the event didn't occur in the correct column.
107
        if (column > -1 && column != event.getColumn()) {
108
          return SelectAction.IGNORE;
109
        }
110

    
111
        // Determine if we clicked on a checkbox.
112
        Element target = nativeEvent.getEventTarget().cast();
113
        if ("input".equals(target.getTagName().toLowerCase())) {
114
          final InputElement input = target.cast();
115
          if ("checkbox".equals(input.getType().toLowerCase())) {
116
            // Synchronize the checkbox with the current selection state.
117
            input.setChecked(event.getDisplay().getSelectionModel().isSelected(
118
                event.getValue()));
119
            return SelectAction.TOGGLE;
120
          }
121
        }
122
        return SelectAction.IGNORE;
123
      }
124

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

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

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

    
150
  /**
151
   * The action that controls how selection is handled.
152
   */
153
  public static enum SelectAction {
154
    DEFAULT, // Perform the default action.
155
    SELECT, // Select the value.
156
    DESELECT, // Deselect the value.
157
    TOGGLE, // Toggle the selected state of the value.
158
    IGNORE; // Ignore the event.
159
  }
160

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

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

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

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

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

    
216
  /**
217
   * The last page start.
218
   */
219
  private int lastPageStart;
220

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

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

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

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

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

    
253
  /**
254
   * Update the selection model based on a user selection event.
255
   * 
256
   * @param selectionModel the selection model to update
257
   * @param row the selected row index relative to the page start
258
   * @param rowValue the selected row value
259
   * @param action the {@link SelectAction} to apply
260
   * @param selectRange true to select the range from the last selected row
261
   * @param clearOthers true to clear the current selection
262
   */
263
  public void doMultiSelection(MultiSelectionModel<? super T> selectionModel,
264
      HasData<T> display, int row, T rowValue, SelectAction action,
265
      boolean selectRange, boolean clearOthers) {
266
    // Determine if we will add or remove selection.
267
    boolean addToSelection = true;
268
    if (action != null) {
269
      switch (action) {
270
        case IGNORE:
271
          // Ignore selection.
272
          return;
273
        case SELECT:
274
          addToSelection = true;
275
          break;
276
        case DESELECT:
277
          addToSelection = false;
278
          break;
279
        case TOGGLE:
280
          addToSelection = !selectionModel.isSelected(rowValue);
281
          break;
282
      }
283
    }
284

    
285
    // Determine which rows will be newly selected.
286
    int pageStart = display.getVisibleRange().getStart();
287
    if (selectRange && pageStart == lastPageStart && lastSelectedIndex > -1
288
        && shiftAnchor > -1 && display == lastDisplay) {
289
      /*
290
       * Get the new shift bounds based on the existing shift anchor and the
291
       * selected row.
292
       */
293
      int start = Math.min(shiftAnchor, row); // Inclusive.
294
      int end = Math.max(shiftAnchor, row); // Inclusive.
295

    
296
      if (lastSelectedIndex < start) {
297
        // Revert previous selection if the user reselects a smaller range.
298
        setRangeSelection(selectionModel, display, new Range(lastSelectedIndex,
299
            start - lastSelectedIndex), !shiftAdditive, false);
300
      } else if (lastSelectedIndex > end) {
301
        // Revert previous selection if the user reselects a smaller range.
302
        setRangeSelection(selectionModel, display, new Range(end + 1,
303
            lastSelectedIndex - end), !shiftAdditive, false);
304
      } else {
305
        // Remember if we are adding or removing rows.
306
        shiftAdditive = addToSelection;
307
      }
308

    
309
      // Update the last selected row, but do not move the shift anchor.
310
      lastSelectedIndex = row;
311

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

    
328
  public void onCellPreview(CellPreviewEvent<T> event) {
329
    // Early exit if selection is already handled or we are editing.
330
    if (event.isCellEditing() || event.isSelectionHandled()) {
331
      return;
332
    }
333

    
334
    // Early exit if we do not have a SelectionModel.
335
    HasData<T> display = event.getDisplay();
336
    SelectionModel<? super T> selectionModel = display.getSelectionModel();
337
    if (selectionModel == null) {
338
      return;
339
    }
340

    
341
    // Check for user defined actions.
342
    SelectAction action = (translator == null) ? SelectAction.DEFAULT
343
        : translator.translateSelectionEvent(event);
344

    
345
    // Handle the event based on the SelectionModel type.
346
    if (selectionModel instanceof MultiSelectionModel) {
347
      // Add shift key support for MultiSelectionModel.
348
      handleMultiSelectionEvent(event, action,
349
          (MultiSelectionModel<? super T>) selectionModel);
350
    } else {
351
      // Use the standard handler.
352
      handleSelectionEvent(event, action, selectionModel);
353
    }
354
  }
355

    
356
  /**
357
   * Removes all items from the selection.
358
   * 
359
   * @param selectionModel the {@link MultiSelectionModel} to clear
360
   */
361
  protected void clearSelection(MultiSelectionModel<? super T> selectionModel) {
362
    selectionModel.clear();
363
  }
364

    
365
  /**
366
   * Handle an event that could cause a value to be selected for a
367
   * {@link MultiSelectionModel}. This overloaded method adds support for both
368
   * the control and shift keys. If the shift key is held down, all rows between
369
   * the previous selected row and the current row are selected.
370
   * 
371
   * @param event the {@link CellPreviewEvent} that triggered selection
372
   * @param action the action to handle
373
   * @param selectionModel the {@link SelectionModel} to update
374
   */
375
  protected void handleMultiSelectionEvent(CellPreviewEvent<T> event,
376
      SelectAction action, MultiSelectionModel<? super T> selectionModel) {
377
    NativeEvent nativeEvent = event.getNativeEvent();
378
    String type = nativeEvent.getType();
379
    boolean rightclick = "mousedown".equals(type) && nativeEvent.getButton()==NativeEvent.BUTTON_RIGHT;
380
    if(rightclick){
381
            boolean shift = nativeEvent.getShiftKey();
382
        boolean ctrlOrMeta = nativeEvent.getCtrlKey() || nativeEvent.getMetaKey();
383
        boolean clearOthers = (translator == null) ? !ctrlOrMeta
384
            : translator.clearCurrentSelection(event);
385
        if (action == null || action == SelectAction.DEFAULT) {
386
          action = ctrlOrMeta ? SelectAction.TOGGLE : SelectAction.SELECT;
387
        }
388
        //if the row is selected then do nothing
389
        if(selectionModel.isSelected(event.getValue())){
390
                return;
391
        }
392
        doMultiSelection(selectionModel, event.getDisplay(), event.getIndex(),
393
            event.getValue(), action, shift, clearOthers);
394
    }
395
    else if ("click".equals(type)) {
396
      /*
397
       * Update selection on click. Selection is toggled only if the user
398
       * presses the ctrl key. If the user does not press the control key,
399
       * selection is additive.
400
       */
401
      boolean shift = nativeEvent.getShiftKey();
402
      boolean ctrlOrMeta = nativeEvent.getCtrlKey() || nativeEvent.getMetaKey();
403
      boolean clearOthers = (translator == null) ? !ctrlOrMeta
404
          : translator.clearCurrentSelection(event);
405
      if (action == null || action == SelectAction.DEFAULT) {
406
        action = ctrlOrMeta ? SelectAction.TOGGLE : SelectAction.SELECT;
407
      }
408
      doMultiSelection(selectionModel, event.getDisplay(), event.getIndex(),
409
          event.getValue(), action, shift, clearOthers);
410
      if(ctrlOrMeta){
411
              event.setCanceled(true);
412
      }
413
    } else if ("keyup".equals(type)) {
414
      int keyCode = nativeEvent.getKeyCode();
415
      if (keyCode == 32) {
416
        /*
417
         * Update selection when the space bar is pressed. The spacebar always
418
         * toggles selection, regardless of whether the control key is pressed.
419
         */
420
        boolean shift = nativeEvent.getShiftKey();
421
        boolean clearOthers = (translator == null) ? false
422
            : translator.clearCurrentSelection(event);
423
        if (action == null || action == SelectAction.DEFAULT) {
424
          action = SelectAction.TOGGLE;
425
        }
426
        doMultiSelection(selectionModel, event.getDisplay(), event.getIndex(),
427
            event.getValue(), action, shift, clearOthers);
428
      }
429
    }
430
  }
431

    
432
  /**
433
   * Handle an event that could cause a value to be selected. This method works
434
   * for any {@link SelectionModel}. Pressing the space bar or ctrl+click will
435
   * toggle the selection state. Clicking selects the row if it is not selected.
436
   * 
437
   * @param event the {@link CellPreviewEvent} that triggered selection
438
   * @param action the action to handle
439
   * @param selectionModel the {@link SelectionModel} to update
440
   */
441
  protected void handleSelectionEvent(CellPreviewEvent<T> event,
442
      SelectAction action, SelectionModel<? super T> selectionModel) {
443
    // Handle selection overrides.
444
    T value = event.getValue();
445
    if (action != null) {
446
      switch (action) {
447
        case IGNORE:
448
          return;
449
        case SELECT:
450
          selectionModel.setSelected(value, true);
451
          return;
452
        case DESELECT:
453
          selectionModel.setSelected(value, false);
454
          return;
455
        case TOGGLE:
456
          selectionModel.setSelected(value, !selectionModel.isSelected(value));
457
          return;
458
      }
459
    }
460

    
461
    // Handle default selection.
462
    NativeEvent nativeEvent = event.getNativeEvent();
463
    String type = nativeEvent.getType();
464
    if ("click".equals(type)) {
465
      if (nativeEvent.getCtrlKey() || nativeEvent.getMetaKey()) {
466
        // Toggle selection on ctrl+click.
467
        selectionModel.setSelected(value, !selectionModel.isSelected(value));
468
      } else {
469
        // Select on click.
470
        selectionModel.setSelected(value, true);
471
      }
472
    } else if ("keyup".equals(type)) {
473
      // Toggle selection on space.
474
      int keyCode = nativeEvent.getKeyCode();
475
      if (keyCode == 32) {
476
        selectionModel.setSelected(value, !selectionModel.isSelected(value));
477
      }
478
    }
479
  }
480

    
481
  /**
482
   * Selects the given item, optionally clearing any prior selection.
483
   * 
484
   * @param selectionModel the {@link MultiSelectionModel} to update
485
   * @param target the item to select
486
   * @param selected true to select, false to deselect
487
   * @param clearOthers true to clear all other selected items
488
   */
489
  protected void selectOne(MultiSelectionModel<? super T> selectionModel,
490
      T target, boolean selected, boolean clearOthers) {
491
    if (clearOthers) {
492
      clearSelection(selectionModel);
493
    }
494
    selectionModel.setSelected(target, selected);
495
  }
496

    
497
  /**
498
   * Select or deselect a range of row indexes, optionally deselecting all other
499
   * values.
500
   * 
501
   * @param selectionModel the {@link MultiSelectionModel} to update
502
   * @param display the {@link HasData} source of the selection event
503
   * @param range the {@link Range} of rows to select or deselect
504
   * @param addToSelection true to select, false to deselect the range
505
   * @param clearOthers true to deselect rows not in the range
506
   */
507
  protected void setRangeSelection(
508
      MultiSelectionModel<? super T> selectionModel, HasData<T> display,
509
      Range range, boolean addToSelection, boolean clearOthers) {
510
    // Get the list of values to select.
511
    List<T> toUpdate = new ArrayList<T>();
512
    int itemCount = display.getVisibleItemCount();
513
    int start = range.getStart();
514
    int end = start + range.getLength();
515
    for (int i = start; i < end ; i++) {
516
             toUpdate.add(display.getVisibleItem(i-display.getVisibleRange().getStart()));
517
        }
518
    // Clear all other values.
519
    if (clearOthers) {
520
      clearSelection(selectionModel);
521
    }
522

    
523
    // Update the state of the values.
524
    for (T value : toUpdate) {
525
      selectionModel.setSelected(value, addToSelection);
526
    }
527
  }
528
}
529