r25048: From the archives (patch found in one of my old working trees):
[kai/samba-autobuild/.git] / webapps / qooxdoo-0.6.5-sdk / frontend / framework / source / class / qx / manager / selection / SelectionManager.js
1 /* ************************************************************************
2
3    qooxdoo - the new era of web development
4
5    http://qooxdoo.org
6
7    Copyright:
8      2004-2007 1&1 Internet AG, Germany, http://www.1and1.org
9
10    License:
11      LGPL: http://www.gnu.org/licenses/lgpl.html
12      EPL: http://www.eclipse.org/org/documents/epl-v10.php
13      See the LICENSE file in the project's top-level directory for details.
14
15    Authors:
16      * Sebastian Werner (wpbasti)
17      * Andreas Ecker (ecker)
18
19 ************************************************************************ */
20
21 /* ************************************************************************
22
23 #module(ui_form)
24
25 ************************************************************************ */
26
27 /**
28  * This class represents a selection and manage incoming events for widgets
29  * which need selection support.
30  *
31  * @event changeSelection {qx.event.type.DataEvent} sets the data property of the event object to an arryas of selected items.
32  */
33 qx.OO.defineClass("qx.manager.selection.SelectionManager", qx.core.Target,
34 function(vBoundedWidget)
35 {
36   qx.core.Target.call(this);
37
38   this._selectedItems = new qx.type.Selection(this);
39
40   if (vBoundedWidget != null) {
41     this.setBoundedWidget(vBoundedWidget);
42   }
43 });
44
45
46
47 /*
48 ---------------------------------------------------------------------------
49   PROPERTIES
50 ---------------------------------------------------------------------------
51 */
52
53 /*!
54 This contains the currently assigned widget (qx.ui.form.List, ...)
55 */
56 qx.OO.addProperty({ name : "boundedWidget", type : "object" });
57
58 /*!
59 Should multiple selection be allowed?
60 */
61 qx.OO.addProperty({ name : "multiSelection", type : "boolean", defaultValue : true });
62
63 /*!
64 Enable drag selection?
65 */
66 qx.OO.addProperty({ name : "dragSelection", type : "boolean", defaultValue : true });
67
68 /*!
69 Should the user be able to select
70 */
71 qx.OO.addProperty({ name : "canDeselect", type : "boolean", defaultValue : true });
72
73 /*!
74 Should a change event be fired?
75 */
76 qx.OO.addProperty({ name : "fireChange", type : "boolean", defaultValue : true });
77
78 /*!
79 The current anchor in range selections.
80 */
81 qx.OO.addProperty({ name : "anchorItem", type : "object" });
82
83 /*!
84 The last selected item
85 */
86 qx.OO.addProperty({ name : "leadItem", type : "object" });
87
88 /*!
89 Grid selection
90 */
91 qx.OO.addProperty({ name : "multiColumnSupport", type : "boolean", defaultValue : false });
92
93
94
95
96
97
98 /*
99 ---------------------------------------------------------------------------
100   MODIFIER
101 ---------------------------------------------------------------------------
102 */
103
104 qx.Proto._modifyAnchorItem = function(propValue, propOldValue, propData)
105 {
106   if (propOldValue) {
107     this.renderItemAnchorState(propOldValue, false);
108   }
109
110   if (propValue) {
111     this.renderItemAnchorState(propValue, true);
112   }
113
114   return true;
115 }
116
117 qx.Proto._modifyLeadItem = function(propValue, propOldValue, propData)
118 {
119   if (propOldValue) {
120     this.renderItemLeadState(propOldValue, false);
121   }
122
123   if (propValue) {
124     this.renderItemLeadState(propValue, true);
125   }
126
127   return true;
128 }
129
130
131
132
133
134
135 /*
136 ---------------------------------------------------------------------------
137   MAPPING TO BOUNDED WIDGET
138 ---------------------------------------------------------------------------
139 */
140
141 qx.Proto._getFirst = function() {
142   return this.getBoundedWidget().getFirstVisibleChild();
143 }
144
145 qx.Proto._getLast = function() {
146   return this.getBoundedWidget().getLastVisibleChild();
147 }
148
149 qx.Proto.getFirst = function()
150 {
151   var vItem = this._getFirst();
152   if (vItem) {
153     return vItem.isEnabled() ? vItem : this.getNext(vItem);
154   }
155 }
156
157 qx.Proto.getLast = function()
158 {
159   var vItem = this._getLast();
160   if (vItem) {
161     return vItem.isEnabled() ? vItem : this.getPrevious(vItem);
162   }
163 }
164
165 qx.Proto.getItems = function() {
166   return this.getBoundedWidget().getChildren();
167 }
168
169 qx.Proto.getNextSibling = function(vItem) {
170   return vItem.getNextSibling();
171 }
172
173 qx.Proto.getPreviousSibling = function(vItem) {
174   return vItem.getPreviousSibling();
175 }
176
177 qx.Proto.getNext = function(vItem)
178 {
179   while(vItem)
180   {
181     vItem = this.getNextSibling(vItem);
182
183     if (!vItem) {
184       break;
185     }
186
187     if (this.getItemEnabled(vItem)) {
188       return vItem;
189     }
190   }
191
192   return null;
193 }
194
195 qx.Proto.getPrevious = function(vItem)
196 {
197   while(vItem)
198   {
199     vItem = this.getPreviousSibling(vItem);
200
201     if (!vItem) {
202       break;
203     }
204
205     if (this.getItemEnabled(vItem)) {
206       return vItem;
207     }
208   }
209
210   return null;
211 }
212
213 qx.Proto.isBefore = function(vItem1, vItem2)
214 {
215   var cs = this.getItems();
216   return cs.indexOf(vItem1) < cs.indexOf(vItem2);
217 }
218
219 qx.Proto.isEqual = function(vItem1, vItem2) {
220   return vItem1 == vItem2;
221 }
222
223
224
225 /*
226 ---------------------------------------------------------------------------
227   MAPPING TO ITEM PROPERTIES
228 ---------------------------------------------------------------------------
229 */
230
231 qx.Proto.getItemHashCode = function(vItem) {
232   return vItem.toHashCode();
233 }
234
235
236
237
238
239 /*
240 ---------------------------------------------------------------------------
241   MAPPING TO ITEM DIMENSIONS
242 ---------------------------------------------------------------------------
243 */
244
245 qx.Proto.scrollItemIntoView = function(vItem, vTopLeft) {
246   vItem.scrollIntoView(vTopLeft);
247 }
248
249 qx.Proto.getItemLeft = function(vItem) {
250   return vItem.getOffsetLeft();
251 }
252
253 qx.Proto.getItemTop = function(vItem) {
254   return vItem.getOffsetTop();
255 }
256
257 qx.Proto.getItemWidth = function(vItem) {
258   return vItem.getOffsetWidth();
259 }
260
261 qx.Proto.getItemHeight = function(vItem) {
262   return vItem.getOffsetHeight();
263 }
264
265 qx.Proto.getItemEnabled = function(vItem) {
266   return vItem.getEnabled();
267 }
268
269
270
271
272
273
274 /*
275 ---------------------------------------------------------------------------
276   ITEM STATE MANAGMENT
277 ---------------------------------------------------------------------------
278 */
279
280 qx.Proto.renderItemSelectionState = function(vItem, vIsSelected)
281 {
282   vIsSelected ? vItem.addState("selected") : vItem.removeState("selected");
283
284   if (vItem.handleStateChange) {
285     vItem.handleStateChange();
286   }
287 }
288
289 qx.Proto.renderItemAnchorState = function(vItem, vIsAnchor)
290 {
291   vIsAnchor ? vItem.addState("anchor") : vItem.removeState("anchor");
292
293   if (vItem.handleStateChange != null) {
294     vItem.handleStateChange();
295   }
296 }
297
298 qx.Proto.renderItemLeadState = function(vItem, vIsLead)
299 {
300   vIsLead ? vItem.addState("lead") : vItem.removeState("lead");
301
302   if (vItem.handleStateChange != null) {
303     vItem.handleStateChange();
304   }
305 }
306
307
308
309
310
311 /*
312 ---------------------------------------------------------------------------
313   SELECTION HANDLING
314 ---------------------------------------------------------------------------
315 */
316
317 qx.Proto.getItemSelected = function(vItem) {
318   return this._selectedItems.contains(vItem);
319 }
320
321 /*!
322 Make a single item selected / not selected
323
324 #param vItem[qx.ui.core.Widget]: Item which should be selected / not selected
325 #param vSelected[Boolean]: Should this item be selected?
326 */
327 qx.Proto.setItemSelected = function(vItem, vSelected)
328 {
329   var hc = this.getItemHashCode(vItem);
330
331   switch(this.getMultiSelection())
332   {
333     // Multiple item selection is allowed
334     case true:
335       if (!this.getItemEnabled(vItem)) {
336         return;
337       }
338
339       // If selection state is not to be changed => return
340       if (this.getItemSelected(vItem) == vSelected) {
341         return;
342       }
343
344       // Otherwise render new state
345       this.renderItemSelectionState(vItem, vSelected);
346
347       // Add item to selection hash / delete it from there
348       vSelected ? this._selectedItems.add(vItem) : this._selectedItems.remove(vItem);
349
350       // Dispatch change Event
351       this._dispatchChange();
352
353       break;
354
355
356
357     // Multiple item selection is NOT allowed
358     case false:
359       var item0 = this.getSelectedItems()[0];
360
361
362
363       if (vSelected)
364       {
365         // Precheck for any changes
366         var old = item0;
367
368         if (this.isEqual(vItem, old)) {
369           return;
370         }
371
372         // Reset rendering of previous selected item
373         if (old != null) {
374           this.renderItemSelectionState(old, false);
375         }
376
377         // Render new item as selected
378         this.renderItemSelectionState(vItem, true);
379
380         // Reset current selection hash
381         this._selectedItems.removeAll();
382
383         // Add new one
384         this._selectedItems.add(vItem);
385
386         // Dispatch change Event
387         this._dispatchChange();
388       }
389       else
390       {
391         // Pre-check if item is currently selected
392         // Do not allow deselection in single selection mode
393         if (!this.isEqual(item0, vItem))
394         {
395           // Reset rendering as selected item
396           this.renderItemSelectionState(vItem, false);
397
398           // Reset current selection hash
399           this._selectedItems.removeAll();
400
401           // Dispatch change Event
402           this._dispatchChange();
403         }
404       }
405
406       break;
407
408   }
409 }
410
411
412
413
414
415
416
417
418 /*!
419   Get the selected items (objects)
420 */
421 qx.Proto.getSelectedItems = function() {
422   return this._selectedItems.toArray();
423 }
424
425 qx.Proto.getSelectedItem = function() {
426   return this._selectedItems.getFirst();
427 }
428
429 /*!
430 Select given items
431
432 #param vItems[Array of Widgets]: Items to select
433 */
434 qx.Proto.setSelectedItems = function(vItems)
435 {
436   var oldVal = this._getChangeValue();
437
438   // Temporary disabling of event fire
439   var oldFireChange = this.getFireChange();
440   this.setFireChange(false);
441
442   // Deselect all currently selected items
443   this._deselectAll();
444
445   // Apply new selection
446   var vItem;
447   var vItemLength = vItems.length;
448
449   for (var i=0; i<vItemLength; i++)
450   {
451     vItem = vItems[i];
452
453     if (!this.getItemEnabled(vItem)) {
454       continue;
455     }
456
457     // Add item to selection
458     this._selectedItems.add(vItem);
459
460     // Render new state for item
461     this.renderItemSelectionState(vItem, true);
462   }
463
464   // Recover change event status
465   this.setFireChange(oldFireChange);
466
467   // Dispatch change Event
468   if (oldFireChange && this._hasChanged(oldVal)) {
469     this._dispatchChange();
470   }
471 }
472
473
474 qx.Proto.setSelectedItem = function(vItem)
475 {
476   if (!vItem) {
477     return;
478   }
479
480   if (!this.getItemEnabled(vItem)) {
481     return;
482   }
483
484   var oldVal = this._getChangeValue();
485
486   // Temporary disabling of event fire
487   var oldFireChange = this.getFireChange();
488   this.setFireChange(false);
489
490   // Deselect all currently selected items
491   this._deselectAll();
492
493   // Add item to selection
494   this._selectedItems.add(vItem);
495
496   // Render new state for item
497   this.renderItemSelectionState(vItem, true);
498
499   // Recover change event status
500   this.setFireChange(oldFireChange);
501
502   // Dispatch change Event
503   if (oldFireChange && this._hasChanged(oldVal)) {
504     this._dispatchChange();
505   }
506 }
507
508
509
510
511
512 /*!
513   Select all items.
514 */
515 qx.Proto.selectAll = function()
516 {
517   var oldVal = this._getChangeValue();
518
519   // Temporary disabling of event fire
520   var oldFireChange = this.getFireChange();
521   this.setFireChange(false);
522
523   // Call sub method to select all items
524   this._selectAll();
525
526   // Recover change event status
527   this.setFireChange(oldFireChange);
528
529   // Dispatch change Event
530   if (oldFireChange && this._hasChanged(oldVal)) {
531     this._dispatchChange();
532   }
533 }
534
535 /*!
536   Sub method for selectAll. Handles the real work
537   to select all items.
538 */
539 qx.Proto._selectAll = function()
540 {
541   if (!this.getMultiSelection()) {
542     return;
543   }
544
545   var vItem;
546   var vItems = this.getItems();
547   var vItemsLength = vItems.length;
548
549   // Reset current selection hash
550   this._selectedItems.removeAll();
551
552   for (var i=0; i<vItemsLength; i++)
553   {
554     vItem = vItems[i];
555
556     if (!this.getItemEnabled(vItem)) {
557       continue;
558     }
559
560     // Add item to selection
561     this._selectedItems.add(vItem);
562
563     // Render new state for item
564     this.renderItemSelectionState(vItem, true);
565   }
566
567   return true;
568 }
569
570
571
572
573
574 /*!
575   Deselect all items.
576 */
577 qx.Proto.deselectAll = function()
578 {
579   var oldVal = this._getChangeValue();
580
581   // Temporary disabling of event fire
582   var oldFireChange = this.getFireChange();
583   this.setFireChange(false);
584
585   // Call sub method to deselect all items
586   this._deselectAll();
587
588   // Recover change event status
589   this.setFireChange(oldFireChange);
590
591   // Dispatch change Event
592   if (oldFireChange && this._hasChanged(oldVal))
593     this._dispatchChange();
594   }
595
596 /*!
597   Sub method for deselectAll. Handles the real work
598   to deselect all items.
599 */
600 qx.Proto._deselectAll = function()
601 {
602   // Render new state for items
603   var items = this._selectedItems.toArray();
604   for (var i = 0; i < items.length; i++) {
605     this.renderItemSelectionState(items[i], false);
606   }
607
608   // Delete all entries in selectedItems hash
609   this._selectedItems.removeAll();
610
611   return true;
612 }
613
614
615
616
617 /*!
618 Select a range of items.
619
620 #param vItem1[qx.ui.core.Widget]: Start item
621 #param vItem2[qx.ui.core.Widget]: Stop item
622 */
623 qx.Proto.selectItemRange = function(vItem1, vItem2)
624 {
625   var oldVal = this._getChangeValue();
626
627   // Temporary disabling of event fire
628   var oldFireChange = this.getFireChange();
629   this.setFireChange(false);
630
631   // Call sub method to select the range of items
632   this._selectItemRange(vItem1, vItem2, true);
633
634   // Recover change event status
635   this.setFireChange(oldFireChange);
636
637   // Dispatch change Event
638   if (oldFireChange && this._hasChanged(oldVal)) {
639     this._dispatchChange();
640   }
641 }
642
643
644
645
646 /*!
647 Sub method for selectItemRange. Handles the real work
648 to select a range of items.
649
650 #param vItem1[qx.ui.core.Widget]: Start item
651 #param vItem2[qx.ui.core.Widget]: Stop item
652 #param vDelect[Boolean]: Deselect currently selected items first?
653 */
654 qx.Proto._selectItemRange = function(vItem1, vItem2, vDeselect)
655 {
656   // this.debug("SELECT_RANGE: " + vItem1.toText() + "<->" + vItem2.toText());
657   // this.debug("SELECT_RANGE: " + vItem1.pos + "<->" + vItem2.pos);
658
659   // Pre-Check a revert call if vItem2 is before vItem1
660   if (this.isBefore(vItem2, vItem1)) {
661     return this._selectItemRange(vItem2, vItem1, vDeselect);
662   }
663
664   // Deselect all
665   if (vDeselect) {
666     this._deselectAll();
667   }
668
669   var vCurrentItem = vItem1;
670
671   while (vCurrentItem != null)
672   {
673     if (this.getItemEnabled(vCurrentItem))
674     {
675       // Add item to selection
676       this._selectedItems.add(vCurrentItem);
677
678       // Render new state for item
679       this.renderItemSelectionState(vCurrentItem, true);
680     }
681
682     // Stop here if we reached target item
683     if (this.isEqual(vCurrentItem, vItem2)) {
684       break;
685     }
686
687     // Get next item
688     vCurrentItem = this.getNext(vCurrentItem);
689   }
690
691   return true;
692 }
693
694 /*!
695 Internal method for deselection of ranges.
696
697 #param vItem1[qx.ui.core.Widget]: Start item
698 #param vItem2[qx.ui.core.Widget]: Stop item
699 */
700 qx.Proto._deselectItemRange = function(vItem1, vItem2)
701 {
702   // Pre-Check a revert call if vItem2 is before vItem1
703   if (this.isBefore(vItem2, vItem1)) {
704     return this._deselectItemRange(vItem2, vItem1);
705   }
706
707   var vCurrentItem = vItem1;
708
709   while (vCurrentItem != null)
710   {
711     // Add item to selection
712     this._selectedItems.remove(vCurrentItem);
713
714     // Render new state for item
715     this.renderItemSelectionState(vCurrentItem, false);
716
717     // Stop here if we reached target item
718     if (this.isEqual(vCurrentItem, vItem2)) {
719       break;
720     }
721
722     // Get next item
723     vCurrentItem = this.getNext(vCurrentItem);
724   }
725 }
726
727
728 /*
729 ---------------------------------------------------------------------------
730   MOUSE EVENT HANDLING
731 ---------------------------------------------------------------------------
732 */
733
734 qx.Proto._activeDragSession = false;
735
736 qx.Proto.handleMouseDown = function(vItem, e)
737 {
738   // Only allow left and right button
739   if (!e.isLeftButtonPressed() && !e.isRightButtonPressed()) {
740     return;
741   }
742
743   // Keep selection on right click on already selected item
744   if (e.isRightButtonPressed() && this.getItemSelected(vItem)) {
745     return;
746   }
747
748   // Shift Key
749   //   or
750   // Click on an unseleted item (without Strg)
751   if (e.isShiftPressed() || this.getDragSelection() || (!this.getItemSelected(vItem) && !e.isCtrlPressed()))
752   {
753     // Handle event
754     this._onmouseevent(vItem, e);
755   }
756   else
757   {
758     // Update lead item
759     this.setLeadItem(vItem);
760   }
761
762
763   // Handle dragging
764   this._activeDragSession = this.getDragSelection();
765
766   if (this._activeDragSession)
767   {
768     // Add mouseup listener and register as capture widget
769     this.getBoundedWidget().addEventListener("mouseup", this._ondragup, this);
770     this.getBoundedWidget().setCapture(true);
771   }
772 }
773
774 qx.Proto._ondragup = function(e)
775 {
776   this.getBoundedWidget().removeEventListener("mouseup", this._ondragup, this);
777   this.getBoundedWidget().setCapture(false);
778   this._activeDragSession = false;
779 }
780
781 qx.Proto.handleMouseUp = function(vItem, e)
782 {
783   if (!e.isLeftButtonPressed()) {
784     return;
785   }
786
787   if (e.isCtrlPressed() || this.getItemSelected(vItem) && !this._activeDragSession) {
788     this._onmouseevent(vItem, e);
789   }
790
791   if (this._activeDragSession)
792   {
793     this._activeDragSession = false;
794     this.getBoundedWidget().setCapture(false);
795   }
796 }
797
798 qx.Proto.handleMouseOver = function(oItem, e)
799 {
800   if (! this.getDragSelection() || !this._activeDragSession) {
801     return;
802   }
803
804   this._onmouseevent(oItem, e, true);
805 }
806
807 // currently unused placeholder
808 qx.Proto.handleClick = function(vItem, e) {}
809
810 // currently unused placeholder
811 qx.Proto.handleDblClick = function(vItem, e) {}
812
813
814 /*!
815 Internal handler for all mouse events bound to this manager.
816 */
817 qx.Proto._onmouseevent = function(oItem, e, bOver)
818 {
819   if (!this.getItemEnabled(oItem)) {
820     return;
821   }
822
823   // ********************************************************************
824   //   Init
825   // ********************************************************************
826
827   // Cache current (old) values
828   var oldVal = this._getChangeValue();
829   var oldLead = this.getLeadItem();
830
831   // Temporary disabling of event fire
832   var oldFireChange = this.getFireChange();
833   this.setFireChange(false);
834
835   // Cache selection and count
836   var selectedItems = this.getSelectedItems();
837   var selectedCount = selectedItems.length;
838
839   // Update lead item
840   this.setLeadItem(oItem);
841
842   // Cache current anchor item
843   var currentAnchorItem = this.getAnchorItem();
844
845   // Cache keys pressed
846   var vCtrlKey = e.isCtrlPressed();
847   var vShiftKey = e.isShiftPressed();
848
849
850   // ********************************************************************
851   //   Do we need to update the anchor?
852   // ********************************************************************
853
854   if (!currentAnchorItem || selectedCount == 0 || (vCtrlKey && !vShiftKey && this.getMultiSelection() && !this.getDragSelection()))
855   {
856     this.setAnchorItem(oItem);
857     currentAnchorItem = oItem;
858   }
859
860
861
862   // ********************************************************************
863   //   Mode #1: Replace current selection with new one
864   // ********************************************************************
865   if ((!vCtrlKey && !vShiftKey && !this._activeDragSession || !this.getMultiSelection()))
866   {
867     if (!this.getItemEnabled(oItem)) {
868       return;
869     }
870
871     // Remove current selection
872     this._deselectAll();
873
874     // Update anchor item
875     this.setAnchorItem(oItem);
876
877     if (this._activeDragSession)
878     {
879       // a little bit hacky, but seems to be a fast way to detect if we slide to top or to bottom
880       this.scrollItemIntoView((this.getBoundedWidget().getScrollTop() > (this.getItemTop(oItem)-1) ? this.getPrevious(oItem) : this.getNext(oItem)) || oItem);
881     }
882
883     if (!this.getItemSelected(oItem)) {
884       this.renderItemSelectionState(oItem, true);
885     }
886
887     // Clear up and add new one
888     //this._selectedItems.removeAll();
889     this._selectedItems.add(oItem);
890
891     this._addToCurrentSelection = true;
892   }
893
894
895   // ********************************************************************
896   //   Mode #2: (De-)Select item range in mouse drag session
897   // ********************************************************************
898   else if (this._activeDragSession && bOver)
899   {
900     if (oldLead) {
901       this._deselectItemRange(currentAnchorItem, oldLead);
902     }
903
904     // Drag down
905     if (this.isBefore(currentAnchorItem, oItem))
906     {
907       if (this._addToCurrentSelection)
908       {
909         this._selectItemRange(currentAnchorItem, oItem, false);
910       }
911       else
912       {
913         this._deselectItemRange(currentAnchorItem, oItem);
914       }
915     }
916
917     // Drag up
918     else
919     {
920       if (this._addToCurrentSelection)
921       {
922         this._selectItemRange(oItem, currentAnchorItem, false);
923       }
924       else
925       {
926         this._deselectItemRange(oItem, currentAnchorItem);
927       }
928     }
929
930     // a little bit hacky, but seems to be a fast way to detect if we slide to top or to bottom
931     this.scrollItemIntoView((this.getBoundedWidget().getScrollTop() > (this.getItemTop(oItem)-1) ? this.getPrevious(oItem) : this.getNext(oItem)) || oItem);
932   }
933
934
935   // ********************************************************************
936   //   Mode #3: Add new item to current selection (ctrl pressed)
937   // ********************************************************************
938   else if (this.getMultiSelection() && vCtrlKey && !vShiftKey)
939   {
940     if (!this._activeDragSession) {
941       this._addToCurrentSelection = !(this.getCanDeselect() && this.getItemSelected(oItem));
942     }
943
944     this.setItemSelected(oItem, this._addToCurrentSelection);
945     this.setAnchorItem(oItem);
946   }
947
948
949   // ********************************************************************
950   //   Mode #4: Add new (or continued) range to selection
951   // ********************************************************************
952   else if (this.getMultiSelection() && vCtrlKey && vShiftKey)
953   {
954     if (!this._activeDragSession) {
955       this._addToCurrentSelection = !(this.getCanDeselect() && this.getItemSelected(oItem));
956     }
957
958     if (this._addToCurrentSelection)
959     {
960       this._selectItemRange(currentAnchorItem, oItem, false);
961     }
962     else
963     {
964       this._deselectItemRange(currentAnchorItem, oItem);
965     }
966   }
967
968   // ********************************************************************
969   //   Mode #5: Replace selection with new range selection
970   // ********************************************************************
971   else if (this.getMultiSelection() && !vCtrlKey && vShiftKey)
972   {
973     if (this.getCanDeselect())
974     {
975       this._selectItemRange(currentAnchorItem, oItem, true);
976     }
977
978     else
979     {
980       if (oldLead) {
981         this._deselectItemRange(currentAnchorItem, oldLead);
982       }
983
984       this._selectItemRange(currentAnchorItem, oItem, false);
985     }
986   }
987
988
989
990   // Recover change event status
991   this.setFireChange(oldFireChange);
992
993   // Dispatch change Event
994   if(oldFireChange && this._hasChanged(oldVal)) {
995     this._dispatchChange();
996   }
997 }
998
999
1000
1001
1002 /*
1003 ---------------------------------------------------------------------------
1004   KEY EVENT HANDLER
1005 ---------------------------------------------------------------------------
1006 */
1007
1008 qx.Proto.handleKeyDown = function(vDomEvent) {
1009   this.warn(
1010     "qx.manager.selection.SelectionManager.handleKeyDown is deprecated! " +
1011     "Use keypress insted and bind it to the onkeypress event."
1012   );
1013   this.handleKeyPress(vDomEvent);
1014 }
1015
1016
1017 /**
1018  * Handles key event to perform selection and navigation
1019  *
1020  * @param vDomEvent {qx.event.type.KeyEvent} event object
1021  */
1022 qx.Proto.handleKeyPress = function(vDomEvent)
1023 {
1024   var oldVal = this._getChangeValue();
1025
1026   // Temporary disabling of event fire
1027   var oldFireChange = this.getFireChange();
1028   this.setFireChange(false);
1029
1030   // Ctrl+A: Select all
1031   if (vDomEvent.getKeyIdentifier() == "A" && vDomEvent.isCtrlPressed())
1032   {
1033     if (this.getMultiSelection())
1034     {
1035       this._selectAll();
1036
1037       // Update lead item to this new last
1038       // (or better here: first) selected item
1039       this.setLeadItem(this.getFirst());
1040     }
1041   }
1042
1043   // Default operation
1044   else
1045   {
1046     var aIndex = this.getAnchorItem();
1047     var itemToSelect = this.getItemToSelect(vDomEvent);
1048
1049     // this.debug("Anchor: " + (aIndex ? aIndex.getLabel() : "null"));
1050     // this.debug("ToSelect: " + (itemToSelect ? itemToSelect.getLabel() : "null"));
1051
1052     if (itemToSelect && this.getItemEnabled(itemToSelect))
1053     {
1054       // Update lead item to this new last selected item
1055       this.setLeadItem(itemToSelect);
1056
1057       // Scroll new item into view
1058       this.scrollItemIntoView(itemToSelect);
1059
1060       // Stop event handling
1061       vDomEvent.preventDefault();
1062
1063       // Select a range
1064       if (vDomEvent.isShiftPressed() && this.getMultiSelection())
1065       {
1066         // Make it a little bit more failsafe:
1067         // Set anchor if not given already. Allows us to select
1068         // a range without any previous selection.
1069         if (aIndex == null) {
1070           this.setAnchorItem(itemToSelect);
1071         }
1072
1073         // Select new range (and clear up current selection first)
1074         this._selectItemRange(this.getAnchorItem(), itemToSelect, true);
1075       }
1076       else if (!vDomEvent.isCtrlPressed())
1077       {
1078         // Clear current selection
1079         this._deselectAll();
1080
1081         // Update new item to be selected
1082         this.renderItemSelectionState(itemToSelect, true);
1083
1084         // Add item to new selection
1085         this._selectedItems.add(itemToSelect);
1086
1087         // Update anchor to this new item
1088         // (allows following shift range selection)
1089         this.setAnchorItem(itemToSelect);
1090       }
1091       else if (vDomEvent.getKeyIdentifier() == "Space")
1092       {
1093         if (this._selectedItems.contains(itemToSelect))
1094         {
1095           // Update new item to be selected
1096           this.renderItemSelectionState(itemToSelect, false);
1097
1098           // Add item to new selection
1099           this._selectedItems.remove(itemToSelect);
1100
1101           // Fix anchor item
1102           this.setAnchorItem(this._selectedItems.getFirst());
1103         }
1104         else
1105         {
1106           // Clear current selection
1107           if (!vDomEvent.isCtrlPressed() || !this.getMultiSelection()) {
1108             this._deselectAll();
1109           }
1110
1111           // Update new item to be selected
1112           this.renderItemSelectionState(itemToSelect, true);
1113
1114           // Add item to new selection
1115           this._selectedItems.add(itemToSelect);
1116
1117           // Update anchor to this new item
1118           // (allows following shift range selection)
1119           this.setAnchorItem(itemToSelect);
1120         }
1121       }
1122     }
1123   }
1124
1125   // Recover change event status
1126   this.setFireChange(oldFireChange);
1127
1128   // Dispatch change Event
1129   if (oldFireChange && this._hasChanged(oldVal)) {
1130     this._dispatchChange();
1131   }
1132 }
1133
1134 qx.Proto.getItemToSelect = function(vKeyboardEvent)
1135 {
1136   // Don't handle ALT here
1137   if (vKeyboardEvent.isAltPressed()) {
1138     return null;
1139   }
1140
1141   // Handle event by keycode
1142   switch (vKeyboardEvent.getKeyIdentifier())
1143   {
1144     case "Home":
1145       return this.getHome(this.getLeadItem());
1146
1147     case "End":
1148       return this.getEnd(this.getLeadItem());
1149
1150
1151     case "Down":
1152       return this.getDown(this.getLeadItem());
1153
1154     case "Up":
1155       return this.getUp(this.getLeadItem());
1156
1157
1158     case "Left":
1159       return this.getLeft(this.getLeadItem());
1160
1161     case "Right":
1162       return this.getRight(this.getLeadItem());
1163
1164
1165     case "PageUp":
1166       return this.getPageUp(this.getLeadItem()) || this.getHome(this.getLeadItem());
1167
1168     case "PageDown":
1169       return this.getPageDown(this.getLeadItem()) || this.getEnd(this.getLeadItem());
1170
1171
1172     case "Space":
1173       if (vKeyboardEvent.isCtrlPressed()) {
1174         return this.getLeadItem();
1175       }
1176   }
1177
1178   return null;
1179 }
1180
1181
1182
1183
1184 /*
1185 ---------------------------------------------------------------------------
1186   CHANGE HANDLING
1187 ---------------------------------------------------------------------------
1188 */
1189
1190 qx.Proto._dispatchChange = function()
1191 {
1192   if (!this.getFireChange()) {
1193     return;
1194   }
1195
1196   if (this.hasEventListeners("changeSelection")) {
1197     this.dispatchEvent(new qx.event.type.DataEvent("changeSelection", this.getSelectedItems()), true);
1198   }
1199 }
1200
1201 qx.Proto._hasChanged = function(sOldValue) {
1202   return sOldValue != this._getChangeValue();
1203 }
1204
1205 qx.Proto._getChangeValue = function() {
1206   return this._selectedItems.getChangeValue();
1207 }
1208
1209
1210
1211
1212
1213
1214 /*
1215 ---------------------------------------------------------------------------
1216   POSITION HANDLING
1217 ---------------------------------------------------------------------------
1218 */
1219
1220 qx.Proto.getHome = function() {
1221   return this.getFirst();
1222 }
1223
1224 qx.Proto.getEnd = function() {
1225   return this.getLast();
1226 }
1227
1228 qx.Proto.getDown = function(vItem)
1229 {
1230   if (!vItem) {
1231     return this.getFirst();
1232   }
1233
1234   return this.getMultiColumnSupport() ? (this.getUnder(vItem) || this.getLast()) : this.getNext(vItem);
1235 }
1236
1237 qx.Proto.getUp = function(vItem)
1238 {
1239   if (!vItem) {
1240     return this.getLast();
1241   }
1242
1243   return this.getMultiColumnSupport() ? (this.getAbove(vItem) || this.getFirst()) : this.getPrevious(vItem);
1244 }
1245
1246 qx.Proto.getLeft = function(vItem)
1247 {
1248   if (!this.getMultiColumnSupport()) {
1249     return null;
1250   }
1251
1252   return !vItem ? this.getLast() : this.getPrevious(vItem);
1253 }
1254
1255 qx.Proto.getRight = function(vItem)
1256 {
1257   if (!this.getMultiColumnSupport()) {
1258     return null;
1259   }
1260
1261   return !vItem ? this.getFirst() : this.getNext(vItem);
1262 }
1263
1264 qx.Proto.getAbove = function(vItem)
1265 {
1266   throw new Error("getAbove(): Not implemented yet");
1267 }
1268
1269 qx.Proto.getUnder = function(vItem)
1270 {
1271   throw new Error("getUnder(): Not implemented yet");
1272 }
1273
1274
1275
1276
1277
1278
1279
1280 /*
1281 ---------------------------------------------------------------------------
1282   PAGE HANDLING
1283 ---------------------------------------------------------------------------
1284 */
1285
1286 /*!
1287 Jump a "page" up.
1288
1289 #param vItem[qx.ui.core.Widget]: Relative to this widget
1290 */
1291 qx.Proto.getPageUp = function(vItem)
1292 {
1293   var vBoundedWidget = this.getBoundedWidget();
1294   var vParentScrollTop = vBoundedWidget.getScrollTop();
1295   var vParentClientHeight = vBoundedWidget.getClientHeight();
1296
1297   // Find next item
1298   var newItem;
1299   var nextItem = this.getLeadItem();
1300   if (!nextItem) {
1301     nextItem = this.getFirst();
1302   }
1303
1304   // Normally we should reach the status "lead" for the
1305   // nextItem after two iterations.
1306   var tryLoops = 0;
1307   while (tryLoops < 2)
1308   {
1309     while (nextItem && (this.getItemTop(nextItem) - this.getItemHeight(nextItem) >= vParentScrollTop)) {
1310       nextItem = this.getUp(nextItem);
1311     }
1312
1313     // This should never occour after the fix above
1314     if (nextItem == null) {
1315       break;
1316     }
1317
1318     // If the nextItem is not anymore the leadItem
1319     // Means: There has occured a change.
1320     // We break here. This is normally the second step.
1321     if (nextItem != this.getLeadItem())
1322     {
1323       // be sure that the top is reached
1324       this.scrollItemIntoView(nextItem, true);
1325       break;
1326     }
1327
1328     // Update scrolling (this is normally the first step)
1329     // this.debug("Scroll-Up: " + (vParentScrollTop + vParentClientHeight - 2 * this.getItemHeight(nextItem)));
1330     vBoundedWidget.setScrollTop(vParentScrollTop - vParentClientHeight - this.getItemHeight(nextItem));
1331
1332     // Use the real applied value instead of the calulated above
1333     vParentScrollTop = vBoundedWidget.getScrollTop();
1334
1335     // Increment counter
1336     tryLoops++;
1337   }
1338
1339   return nextItem;
1340 }
1341
1342 /*!
1343 Jump a "page" down.
1344
1345 #param vItem[qx.ui.core.Widget]: Relative to this widget
1346 */
1347 qx.Proto.getPageDown = function(vItem)
1348 {
1349   var vBoundedWidget = this.getBoundedWidget();
1350   var vParentScrollTop = vBoundedWidget.getScrollTop();
1351   var vParentClientHeight = vBoundedWidget.getClientHeight();
1352
1353   // this.debug("Bound: " + (vBoundedWidget._getTargetNode() != vBoundedWidget.getElement()));
1354
1355   // this.debug("ClientHeight-1: " + vBoundedWidget._getTargetNode().clientHeight);
1356   // this.debug("ClientHeight-2: " + vBoundedWidget.getElement().clientHeight);
1357
1358   // Find next item
1359   var newItem;
1360   var nextItem = this.getLeadItem();
1361   if (!nextItem) {
1362     nextItem = this.getFirst();
1363   }
1364
1365   // Normally we should reach the status "lead" for the
1366   // nextItem after two iterations.
1367   var tryLoops = 0;
1368   while (tryLoops < 2)
1369   {
1370     // this.debug("Loop: " + tryLoops);
1371     // this.debug("Info: " + nextItem + " :: " + (this.getItemTop(nextItem) + (2 * this.getItemHeight(nextItem))) + " <> " + (vParentScrollTop + vParentClientHeight));
1372     // this.debug("Detail: " + vParentScrollTop + ", " + vParentClientHeight);
1373
1374     // Find next
1375     while (nextItem && ((this.getItemTop(nextItem) + (2 * this.getItemHeight(nextItem))) <= (vParentScrollTop + vParentClientHeight))) {
1376       nextItem = this.getDown(nextItem);
1377     }
1378
1379     // This should never occour after the fix above
1380     if (nextItem == null) {
1381       break;
1382     }
1383
1384     // If the nextItem is not anymore the leadItem
1385     // Means: There has occured a change.
1386     // We break here. This is normally the second step.
1387     if (nextItem != this.getLeadItem()) {
1388       break;
1389     }
1390
1391     // Update scrolling (this is normally the first step)
1392     // this.debug("Scroll-Down: " + (vParentScrollTop + vParentClientHeight - 2 * this.getItemHeight(nextItem)));
1393     vBoundedWidget.setScrollTop(vParentScrollTop + vParentClientHeight - 2 * this.getItemHeight(nextItem));
1394
1395     // Use the real applied value instead of the calulated above
1396     vParentScrollTop = vBoundedWidget.getScrollTop();
1397
1398     // Increment counter
1399     tryLoops++;
1400   }
1401
1402   //this.debug("Select: " + nextItem._labelObject.getHtml());
1403
1404   return nextItem;
1405 }
1406
1407
1408
1409
1410
1411
1412
1413
1414
1415
1416 /*
1417 ---------------------------------------------------------------------------
1418   DISPOSE
1419 ---------------------------------------------------------------------------
1420 */
1421
1422 qx.Proto.dispose = function()
1423 {
1424   if (this.getDisposed()) {
1425     return;
1426   }
1427
1428   if (this._selectedItems)
1429   {
1430     this._selectedItems.dispose();
1431     this._selectedItems = null;
1432   }
1433
1434   return qx.core.Target.prototype.dispose.call(this);
1435 }