r21248: - Test for Simo: no change of selected row(s) upon open/close click. Simo,
[Samba/bb.git] / webapps / qooxdoo-0.6.5-sdk / frontend / framework / source / class / qx / ui / treevirtual / TreeVirtual.js
blob39e9a1c54f4373381d976dcfff2b9b62a63a5a06
1 /* ************************************************************************
3    qooxdoo - the new era of web development
5    http://qooxdoo.org
7    Copyright:
8      2007 Derrell Lipman
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.
15    Authors:
16      * Derrell Lipman (derrell)
18 ************************************************************************ */
20 /* ************************************************************************
22 #module(treevirtual)
24 ************************************************************************ */
26 /**
27  * A "virtual" tree
28  *
29  * @event treeOpenWithContent {qx.event.type.DataEvent}
30  * @event treeOpenWhileEmpty {qx.event.type.DataEvent}
31  * @event treeClose {qx.event.type.DataEvent}
32  * @event changeSelection {qx.event.type.Event}
33  *
34  * WARNING: This widget is in active development and the interface to it is
35  *          very likely to change, possibly on a daily basis, for a while.  Do
36  *          not use this widget yet.
37  *
38  */
39 qx.OO.defineClass("qx.ui.treevirtual.TreeVirtual", qx.ui.table.Table,
40 function(headings)
42   // Create a table model
43   var tableModel = new qx.ui.treevirtual.SimpleTreeDataModel();
45   // Specify the column headings.  We accept a single string (one single
46   // column) or an array of strings (one or more columns).
47   if (typeof(headings) == "string")
48   {
49     headings = [ headings ];
50   }
51   tableModel.setColumns(headings);
53   this.setNewSelectionManager(
54       function(obj)
55       {
56         return new qx.ui.treevirtual.SelectionManager(obj);
57       });
59   // Call our superclass constructor
60   qx.ui.table.Table.call(this, tableModel);
62   // Set sizes
63   this.setRowHeight(16);
64   this.setMetaColumnCounts([1, -1]);
66   // Set the data cell render.  We use the SimpleTreeDataCellRenderer for the
67   // tree column, and our DefaultDataCellRenderer for all other columns.
68   var stdcr = new qx.ui.treevirtual.SimpleTreeDataCellRenderer();
69   var ddcr = new qx.ui.treevirtual.DefaultDataCellRenderer();
70   var tcm = this.getTableColumnModel();
71   var treeCol = this.getTableModel().getTreeColumn();
72   for (var i = 0; i < headings.length; i++)
73   {
74     tcm.setDataCellRenderer(i, i == treeCol ? stdcr : ddcr);
75   }
77   // Set the data row renderer.
78   this.setDataRowRenderer(new qx.ui.treevirtual.SimpleTreeDataRowRenderer());
80   // We need our cell renderer called on selection change, to update the icon
81   this.setAlwaysUpdateCells(true);
83   // Move the focus with the mouse
84   this.setFocusCellOnMouseMove(true);
86   // Change focus colors.  Make them less obtrusive.
87   this.setRowColors(
88     {
89       bgcolFocused             : "#f0f0f0",
90       bgcolFocusedBlur         : "#f0f0f0"
91     });
93   // Set the cell focus color
94   this.setCellFocusAttributes({ backgroundColor : "lightblue" });
97   // Use this instead, to help determine which does what
98   this.setRowColors(
99     {
100       bgcolFocusedSelected     : "cyan",
101       bgcolFocusedSelectedBlur : "green",
102       bgcolFocused             : "yellow",
103       bgcolFocusedBlur         : "blue",
104       bgcolSelected            : "red",
105       bgcolSelectedBlur        : "pink",
106     });
109   // Get the list of pane scrollers
110   var scrollers = this._getPaneScrollerArr();
112   // For each scroller...
113   for (var i = 0; i < scrollers.length; i++)
114   {
115     // ... remove the outline on focus, 
116     scrollers[i]._focusIndicator.setAppearance("treevirtual-focus-indicator");
118     // ... and set the pane scrollers to handle the selection before
119     // displaying the focus, so we can manipulate the selected icon.
120     scrollers[i].setSelectBeforeFocus(true);
121   }
126  * Whether a click on the open/close button should also cause selection of the
127  * row.
128  */
129 qx.OO.addProperty(
130   {
131     name         : "openCloseClickSelectsRow",
132     type         : "boolean",
133     defaultValue : false,
134     getAlias     : "openCloseClickSelectsRow"
135   });
139  * Return the data model for this tree.
140  */
141 qx.Proto.getDataModel = function()
143   return this.getTableModel();
148  * Set whether lines linking tree children shall be drawn on the tree.
150  * @param b {Boolean}
151  *   <i>true</i> if tree lines should be shown; <i>false</i> otherwise.
152  */
153 qx.Proto.setUseTreeLines = function(b)
155   var stdcm = this.getTableModel();
156   var treeCol = stdcm.getTreeColumn();
157   var dcr = this.getTableColumnModel().getDataCellRenderer(treeCol);
158   dcr.setUseTreeLines(b);
160   // Inform the listeners
161   if (stdcm.hasEventListeners(qx.ui.table.TableModel.EVENT_TYPE_DATA_CHANGED))
162   {
163     var data =
164       {
165         firstRow        : 0,
166         lastRow         : stdcm._rowArr.length - 1,
167         firstColumn     : 0,
168         lastColumn      : stdcm.getColumnCount() - 1
169       };
171     stdcm.dispatchEvent(new qx.event.type.DataEvent(
172                           qx.ui.table.TableModel.EVENT_TYPE_DATA_CHANGED,
173                           data),
174                         true);
175   }
180  * Get whether lines linking tree children shall be drawn on the tree.
182  * @return {Boolean}
183  *   <i>true</i> if tree lines are in use; <i>false</i> otherwise.
184  */
185 qx.Proto.getUseTreeLines = function()
187   var treeCol = this.getTableModel().getTreeColumn();
188   var dcr = this.getTableColumnModel().getDataCellRenderer(treeCol);
189   return dcr.getUseTreeLines();
194  * Set whether the open/close button should be displayed on a branch, even if
195  * the branch has no children.
197  * @param b {Boolean}
198  *   <i>true</i> if the open/close button should be shown; <i>false</i>
199  *   otherwise.
200  */
201 qx.Proto.setAlwaysShowOpenCloseSymbol = function(b)
203   var stdcm = this.getTableModel();
204   var treeCol = stdcm.getTreeColumn();
205   var dcr = this.getTableColumnModel().getDataCellRenderer(treeCol);
206   dcr.setAlwaysShowOpenCloseSymbol(b);
208   // Inform the listeners
209   if (stdcm.hasEventListeners(qx.ui.table.TableModel.EVENT_TYPE_DATA_CHANGED))
210   {
211     var data =
212       {
213         firstRow        : 0,
214         lastRow         : stdcm._rowArr.length - 1,
215         firstColumn     : 0,
216         lastColumn      : stdcm.getColumnCount() - 1
217       };
219     stdcm.dispatchEvent(new qx.event.type.DataEvent(
220                           qx.ui.table.TableModel.EVENT_TYPE_DATA_CHANGED,
221                           data),
222                         true);
223   }
228  * Set whether drawing of first-level tree-node lines are disabled even if
229  * drawing of tree lines is enabled.  (See also @link {#setUseTreeLines})
231  * @param b {Boolean}
232  *   <i>true</i> if first-level tree lines should be disabled;
233  *   <i>false</i> for normal operation.
234  */
235 qx.Proto.setExcludeFirstLevelTreeLines = function(b)
237   var stdcm = this.getTableModel();
238   var treeCol = stdcm.getTreeColumn();
239   var dcr = this.getTableColumnModel().getDataCellRenderer(treeCol);
240   dcr.setExcludeFirstLevelTreeLines(b);
242   // Inform the listeners
243   if (stdcm.hasEventListeners(qx.ui.table.TableModel.EVENT_TYPE_DATA_CHANGED))
244   {
245     var data =
246       {
247         firstRow        : 0,
248         lastRow         : stdcm._rowArr.length - 1,
249         firstColumn     : 0,
250         lastColumn      : stdcm.getColumnCount() - 1
251       };
253     stdcm.dispatchEvent(new qx.event.type.DataEvent(
254                           qx.ui.table.TableModel.EVENT_TYPE_DATA_CHANGED,
255                           data),
256                         true);
257   }
262  * Get whether drawing of first-level tree lines should be disabled even if
263  * drawing of tree lines is enabled.  (See also {@link #getUseTreeLines})
265  * @return {Boolean}
266  *   <i>true</i> if tree lines are in use; <i>false</i> otherwise.
267  */
268 qx.Proto.getExcludeFirstLevelTreeLines = function()
270   var treeCol = this.getTableModel().getTreeColumn();
271   var dcr = this.getTableColumnModel().getDataCellRenderer(treeCol);
272   return dcr.getExcludeFirstLevelTreeLines();
277  * Set whether the open/close button should be displayed on a branch, even if
278  * the branch has no children.
280  * @return {Boolean}
281  *   <i>true</i> if tree lines are in use; <i>false</i> otherwise.
282  */
283 qx.Proto.getAlwaysShowOpenCloseSymbol = function()
285   var treeCol = this.getTableModel().getTreeColumn();
286   var dcr = this.getTableColumnModel().getDataCellRenderer(treeCol);
287   return dcr.getAlwaysShowOpenCloseSymbol();
292  * Set the selection mode.
294  * @param mode {Integer}
295  *   The selection mode to be used.  It may be any of:
296  *   <pre>
297  *     qx.ui.treevirtual.SelectionMode.NONE:
298  *        Nothing can ever be selected.
300  *     qx.ui.treevirtual.SelectionMode.SINGLE
301  *        Allow only one selected item.
303  *     qx.ui.treevirtual.SelectionMode.SINGLE_INTERVAL
304  *        Allow one contiguous interval of selected items.
306  *     qx.ui.treevirtual.SelectionMode.MULTIPLE_INTERVAL
307  *        Allow any selected items, whether contiguous or not.
308  *   </pre>
309  */
310 qx.Proto.setSelectionMode = function(mode)
312   this.getSelectionModel().setSelectionMode(mode);
316  * Get the selection mode currently in use.
318  * @return {Integer}
319  *   One of the values documented in {@link #setSelectionMode}
320  */
321 qx.Proto.getSelectionMode = function(mode)
323   return this.getSelectionModel().getSelectionMode();
328  * Toggle the opened state of the node: if the node is opened, close
329  * it; if it is closed, open it.
331  * @param node {Object}
332  *   The object representing the node to have its opened/closed state
333  *   toggled.
334  */
335 qx.Proto.toggleOpened = function(node)
337   // Are we opening or closing?
338   if (node.bOpened)
339   {
340     // We're closing.  If there are listeners, generate a treeClose event.
341     this.createDispatchDataEvent("treeClose", node);
342   }
343   else
344   {
345     // We're opening.  Are there any children?
346     if (node.children.length > 0)
347     {
348       // Yup.  If there any listeners, generate a "treeOpenWithContent" event.
349       this.createDispatchDataEvent("treeOpenWithContent", node);
350     }
351     else
352     {
353       // No children.  If there are listeners, generate a "treeOpenWhileEmpty"
354       // event.
355       this.createDispatchDataEvent("treeOpenWhileEmpty", node);
356     }
357   }
359   // Event handler may have modified the opened state.  Check before toggling.
360   if (! node.bHideOpenClose)
361   {
362     // It's still boolean.  Toggle the state
363     node.bOpened = ! node.bOpened;
365     // Get the selection model
366     var sm = this.getSelectionModel();
368     // Get the data model
369     var dm = this.getTableModel();
371     // Determine if this node was selected
372     var rowIndex = dm.getNodeRowMap()[node.nodeId];
374     // Is this row already selected?
375     var bSelected = sm.isSelectedIndex(rowIndex);
377     // Clear the old selections in the tree
378     this.getSelectionModel()._clearSelection();
381     // Clear the old selections in the data model
382     dm._clearSelections();
384     // If this row was selected, re-select it
385     if (bSelected)
386     {
387       this.setState(node.nodeId, { bSelected : true });
388     }
390   }
392   // Re-render the row data since formerly visible rows may now be invisible,
393   // or vice versa.
394   this.getTableModel().setData();
399  * Set state attributes of a tree node.
401  * @param nodeId {Integer}
402  *   The node identifier (returned by addBranch() or addLeaf()) representing
403  *   the node for which attributes are being set.
405  * @param attributes {Map}
406  *   Map with the node properties to be set.  The map may contain any of the
407  *   properties described in {@link qx.ui.treevirtual.SimpleTreeDataModel}
408  */
409 qx.Proto.setState = function(nodeId, attributes)
411   this.getTableModel().setState(nodeId, attributes);
416  * Allow setting the tree row colors.
418  * @param colors {Map}
419  *    The value of each property in the map is a string containing either a
420  *    number (e.g. "#518ad3") or color name ("white") representing the color
421  *    for that type of display.  The map may contain any or all of the
422  *    following properties:
423  *    <ul>
424  *      <li>bgcolFocusedSelected</li>
425  *      <li>bgcolFocusedSelectedBlur</li>
426  *      <li>bgcolFocused</li>
427  *      <li>bgcolFocusedBlur</li>
428  *      <li>bgcolSelected</li>
429  *      <li>bgcolSelectedBlur</li>
430  *      <li>bgcolEven</li>
431  *      <li>bgcolOdd</li>
432  *      <li>colSelected</li>
433  *      <li>colNormal</li>
434  *    </ul>
435  */
436 qx.Proto.setRowColors = function(colors)
438   this.getDataRowRenderer().setRowColors(colors);
443  * Set the attributes used to indicate the cell that has the focus.
445  * @param attributes {Map}
446  *   The set of attributes that the cell focus indicator should have.  This is
447  *   in the format required to call the <i>set()</i> method of a widget, e.g.
448  *   <p>
449  *   { backgroundColor: blue }
450  *   <p>
451  *   If not otherwise specified, the opacity is set to 0.2 so that the cell
452  *   data can be seen "through" the cell focus indicator which overlays it.
453  *   <p>
454  *   For no visible focus indicator, use { backgroundColor : "transparent" }
455  *   <p>
456  *   The focus indicator is a box the size of the cell, which overlays the
457  *   cell itself.  There is no text in the focus indicator itself, so it makes
458  *   no sense to set the color attribute or any other attribute that affects
459  *   fonts.
460  */
461 qx.Proto.setCellFocusAttributes = function(attributes)
463   // Add an opacity attribute so what's below the focus can be seen
464   if (! attributes.opacity)
465   {
466     attributes.opacity = 0.2;
467   }
469   var scrollers = this._getPaneScrollerArr();
470   for (var i = 0; i < scrollers.length; i++)
471   {
472     scrollers[i]._focusIndicator.set(attributes);
473   }  
478  * Event handler. Called when a key was pressed.
480  * We handle the Enter key to toggle opened/closed tree state.  All
481  * other keydown events are passed to our superclass.
483  * @param evt {Map} the event.
484  */
485 qx.Proto._onkeydown = function(evt)
487   if (! this.getEnabled()) {
488     return;
489   }
491   var identifier = evt.getKeyIdentifier();
493   var consumed = false;
494   var modifiers = evt.getModifiers();
495   if (modifiers == 0)
496   {
497     switch (identifier)
498     {
499     case "Enter":
500       // Get the data model
501       var dm = this.getTableModel();
503       // Get the focused node
504       var focusedRow = this.getFocusedRow();
505       var treeCol = dm.getTreeColumn();
506       var node = dm.getValue(treeCol, focusedRow);
508       if (! node.bHideOpenClose)
509       {
510         this.toggleOpened(node);
511       }
512       consumed = true;
513       break;
515     case "Left":
516       this.moveFocusedCell(-1, 0);
517       break;
519     case "Right":
520       this.moveFocusedCell(1, 0);
521       break;
522     }
523   }
524   else if (modifiers == qx.event.type.DomEvent.CTRL_MASK)
525   {
526     switch (identifier)
527     {
528     case "Left":
529       // Get the data model
530       var dm = this.getTableModel();
532       // Get the focused node
533       var focusedRow = this.getFocusedRow();
534       var treeCol = dm.getTreeColumn();
535       var node = dm.getValue(treeCol, focusedRow);
537       // If it's an open branch and open/close is allowed...
538       if (node.type == qx.ui.treevirtual.SimpleTreeDataModel.Type.BRANCH &&
539           ! node.bHideOpenClose &&
540           node.bOpened)
541       {
542         // ... then close it
543         this.toggleOpened(node);
544       }
545     
546       // Reset the focus to the current node
547       this.setFocusedCell(treeCol, focusedRow, true);
549       consumed = true;
550       break;
552     case "Right":
553       // Get the data model
554       var dm = this.getTableModel();
556       // Get the focused node
557       var focusedRow = this.getFocusedRow();
558       var treeCol = dm.getTreeColumn();
559       var node = dm.getValue(treeCol, focusedRow);
561       // If it's a closed branch and open/close is allowed...
562       if (node.type == qx.ui.treevirtual.SimpleTreeDataModel.Type.BRANCH &&
563           ! node.bHideOpenClose &&
564           ! node.bOpened)
565       {
566         // ... then open it
567         this.toggleOpened(node);
568       }
570       // Reset the focus to the current node
571       this.setFocusedCell(treeCol, focusedRow, true);
572     
573       consumed = true;
574       break;
575     }
576   }
577   else if (modifiers == qx.event.type.DomEvent.SHIFT_MASK)
578   {
579     switch (identifier)
580     {
581       case "Left":
582       // Get the data model
583       var dm = this.getTableModel();
585       // Get the focused node
586       var focusedRow = this.getFocusedRow();
587       var treeCol = dm.getTreeColumn();
588       var node = dm.getValue(treeCol, focusedRow);
590       // If we're not at the top-level already...
591       if (node.parentNodeId)
592       {
593         // Find out what rendered row our parent node is at
594         var rowIndex = dm.getNodeRowMap()[node.parentNodeId];
595       
596         // Set the focus to our parent
597         this.setFocusedCell(this._focusedCol, rowIndex, true);
598       }
599       
600       consumed = true;
601       break;
603       case "Right":
604       // Get the data model
605       var dm = this.getTableModel();
607       // Get the focused node
608       var focusedRow = this.getFocusedRow();
609       var treeCol = dm.getTreeColumn();
610       var node = dm.getValue(treeCol, focusedRow);
612       // If we're on a branch and open/close is allowed...
613       if (node.type == qx.ui.treevirtual.SimpleTreeDataModel.Type.BRANCH &&
614           ! node.bHideOpenClose)
615       {
616         // ... then first ensure the branch is open
617         if (! node.bOpened)
618         {
619           this.toggleOpened(node);
620         }
622         // If this node has children...
623         if (node.children.length > 0)
624         {
625           // ... then move the focus to the first child
626           this.moveFocusedCell(0, 1);
627         }
628       }
629       
630       consumed = true;
631       break;
632     }
633   }
635   // Was this one of our events that we handled?
636   if (consumed)
637   {
638     // Yup.  Don't propagate it.
639     evt.preventDefault();
640     evt.stopPropagation();
641   }
642   else
643   {
644     // It's not one of ours.  Let our superclass handle this event
645     qx.ui.table.Table.prototype._onkeydown.call(this, evt);
646   }
650 qx.Proto._onkeypress = function(evt)
652   if (! this.getEnabled()) {
653     return;
654   }
656   var consumed = false;
658   // Handle keys that are independant from the modifiers
659   var identifier = evt.getKeyIdentifier();
660   switch (identifier)
661   {
662     // Ignore events we already handled in _onkeydown
663     case "Left":
664     case "Right":
665       consumed = true;
666       break;
667   }
669   if (consumed)
670   {
671     evt.preventDefault();
672     evt.stopPropagation();
673   }
674   else
675   {
676     // Let our superclass handle this event
677     qx.ui.table.Table.prototype._onkeypress.call(this, evt);
678   }
684  * Event handler. Called when the selection has changed.
686  * @param evt {Map} the event.
687  */
688 qx.Proto._onSelectionChanged = function(evt)
690   // Clear the old list of selected nodes
691   this.getTableModel()._clearSelections();
693   // If selections are allowed, pass an event to our listeners
694   if (this.getSelectionMode() !=
695       qx.ui.treevirtual.TreeVirtual.SelectionMode.NONE)
696   {
697     var selectedNodes = this._calculateSelectedNodes();
699     // Get the now-focused
700     this.createDispatchDataEvent("changeSelection", selectedNodes);
701   }
703   // Call the superclass method
704   qx.ui.table.Table.prototype._onSelectionChanged.call(this, evt);
709  * Obtain the entire hierarchy of labels from the root down to the specified
710  * node.
712  * @param nodeId {Integer}
713  *   The node id of the node for which the hierarchy is desired.
715  * @return {Array}
716  *   The returned array contains one string for each label in the hierarchy of
717  *   the node specified by the parameter.  Element 0 of the array contains the
718  *   label of the root node, element 1 contains the label of the node
719  *   immediately below root in the specified node's hierarchy, etc., down to
720  *   the last element in the array contain the label of the node referenced by
721  *   the parameter.
722  */
723 qx.Proto.getHierarchy = function(nodeId)
725   var _this = this;
726   var components = [ ];
728   function addHierarchy(nodeId)
729   {
730     // If we're at the root...
731     if (! nodeId)
732     {
733       // ... then we're done
734       return;
735     }
737     // Get the requested node
738     var node = _this.getTableModel().getData()[nodeId];
740     // Add its label to the hierarchy components
741     components.unshift(node.label);
743     // Call recursively to our parent node.
744     addHierarchy(node.parentNodeId);
745   }
747   addHierarchy(nodeId);
748   return components;
753  * Calculate and return the set of nodes which are currently selected by the
754  * user, on the screen.  In the process of calculating which nodes are
755  * selected, the nodes corresponding to the selected rows on the screen are
756  * marked as selected by setting their <i>bSelected</i> property to true, and
757  * all previously-selected nodes have their <i>bSelected</i> property reset to
758  * false.
760  * @return {Array}
761  *   An array of nodes matching the set of rows which are selected on the
762  *   screen. 
763  */
764 qx.Proto._calculateSelectedNodes = function()
766   // Create an array of nodes that are now selected
767   var stdcm = this.getTableModel();
768   var selectedRanges = this.getSelectionModel().getSelectedRanges();
769   var selectedNodes = [ ];
770   var node;
772   for (var i = 0; i < selectedRanges.length; i++)
773   {
774     for (var j = selectedRanges[i].minIndex;
775          j <= selectedRanges[i].maxIndex;
776          j++)
777     {
778       node = stdcm.getValue(stdcm.getTreeColumn(), j);
779       stdcm.setState(node.nodeId, { bSelected : true });
780       selectedNodes.push(node);
781     }
782   }
784   return selectedNodes;
789  * Return the nodes that are currently selected.
791  * @return {Array}
792  *   An array containing the nodes that are currently selected.
793  */
794 qx.Proto.getSelectedNodes = function()
796   return this.getTableModel().getSelectedNodes();
801  * Selection Modes {int}
803  *   NONE
804  *     Nothing can ever be selected.
806  *   SINGLE
807  *     Allow only one selected item.
809  *   SINGLE_INTERVAL
810  *     Allow one contiguous interval of selected items.
812  *   MULTIPLE_INTERVAL
813  *     Allow any set of selected items, whether contiguous or not.
814  */
815 qx.Class.SelectionMode =
817   NONE              :
818     qx.ui.table.SelectionModel.NO_SELECTION,
820   SINGLE            :
821     qx.ui.table.SelectionModel.SINGLE_SELECTION,
823   SINGLE_INTERVAL   :
824     qx.ui.table.SelectionModel.SINGLE_INTERVAL_SELECTION,
826   MULTIPLE_INTERVAL :
827     qx.ui.table.SelectionModel.MULTIPLE_INTERVAL_SELECTION