Statistics
| Branch: | Tag: | Revision:

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

History | View | Annotate | Download (21.3 kB)

1
/*
2
 * Copyright 2011-2013 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 com.google.gwt.dom.client.Element;
40
import com.google.gwt.dom.client.InputElement;
41
import com.google.gwt.dom.client.NativeEvent;
42
import com.google.gwt.view.client.*;
43

    
44
import java.util.ArrayList;
45
import java.util.List;
46
import java.util.Set;
47

    
48
/**
49
 * An implementation of {@link com.google.gwt.view.client.CellPreviewEvent.Handler} that adds selection
50
 * support via the spacebar and mouse clicks and handles the control key.
51
 * 
52
 * <p>
53
 * If the {@link HasData} source of the selection event uses a
54
 * {@link MultiSelectionModel}, this manager additionally provides support for
55
 * shift key to select a range of values. For all other {@link SelectionModel}s,
56
 * only the control key is supported.
57
 * </p>
58
 * 
59
 * @param <T> the data type of records in the list
60
 */
61
public class PithosSelectionEventManager<T> implements
62
    CellPreviewEvent.Handler<T> {
63

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

    
72
    /**
73
     * The column index of the checkbox. Other columns are ignored.
74
     */
75
    private final int column;
76

    
77
    /**
78
     * Construct a new {@link gr.grnet.pithos.web.client.PithosSelectionEventManager.CheckboxEventTranslator} that will trigger
79
     * selection when any checkbox in any column is selected.
80
     */
81
    public CheckboxEventTranslator() {
82
      this(-1);
83
    }
84

    
85
    /**
86
     * Construct a new {@link gr.grnet.pithos.web.client.PithosSelectionEventManager.CheckboxEventTranslator} that will trigger
87
     * selection when a checkbox in the specified column is selected.
88
     * 
89
     * @param column the column index, or -1 for all columns
90
     */
91
    public CheckboxEventTranslator(int column) {
92
      this.column = column;
93
    }
94

    
95
    @Override
96
        public boolean clearCurrentSelection(CellPreviewEvent<T> event) {
97
      return false;
98
    }
99

    
100
    @Override
101
        public SelectAction translateSelectionEvent(CellPreviewEvent<T> event) {
102
      // Handle the event.
103
      NativeEvent nativeEvent = event.getNativeEvent();
104
      if ("click".equals(nativeEvent.getType())) {
105
        // Ignore if the event didn't occur in the correct column.
106
        if (column > -1 && column != event.getColumn()) {
107
            Pithos.LOG("PithosSelectionEventManager::translateSelectionEvent(), IGNORE because not same column: ", 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
              Pithos.LOG("PithosSelectionEventManager::translateSelectionEvent(), TOGGLE: synchronizing checkbox with current selection");
120
            return SelectAction.TOGGLE;
121
          }
122
        }
123
          Pithos.LOG("PithosSelectionEventManager::translateSelectionEvent(), IGNORE");
124
        return SelectAction.IGNORE;
125
      }
126

    
127
      // For keyboard events, do the default action.
128
        Pithos.LOG("PithosSelectionEventManager::translateSelectionEvent(), DEFAULT, since keyboard event");
129
      return SelectAction.DEFAULT;
130
    }
131
  }
132

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

    
334
  @Override
335
public void onCellPreview(CellPreviewEvent<T> event) {
336
    // Early exit if selection is already handled or we are editing.
337
    if (event.isCellEditing() || event.isSelectionHandled()) {
338
      return;
339
    }
340

    
341
    // Early exit if we do not have a SelectionModel.
342
    HasData<T> display = event.getDisplay();
343
    SelectionModel<? super T> selectionModel = display.getSelectionModel();
344
    if (selectionModel == null) {
345
      return;
346
    }
347

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

    
352
    // Handle the event based on the SelectionModel type.
353
    if (selectionModel instanceof MultiSelectionModel) {
354
      // Add shift key support for MultiSelectionModel.
355
      handleMultiSelectionEvent(event, action,
356
          (MultiSelectionModel<? super T>) selectionModel);
357
    } else {
358
      // Use the standard handler.
359
      handleSelectionEvent(event, action, selectionModel);
360
    }
361
  }
362

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

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

    
445
  /**
446
   * Handle an event that could cause a value to be selected. This method works
447
   * for any {@link SelectionModel}. Pressing the space bar or ctrl+click will
448
   * toggle the selection state. Clicking selects the row if it is not selected.
449
   * 
450
   * @param event the {@link CellPreviewEvent} that triggered selection
451
   * @param action the action to handle
452
   * @param selectionModel the {@link SelectionModel} to update
453
   */
454
  protected void handleSelectionEvent(CellPreviewEvent<T> event,
455
      SelectAction action, SelectionModel<? super T> selectionModel) {
456
    // Handle selection overrides.
457
    T value = event.getValue();
458
    if (action != null) {
459
      switch (action) {
460
        case IGNORE:
461
          return;
462
        case SELECT:
463
          selectionModel.setSelected(value, true);
464
          return;
465
        case DESELECT:
466
          selectionModel.setSelected(value, false);
467
          return;
468
        case TOGGLE:
469
          selectionModel.setSelected(value, !selectionModel.isSelected(value));
470
          return;
471
                case DEFAULT:
472
                        break;
473
      }
474
    }
475

    
476
    // Handle default selection.
477
    NativeEvent nativeEvent = event.getNativeEvent();
478
    String type = nativeEvent.getType();
479
    if ("click".equals(type)) {
480
      if (nativeEvent.getCtrlKey() || nativeEvent.getMetaKey()) {
481
        // Toggle selection on ctrl+click.
482
        selectionModel.setSelected(value, !selectionModel.isSelected(value));
483
      } else {
484
        // Select on click.
485
        selectionModel.setSelected(value, true);
486
      }
487
    } else if ("keyup".equals(type)) {
488
      // Toggle selection on space.
489
      int keyCode = nativeEvent.getKeyCode();
490
      if (keyCode == 32) {
491
        selectionModel.setSelected(value, !selectionModel.isSelected(value));
492
      }
493
    }
494
  }
495

    
496
  /**
497
   * Selects the given item, optionally clearing any prior selection.
498
   * 
499
   * @param selectionModel the {@link MultiSelectionModel} to update
500
   * @param target the item to select
501
   * @param selected true to select, false to deselect
502
   * @param clearOthers true to clear all other selected items
503
   */
504
  protected void selectOne(
505
      MultiSelectionModel<? super T> selectionModel,
506
      T target,
507
      boolean selected,
508
      boolean clearOthers
509
  ) {
510
      Pithos.LOG("PithosSelectionEventManager::selectOne(target=", target, ", selected=", selected, ", clearOthers=", clearOthers, ")");
511
      if(clearOthers) {
512
          Pithos.LOG("PithosSelectionEventManager::selectOne(...), before clearSelection");
513
          Set<? super T> selectedSetBefore = selectionModel.getSelectedSet();
514
          for(Object selectedObj : selectedSetBefore) {
515
              Pithos.LOG("  ==> selected: ", selectedObj);
516
          }
517

    
518
          clearSelection(selectionModel);
519

    
520
          Pithos.LOG("PithosSelectionEventManager::selectOne(...), after clearSelection");
521
          Set<? super T> selectedSetAfter = selectionModel.getSelectedSet();
522
          for(Object selectedObj : selectedSetAfter) {
523
              Pithos.LOG("  ==> selected: ", selectedObj);
524
          }
525
      }
526

    
527
      selectionModel.setSelected(target, selected);
528
      Pithos.LOG("PithosSelectionEventManager::selectOne(...), after selectionModel.setSelected(target, selected)");
529
      Set<? super T> selectedSetAfter2 = selectionModel.getSelectedSet();
530
      for(Object selectedObj : selectedSetAfter2) {
531
          Pithos.LOG("  ==> selected: ", selectedObj);
532
      }
533
  }
534

    
535
  /**
536
   * Select or deselect a range of row indexes, optionally deselecting all other
537
   * values.
538
   * 
539
   * @param selectionModel the {@link MultiSelectionModel} to update
540
   * @param display the {@link HasData} source of the selection event
541
   * @param range the {@link Range} of rows to select or deselect
542
   * @param addToSelection true to select, false to deselect the range
543
   * @param clearOthers true to deselect rows not in the range
544
   */
545
  protected void setRangeSelection(
546
      MultiSelectionModel<? super T> selectionModel, HasData<T> display,
547
      Range range, boolean addToSelection, boolean clearOthers) {
548
    // Get the list of values to select.
549
    List<T> toUpdate = new ArrayList<T>();
550
    int start = range.getStart();
551
    int end = start + range.getLength();
552
    for (int i = start; i < end ; i++) {
553
             toUpdate.add(display.getVisibleItem(i-display.getVisibleRange().getStart()));
554
        }
555
    // Clear all other values.
556
    if (clearOthers) {
557
      clearSelection(selectionModel);
558
    }
559

    
560
    // Update the state of the values.
561
    for (T value : toUpdate) {
562
      selectionModel.setSelected(value, addToSelection);
563
    }
564
  }
565
}
566