Statistics
| Branch: | Tag: | Revision:

root / src / gr / ebs / gss / client / GSSSelectionEventManager.java @ 3f19a280

History | View | Annotate | Download (17.9 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 gr.ebs.gss.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.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;
41

    
42
/**
43
 * An implementation of {@link CellPreviewEvent.Handler} that adds selection
44
 * support via the spacebar and mouse clicks and handles the control key.
45
 * 
46
 * <p>
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.
51
 * </p>
52
 * 
53
 * @param <T> the data type of records in the list
54
 */
55
public class GSSSelectionEventManager<T> implements
56
    CellPreviewEvent.Handler<T> {
57

    
58
  /**
59
   * Implementation of {@link EventTranslator} that only triggers selection when
60
   * any checkbox is selected.
61
   * 
62
   * @param <T> the data type
63
   */
64
  public static class CheckboxEventTranslator<T> implements EventTranslator<T> {
65

    
66
    /**
67
     * The column index of the checkbox. Other columns are ignored.
68
     */
69
    private final int column;
70

    
71
    /**
72
     * Construct a new {@link CheckboxEventTranslator} that will trigger
73
     * selection when any checkbox in any column is selected.
74
     */
75
    public CheckboxEventTranslator() {
76
      this(-1);
77
    }
78

    
79
    /**
80
     * Construct a new {@link CheckboxEventTranslator} that will trigger
81
     * selection when a checkbox in the specified column is selected.
82
     * 
83
     * @param column the column index, or -1 for all columns
84
     */
85
    public CheckboxEventTranslator(int column) {
86
      this.column = column;
87
    }
88

    
89
    public boolean clearCurrentSelection(CellPreviewEvent<T> event) {
90
      return false;
91
    }
92

    
93
    public SelectAction translateSelectionEvent(CellPreviewEvent<T> event) {
94
      // Handle the 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;
100
        }
101

    
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(
109
                event.getValue()));
110
            return SelectAction.TOGGLE;
111
          }
112
        }
113
        return SelectAction.IGNORE;
114
      }
115

    
116
      // For keyboard events, do the default action.
117
      return SelectAction.DEFAULT;
118
    }
119
  }
120

    
121
  /**
122
   * Translates {@link CellPreviewEvent}s into {@link SelectAction}s.
123
   */
124
  public static interface EventTranslator<T> {
125
    /**
126
     * Check whether a user selection event should clear all currently selected
127
     * values.
128
     * 
129
     * @param event the {@link CellPreviewEvent} to translate
130
     */
131
    boolean clearCurrentSelection(CellPreviewEvent<T> event);
132

    
133
    /**
134
     * Translate the user selection event into a {@link SelectAction}.
135
     * 
136
     * @param event the {@link CellPreviewEvent} to translate
137
     */
138
    SelectAction translateSelectionEvent(CellPreviewEvent<T> event);
139
  }
140

    
141
  /**
142
   * The action that controls how selection is handled.
143
   */
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.
150
  }
151

    
152
  /**
153
   * Construct a new {@link GSSSelectionEventManager} that triggers
154
   * selection when any checkbox in any column is clicked.
155
   * 
156
   * @param <T> the data type of the display
157
   * @return a {@link GSSSelectionEventManager} instance
158
   */
159
  public static <T> GSSSelectionEventManager<T> createCheckboxManager() {
160
    return new GSSSelectionEventManager<T>(new CheckboxEventTranslator<T>());
161
  }
162

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

    
177
  /**
178
   * Create a new {@link GSSSelectionEventManager} using the specified
179
   * {@link EventTranslator} to control which {@link SelectAction} to take for
180
   * each event.
181
   * 
182
   * @param <T> the data type of the display
183
   * @param translator the {@link EventTranslator} to use
184
   * @return a {@link GSSSelectionEventManager} instance
185
   */
186
  public static <T> GSSSelectionEventManager<T> createCustomManager(
187
      EventTranslator<T> translator) {
188
    return new GSSSelectionEventManager<T>(translator);
189
  }
190

    
191
  /**
192
   * Create a new {@link GSSSelectionEventManager} that handles selection
193
   * via user interactions.
194
   * 
195
   * @param <T> the data type of the display
196
   * @return a new {@link GSSSelectionEventManager} instance
197
   */
198
  public static <T> GSSSelectionEventManager<T> createDefaultManager() {
199
    return new GSSSelectionEventManager<T>(null);
200
  }
201

    
202
  /**
203
   * The last {@link HasData} that was handled.
204
   */
205
  private HasData<T> lastDisplay;
206

    
207
  /**
208
   * The last page start.
209
   */
210
  private int lastPageStart;
211

    
212
  /**
213
   * The last selected row index.
214
   */
215
  private int lastSelectedIndex = -1;
216

    
217
  /**
218
   * A boolean indicating that the last shift selection was additive.
219
   */
220
  private boolean shiftAdditive;
221

    
222
  /**
223
   * The last place where the user clicked without holding shift. Multi
224
   * selections that use the shift key are rooted at the anchor.
225
   */
226
  private int shiftAnchor = -1;
227

    
228
  /**
229
   * The {@link EventTranslator} that controls how selection is handled.
230
   */
231
  private final EventTranslator<T> translator;
232

    
233
  /**
234
   * Construct a new {@link GSSSelectionEventManager} using the specified
235
   * {@link EventTranslator} to control which {@link SelectAction} to take for
236
   * each event.
237
   * 
238
   * @param translator the {@link EventTranslator} to use
239
   */
240
  protected GSSSelectionEventManager(EventTranslator<T> translator) {
241
    this.translator = translator;
242
  }
243

    
244
  /**
245
   * Update the selection model based on a user selection event.
246
   * 
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
253
   */
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) {
260
      switch (action) {
261
        case IGNORE:
262
          // Ignore selection.
263
          return;
264
        case SELECT:
265
          addToSelection = true;
266
          break;
267
        case DESELECT:
268
          addToSelection = false;
269
          break;
270
        case TOGGLE:
271
          addToSelection = !selectionModel.isSelected(rowValue);
272
          break;
273
      }
274
    }
275

    
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) {
280
      /*
281
       * Get the new shift bounds based on the existing shift anchor and the
282
       * selected row.
283
       */
284
      int start = Math.min(shiftAnchor, row); // Inclusive.
285
      int end = Math.max(shiftAnchor, row); // Inclusive.
286

    
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);
295
      } else {
296
        // Remember if we are adding or removing rows.
297
        shiftAdditive = addToSelection;
298
      }
299

    
300
      // Update the last selected row, but do not move the shift anchor.
301
      lastSelectedIndex = row;
302

    
303
      // Select the range.
304
      setRangeSelection(selectionModel, display, new Range(start, end - start
305
          + 1), shiftAdditive, clearOthers);
306
    } else {
307
      /*
308
       * If we are not selecting a range, save the last row and set the shift
309
       * anchor.
310
       */
311
      lastDisplay = display;
312
      lastPageStart = pageStart;
313
      lastSelectedIndex = row;
314
      shiftAnchor = row;
315
      selectOne(selectionModel, rowValue, addToSelection, clearOthers);
316
    }
317
  }
318

    
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()) {
322
      return;
323
    }
324

    
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) {
329
      return;
330
    }
331

    
332
    // Check for user defined actions.
333
    SelectAction action = (translator == null) ? SelectAction.DEFAULT
334
        : translator.translateSelectionEvent(event);
335

    
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);
341
    } else {
342
      // Use the standard handler.
343
      handleSelectionEvent(event, action, selectionModel);
344
    }
345
  }
346

    
347
  /**
348
   * Removes all items from the selection.
349
   * 
350
   * @param selectionModel the {@link MultiSelectionModel} to clear
351
   */
352
  protected void clearSelection(MultiSelectionModel<? super T> selectionModel) {
353
    selectionModel.clear();
354
  }
355

    
356
  /**
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.
361
   * 
362
   * @param event the {@link CellPreviewEvent} that triggered selection
363
   * @param action the action to handle
364
   * @param selectionModel the {@link SelectionModel} to update
365
   */
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
    if(rightclick){
372
            boolean shift = nativeEvent.getShiftKey();
373
        boolean ctrlOrMeta = nativeEvent.getCtrlKey() || nativeEvent.getMetaKey();
374
        boolean clearOthers = (translator == null) ? !ctrlOrMeta
375
            : translator.clearCurrentSelection(event);
376
        if (action == null || action == SelectAction.DEFAULT) {
377
          action = ctrlOrMeta ? SelectAction.TOGGLE : SelectAction.SELECT;
378
        }
379
        //if the row is selected then do nothing
380
        if(selectionModel.isSelected(event.getValue())){
381
                return;
382
        }
383
        doMultiSelection(selectionModel, event.getDisplay(), event.getIndex(),
384
            event.getValue(), action, shift, clearOthers);
385
    }
386
    else if ("click".equals(type)) {
387
      /*
388
       * Update selection on click. Selection is toggled only if the user
389
       * presses the ctrl key. If the user does not press the control key,
390
       * selection is additive.
391
       */
392
      boolean shift = nativeEvent.getShiftKey();
393
      boolean ctrlOrMeta = nativeEvent.getCtrlKey() || nativeEvent.getMetaKey();
394
      boolean clearOthers = (translator == null) ? !ctrlOrMeta
395
          : translator.clearCurrentSelection(event);
396
      if (action == null || action == SelectAction.DEFAULT) {
397
        action = ctrlOrMeta ? SelectAction.TOGGLE : SelectAction.SELECT;
398
      }
399
      doMultiSelection(selectionModel, event.getDisplay(), event.getIndex(),
400
          event.getValue(), action, shift, clearOthers);
401
      if(ctrlOrMeta){
402
              event.setCanceled(true);
403
      }
404
    } else if ("keyup".equals(type)) {
405
      int keyCode = nativeEvent.getKeyCode();
406
      if (keyCode == 32) {
407
        /*
408
         * Update selection when the space bar is pressed. The spacebar always
409
         * toggles selection, regardless of whether the control key is pressed.
410
         */
411
        boolean shift = nativeEvent.getShiftKey();
412
        boolean clearOthers = (translator == null) ? false
413
            : translator.clearCurrentSelection(event);
414
        if (action == null || action == SelectAction.DEFAULT) {
415
          action = SelectAction.TOGGLE;
416
        }
417
        doMultiSelection(selectionModel, event.getDisplay(), event.getIndex(),
418
            event.getValue(), action, shift, clearOthers);
419
      }
420
    }
421
  }
422

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

    
452
    // Handle default selection.
453
    NativeEvent nativeEvent = event.getNativeEvent();
454
    String type = nativeEvent.getType();
455
    if ("click".equals(type)) {
456
      if (nativeEvent.getCtrlKey() || nativeEvent.getMetaKey()) {
457
        // Toggle selection on ctrl+click.
458
        selectionModel.setSelected(value, !selectionModel.isSelected(value));
459
      } else {
460
        // Select on click.
461
        selectionModel.setSelected(value, true);
462
      }
463
    } else if ("keyup".equals(type)) {
464
      // Toggle selection on space.
465
      int keyCode = nativeEvent.getKeyCode();
466
      if (keyCode == 32) {
467
        selectionModel.setSelected(value, !selectionModel.isSelected(value));
468
      }
469
    }
470
  }
471

    
472
  /**
473
   * Selects the given item, optionally clearing any prior selection.
474
   * 
475
   * @param selectionModel the {@link MultiSelectionModel} to update
476
   * @param target the item to select
477
   * @param selected true to select, false to deselect
478
   * @param clearOthers true to clear all other selected items
479
   */
480
  protected void selectOne(MultiSelectionModel<? super T> selectionModel,
481
      T target, boolean selected, boolean clearOthers) {
482
    if (clearOthers) {
483
      clearSelection(selectionModel);
484
    }
485
    selectionModel.setSelected(target, selected);
486
  }
487

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

    
514
    // Update the state of the values.
515
    for (T value : toUpdate) {
516
      selectionModel.setSelected(value, addToSelection);
517
    }
518
  }
519
}
520