summaryrefslogtreecommitdiffstats
path: root/mailnews/base/util/jsTreeSelection.js
blob: 0ba5ea42c7fc99289b3e61f46c35ac4d0ae42718 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
/* This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this
 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */

this.EXPORTED_SYMBOLS = ['JSTreeSelection'];

var Cc = Components.classes;
var Ci = Components.interfaces;
var Cr = Components.results;
var Cu = Components.utils;

Components.utils.import("resource://gre/modules/XPCOMUtils.jsm");

/**
 * Partial nsITreeSelection implementation so that we can have nsMsgDBViews that
 *  exist only for message display but do not need to be backed by a full
 *  tree view widget.  This could also hopefully be used for more xpcshell unit
 *  testing of the FolderDisplayWidget.  It might also be useful for creating
 *  transient selections when right-click selection happens.
 *
 * Our current limitations:
 * - We do not support any single selection modes.  This is mainly because we
 *   need to look at the box object for that and we don't want to do it.
 * - Timed selection.  Our expected consumers don't use it.
 *
 * Our current laziness:
 * - We aren't very precise about invalidation when it would be potentially
 *   complicated.  The theory is that if there is a tree box object, it's
 *   probably native and the XPConnect overhead is probably a lot more than
 *   any potential savings, at least for now when the tree display is
 *   generally C++ XPCOM backed rather than JS XPCOM backed.  Also, we
 *   aren't intended to actually be used with a real tree display; you should
 *   be using the C++ object in that case!
 *
 * If documentation is omitted for something, it is because we have little to
 *  add to the documentation of nsITreeSelection and really hope that our
 *  documentation tool will copy-down that documentation.
 *
 * This implementation attempts to mimic the behavior of nsTreeSelection.  In
 *  a few cases, this leads to potentially confusing actions.  I attempt to note
 *  when we are doing this and why we do it.
 *
 * Unit test is in mailnews/base/util/test_jsTreeSelection.js
 */
function JSTreeSelection(aTreeBoxObject) {
  this._treeBoxObject = aTreeBoxObject;

  this._currentIndex = null;
  this._shiftSelectPivot = null;
  this._ranges = [];
  this._count = 0;

  this._selectEventsSuppressed = false;
}
JSTreeSelection.prototype = {
  /**
   * The current nsITreeBoxObject, appropriately QueryInterfaced.  May be null.
   */
  _treeBoxObject: null,

  /**
   * Where the focus rectangle (that little dotted thing) shows up.  Just
   *  because something is focused does not mean it is actually selected.
   */
  _currentIndex: null,
  /**
   * The view index where the shift is anchored when it is not (conceptually)
   *  the same as _currentIndex.  This only happens when you perform a ranged
   *  selection.  In that case, the start index of the ranged selection becomes
   *  the shift pivot (and the _currentIndex becomes the end of the ranged
   *  selection.)
   * It gets cleared whenever the selection changes and it's not the result of
   *  a call to rangedSelect.
   */
  _shiftSelectPivot: null,
  /**
   * A list of [lowIndexInclusive, highIndexInclusive] non-overlapping,
   *  non-adjacent 'tuples' sort in ascending order.
   */
  _ranges: [],
  /**
   * The number of currently selected rows.
   */
  _count: 0,

  // In the case of the stand-alone message window, there's no tree, but
  // there's a view.
  _view: null,

  get tree() {
    return this._treeBoxObject;
  },
  set tree(aTreeBoxObject) {
    this._treeBoxObject = aTreeBoxObject;
  },

  set view(aView) {
    this._view = aView;
  },
  /**
   * Although the nsITreeSelection documentation doesn't say, what this method
   *  is supposed to do is check if the seltype attribute on the XUL tree is any
   *  of the following: "single" (only a single row may be selected at a time,
   *  "cell" (a single cell may be selected), or "text" (the row gets selected
   *  but only the primary column shows up as selected.)
   *
   * @return false because we don't support single-selection.
   */
  get single() {
    return false;
  },

  _updateCount: function JSTreeSelection__updateCount() {
    this._count = 0;
    for (let [low, high] of this._ranges) {
      this._count += high - low + 1;
    }
  },

  get count() {
    return this._count;
  },

  isSelected: function JSTreeSelection_isSelected(aViewIndex) {
    for (let [low, high] of this._ranges) {
      if (aViewIndex >= low && aViewIndex <= high)
        return true;
    }
    return false;
  },

  /**
   * Select the given row.  It does nothing if that row was already selected.
   */
  select: function JSTreeSelection_select(aViewIndex) {
    // current index will provide our effective shift pivot
    this._shiftSelectPivot = null;
    this.currentIndex = aViewIndex;

    if (this._count == 1 && this._ranges[0][0] == aViewIndex)
      return;

    this._count = 1;
    this._ranges = [[aViewIndex, aViewIndex]];

    if (this._treeBoxObject)
      this._treeBoxObject.invalidate();

    this._fireSelectionChanged();
  },

  timedSelect: function JSTreeSelection_timedSelect(aIndex, aDelay) {
    throw new Error("We do not implement timed selection.");
  },

  toggleSelect: function JSTreeSelection_toggleSelect(aIndex) {
    this.currentIndex = aIndex;
    // If nothing's selected, select aIndex
    if (this._count == 0) {
      this._count = 1;
      this._ranges = [[aIndex, aIndex]];
    }
    else for (let [iTupe, [low, high]] of this._ranges.entries()) {
      // below the range? add it to the existing range or create a new one
      if (aIndex < low) {
        this._count++;
        // is it just below an existing range? (range fusion only happens in the
        //  high case, not here.)
        if (aIndex == low - 1) {
          this._ranges[iTupe][0] = aIndex;
          break;
        }
        // then it gets its own range
        this._ranges.splice(iTupe, 0, [aIndex, aIndex]);
        break;
      }
      // in the range?  will need to either nuke, shrink, or split the range to
      //  remove it
      if (aIndex >= low && aIndex <= high) {
        this._count--;
        // nuke
        if (aIndex == low && aIndex == high)
          this._ranges.splice(iTupe, 1);
        // lower shrink
        else if (aIndex == low)
          this._ranges[iTupe][0] = aIndex + 1;
        // upper shrink
        else if (aIndex == high)
          this._ranges[iTupe][1] = aIndex - 1;
        // split
        else
          this._ranges.splice(iTupe, 1, [low, aIndex - 1], [aIndex + 1, high]);
        break;
      }
      // just above the range?  fuse into the range, and possibly the next
      //  range up.
      if (aIndex == high + 1) {
        this._count++;
        // see if there is another range and there was just a gap of one between
        //  the two ranges.
        if ((iTupe + 1 < this._ranges.length) &&
            (this._ranges[iTupe+1][0] == aIndex + 1)) {
          // yes, merge the ranges
          this._ranges.splice(iTupe, 2, [low, this._ranges[iTupe+1][1]]);
          break;
        }
        // nope, no merge required, just update the range
        this._ranges[iTupe][1] = aIndex;
        break;
      }
      // otherwise we need to keep going
    }

    if (this._treeBoxObject)
      this._treeBoxObject.invalidateRow(aIndex);
    this._fireSelectionChanged();
  },

  /**
   * @param aRangeStart If omitted, it implies a shift-selection is happening,
   *     in which case we use _shiftSelectPivot as the start if we have it,
   *     _currentIndex if we don't, and if we somehow didn't have a
   *     _currentIndex, we use the range end.
   * @param aRangeEnd Just the inclusive end of the range.
   * @param aAugment Does this set a new selection or should it be merged with
   *     the existing selection?
   */
  rangedSelect: function JSTreeSelection_rangedSelect(aRangeStart, aRangeEnd,
                                                      aAugment) {
    if (aRangeStart == -1) {
      if (this._shiftSelectPivot != null)
        aRangeStart = this._shiftSelectPivot;
      else if (this._currentIndex != null)
        aRangeStart = this._currentIndex;
      else
        aRangeStart = aRangeEnd;
    }

    this._shiftSelectPivot = aRangeStart;
    this.currentIndex = aRangeEnd;

    // enforce our ordering constraint for our ranges
    if (aRangeStart > aRangeEnd)
      [aRangeStart, aRangeEnd] = [aRangeEnd, aRangeStart];

    // if we're not augmenting, then this is really easy.
    if (!aAugment) {
      this._count = aRangeEnd - aRangeStart + 1;
      this._ranges = [[aRangeStart, aRangeEnd]];
      if (this._treeBoxObject)
        this._treeBoxObject.invalidate();
      this._fireSelectionChanged();
      return;
    }

    // Iterate over our existing set of ranges, finding the 'range' of ranges
    //  that our new range overlaps or simply obviates.
    // Overlap variables track blocks we need to keep some part of, Nuke
    //  variables are for blocks that get spliced out.  For our purposes, all
    //  overlap blocks are also nuke blocks.
    let lowOverlap, lowNuke, highNuke, highOverlap;
    // in case there is no overlap, also figure an insertionPoint
    let insertionPoint = this._ranges.length; // default to the end
    for (let [iTupe, [low, high]] of this._ranges.entries()) {
      // If it's completely include the range, it should be nuked
      if (aRangeStart <= low && aRangeEnd >= high) {
        if (lowNuke == null) // only the first one we see is the low one
          lowNuke = iTupe;
        highNuke = iTupe;
      }
      // If our new range start is inside a range or is adjacent, it's overlap
      if (aRangeStart >= low - 1 && aRangeStart <= high + 1 &&
          lowOverlap == null)
        lowOverlap = lowNuke = highNuke = iTupe;
      // If our new range ends inside a range or is adjacent, it's overlap
      if (aRangeEnd >= low - 1 && aRangeEnd <= high + 1) {
        highOverlap = highNuke = iTupe;
        if (lowNuke == null)
          lowNuke = iTupe;
      }

      // we're done when no more overlap is possible
      if (aRangeEnd < low) {
        insertionPoint = iTupe;
        break;
      }
    }

    if (lowOverlap != null)
      aRangeStart = Math.min(aRangeStart, this._ranges[lowOverlap][0]);
    if (highOverlap != null)
      aRangeEnd = Math.max(aRangeEnd, this._ranges[highOverlap][1]);
    if (lowNuke != null)
      this._ranges.splice(lowNuke, highNuke - lowNuke + 1,
                          [aRangeStart, aRangeEnd]);
    else
      this._ranges.splice(insertionPoint, 0, [aRangeStart, aRangeEnd]);

    this._updateCount();
    if (this._treeBoxObject)
      this._treeBoxObject.invalidate();
    this._fireSelectionChanged();
  },

  /**
   * This is basically RangedSelect but without insertion of a new range and we
   *  don't need to worry about adjacency.
   * Oddly, nsTreeSelection doesn't fire a selection changed event here...
   */
  clearRange: function JSTreeSelection_clearRange(aRangeStart, aRangeEnd) {
    // Iterate over our existing set of ranges, finding the 'range' of ranges
    //  that our clear range overlaps or simply obviates.
    // Overlap variables track blocks we need to keep some part of, Nuke
    //  variables are for blocks that get spliced out.  For our purposes, all
    //  overlap blocks are also nuke blocks.
    let lowOverlap, lowNuke, highNuke, highOverlap;
    for (let [iTupe, [low, high]] of this._ranges.entries()) {
      // If we completely include the range, it should be nuked
      if (aRangeStart <= low && aRangeEnd >= high) {
        if (lowNuke == null) // only the first one we see is the low one
          lowNuke = iTupe;
        highNuke = iTupe;
      }
      // If our new range start is inside a range, it's nuke and maybe overlap
      if (aRangeStart >= low && aRangeStart <= high && lowNuke == null) {
        lowNuke = highNuke = iTupe;
        // it's only overlap if we don't match at the low end
        if (aRangeStart > low)
          lowOverlap = iTupe;
      }
      // If our new range ends inside a range, it's nuke and maybe overlap
      if (aRangeEnd >= low && aRangeEnd <= high) {
        highNuke = iTupe;
        // it's only overlap if we don't match at the high end
        if (aRangeEnd < high)
          highOverlap = iTupe;
        if (lowNuke == null)
          lowNuke = iTupe;
      }

      // we're done when no more overlap is possible
      if (aRangeEnd < low)
        break;
    }
    // nothing to do since there's nothing to nuke
    if (lowNuke == null)
      return;
    let args = [lowNuke, highNuke - lowNuke + 1];
    if (lowOverlap != null)
      args.push([this._ranges[lowOverlap][0], aRangeStart - 1]);
    if (highOverlap != null)
      args.push([aRangeEnd + 1, this._ranges[highOverlap][1]]);
    this._ranges.splice.apply(this._ranges, args);

    this._updateCount();
    if (this._treeBoxObject)
      this._treeBoxObject.invalidate();
    // note! nsTreeSelection doesn't fire a selection changed event, so neither
    //  do we, but it seems like we should
  },

  /**
   * nsTreeSelection always fires a select notification when the range is
   *  cleared, even if there is no effective chance in selection.
   */
  clearSelection: function JSTreeSelection_clearSelection() {
    this._shiftSelectPivot = null;
    this._count = 0;
    this._ranges = [];
    if (this._treeBoxObject)
      this._treeBoxObject.invalidate();
    this._fireSelectionChanged();
  },

  /**
   * Not even nsTreeSelection implements this.
   */
  invertSelection: function JSTreeSelection_invertSelection() {
    throw new Error("Who really was going to use this?");
  },

  /**
   * Select all with no rows is a no-op, otherwise we select all and notify.
   */
  selectAll: function JSTreeSelection_selectAll() {
    if (!this._view)
      return;

    let view = this._view;
    let rowCount = view.rowCount;

    // no-ops-ville
    if (!rowCount)
      return;

    this._count = rowCount;
    this._ranges = [[0, rowCount - 1]];

    if (this._treeBoxObject)
      this._treeBoxObject.invalidate();
    this._fireSelectionChanged();
  },

  getRangeCount: function JSTreeSelection_getRangeCount() {
    return this._ranges.length;
  },
  getRangeAt: function JSTreeSelection_getRangeAt(aRangeIndex, aMinObj,
                                                  aMaxObj) {
    if (aRangeIndex < 0 || aRangeIndex > this._ranges.length)
      throw new Exception("Try a real range index next time.");
    [aMinObj.value, aMaxObj.value] = this._ranges[aRangeIndex];
  },

  invalidateSelection: function JSTreeSelection_invalidateSelection() {
    if (this._treeBoxObject)
      this._treeBoxObject.invalidate();
  },

  /**
   * Helper method to adjust points in the face of row additions/removal.
   * @param aPoint The point, null if there isn't one, or an index otherwise.
   * @param aDeltaAt The row at which the change is happening.
   * @param aDelta The number of rows added if positive, or the (negative)
   *     number of rows removed.
   */
  _adjustPoint: function JSTreeSelection__adjustPoint(aPoint, aDeltaAt,
                                                      aDelta) {
    // if there is no point, no change
    if (aPoint == null)
      return aPoint;
    // if the point is before the change, no change
    if (aPoint < aDeltaAt)
      return aPoint;
    // if it's a deletion and it includes the point, clear it
    if (aDelta < 0 && aPoint >= aDeltaAt && (aPoint + aDelta < aDeltaAt))
      return null;
    // (else) the point is at/after the change, compensate
    return aPoint + aDelta;
  },
  /**
   * Find the index of the range, if any, that contains the given index, and
   *  the index at which to insert a range if one does not exist.
   *
   * @return A tuple containing: 1) the index if there is one, null otherwise,
   *     2) the index at which to insert a range that would contain the point.
   */
  _findRangeContainingRow:
      function JSTreeSelection__findRangeContainingRow(aIndex) {
    for (let [iTupe, [low, high]] of this._ranges.entries()) {
      if (aIndex >= low && aIndex <= high)
        return [iTupe, iTupe];
      if (aIndex < low)
        return [null, iTupe];
    }
    return [null, this._ranges.length];
  },


  /**
   * When present, a list of calls made to adjustSelection.  See
   *  |logAdjustSelectionForReplay| and |replayAdjustSelectionLog|.
   */
  _adjustSelectionLog: null,
  /**
   * Start logging calls to adjustSelection made against this instance.  You
   *  would do this because you are replacing an existing selection object
   *  with this instance for the purposes of creating a transient selection.
   *  Of course, you want the original selection object to be up-to-date when
   *  you go to put it back, so then you can call replayAdjustSelectionLog
   *  with that selection object and everything will be peachy.
   */
  logAdjustSelectionForReplay:
      function JSTreeSelection_logAdjustSelectionForReplay() {
    this._adjustSelectionLog = [];
  },
  /**
   * Stop logging calls to adjustSelection and replay the existing log against
   *  aSelection.
   *
   * @param aSelection {nsITreeSelection}.
   */
  replayAdjustSelectionLog:
      function JSTreeSelection_replayAdjustSelectionLog(aSelection) {
    if (this._adjustSelectionLog.length) {
      // Temporarily disable selection events because adjustSelection is going
      //  to generate an event each time otherwise, and better 1 event than
      //  many.
      aSelection.selectEventsSuppressed = true;
      for (let [index, count] of this._adjustSelectionLog) {
        aSelection.adjustSelection(index, count);
      }
      aSelection.selectEventsSuppressed = false;
    }
    this._adjustSelectionLog = null;
  },

  adjustSelection: function JSTreeSelection_adjustSelection(aIndex, aCount) {
    // nothing to do if there is no actual change
    if (!aCount)
      return;

    if (this._adjustSelectionLog)
      this._adjustSelectionLog.push([aIndex, aCount]);

    // adjust our points
    this._shiftSelectPivot = this._adjustPoint(this._shiftSelectPivot,
                                               aIndex, aCount);
    this._currentIndex = this._adjustPoint(this._currentIndex, aIndex, aCount);

    // If we are adding rows, we want to split any range at aIndex and then
    //  translate all of the ranges above that point up.
    if (aCount > 0) {
      let [iContain, iInsert] = this._findRangeContainingRow(aIndex);
      if (iContain != null) {
        let [low, high] = this._ranges[iContain];
        // if it is the low value, we just want to shift the range entirely, so
        //  do nothing (and keep iInsert pointing at it for translation)
        // if it is not the low value, then there must be at least two values so
        //  we should split it and only translate the new/upper block
        if (aIndex != low) {
          this._ranges.splice(iContain, 1, [low, aIndex - 1], [aIndex, high]);
          iInsert++;
        }
      }
      // now translate everything from iInsert on up
      for (let iTrans = iInsert; iTrans < this._ranges.length; iTrans++) {
        let [low, high] = this._ranges[iTrans];
        this._ranges[iTrans] = [low + aCount, high + aCount];
      }
      // invalidate and fire selection change notice
      if (this._treeBoxObject)
        this._treeBoxObject.invalidate();
      this._fireSelectionChanged();
      return;
    }

    // If we are removing rows, we are basically clearing the range that is
    //  getting deleted and translating everyone above the remaining point
    //  downwards.  The one trick is we may have to merge the lowest translated
    //  block.
    let saveSuppress = this.selectEventsSuppressed;
    this.selectEventsSuppressed = true;
    this.clearRange(aIndex, aIndex - aCount - 1);
    // translate
    let iTrans = this._findRangeContainingRow(aIndex)[1];
    for (; iTrans < this._ranges.length; iTrans++) {
      let [low, high] = this._ranges[iTrans];
      // for the first range, low may be below the index, in which case it
      //  should not get translated
      this._ranges[iTrans] = [(low >= aIndex) ? low + aCount : low,
                              high + aCount];
    }
    // we may have to merge the lowest translated block because it may now be
    //  adjacent to the previous block
    if (iTrans > 0 && iTrans < this._ranges.length &&
        this._ranges[iTrans-1][1] == this_ranges[iTrans][0]) {
      this._ranges[iTrans-1][1] = this._ranges[iTrans][1];
      this._ranges.splice(iTrans, 1);
    }

    if (this._treeBoxObject)
      this._treeBoxObject.invalidate();
    this.selectEventsSuppressed = saveSuppress;
  },

  get selectEventsSuppressed() {
    return this._selectEventsSuppressed;
  },
  /**
   * Control whether selection events are suppressed.  For consistency with
   *  nsTreeSelection, we always generate a selection event when a value of
   *  false is assigned, even if the value was already false.
   */
  set selectEventsSuppressed(aSuppress) {
    this._selectEventsSuppressed = aSuppress;
    if (!aSuppress)
      this._fireSelectionChanged();
  },

  /**
   * Note that we bypass any XUL "onselect" handler that may exist and go
   *  straight to the view.  If you have a tree, you shouldn't be using us,
   *  so this seems aboot right.
   */
  _fireSelectionChanged: function JSTreeSelection__fireSelectionChanged() {
    // don't fire if we are suppressed; we will fire when un-suppressed
    if (this.selectEventsSuppressed)
      return;
    let view;
    if (this._treeBoxObject && this._treeBoxObject.view)
      view = this._treeBoxObject.view;
    else
      view = this._view;

    // We might not have a view if we're in the middle of setting up things
    if (view) {
      view = view.QueryInterface(Ci.nsITreeView);
      view.selectionChanged();
    }
  },

  get currentIndex() {
    if (this._currentIndex == null)
      return -1;
    return this._currentIndex;
  },
  /**
   * Sets the current index.  Other than updating the variable, this just
   *  invalidates the tree row if we have a tree.
   * The real selection object would send a DOM event we don't care about.
   */
  set currentIndex(aIndex) {
    if (aIndex == this.currentIndex)
      return;

    this._currentIndex = (aIndex != -1) ? aIndex : null;
    if (this._treeBoxObject)
      this._treeBoxObject.invalidateRow(aIndex);
  },

  currentColumn: null,

  get shiftSelectPivot() {
    return this._shiftSelectPivot != null ? this._shiftSelectPivot : -1;
  },

  QueryInterface: XPCOMUtils.generateQI(
    [Ci.nsITreeSelection]),

  /*
   * Functions after this aren't part of the nsITreeSelection interface.
   */

  /**
   * Duplicate this selection on another nsITreeSelection. This is useful
   * when you would like to discard this selection for a real tree selection.
   * We assume that both selections are for the same tree.
   *
   * @note We don't transfer the correct shiftSelectPivot over.
   * @note This will fire a selectionChanged event on the tree view.
   *
   * @param aSelection an nsITreeSelection to duplicate this selection onto
   */
  duplicateSelection: function JSTreeSelection_duplicateSelection(aSelection) {
    aSelection.selectEventsSuppressed = true;
    aSelection.clearSelection();
    for (let [iTupe, [low, high]] of this._ranges.entries())
      aSelection.rangedSelect(low, high, iTupe > 0);

    aSelection.currentIndex = this.currentIndex;
    // This will fire a selectionChanged event
    aSelection.selectEventsSuppressed = false;
  },
};