1 /****************************************************************************
  2  Copyright (c) 2012 cocos2d-x.org
  3  Copyright (c) 2010 Sangwoo Im
  4 
  5  http://www.cocos2d-x.org
  6 
  7  Permission is hereby granted, free of charge, to any person obtaining a copy
  8  of this software and associated documentation files (the "Software"), to deal
  9  in the Software without restriction, including without limitation the rights
 10  to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 11  copies of the Software, and to permit persons to whom the Software is
 12  furnished to do so, subject to the following conditions:
 13 
 14  The above copyright notice and this permission notice shall be included in
 15  all copies or substantial portions of the Software.
 16 
 17  THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 18  IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 19  FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 20  AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 21  LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 22  OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
 23  THE SOFTWARE.
 24  ****************************************************************************/
 25 
 26 /**
 27  * The constant value of the fill style from top to bottom for cc.TableView
 28  * @constant
 29  * @type {number}
 30  */
 31 cc.TABLEVIEW_FILL_TOPDOWN = 0;
 32 
 33 /**
 34  * The constant value of the fill style from bottom to top for cc.TableView
 35  * @constant
 36  * @type {number}
 37  */
 38 cc.TABLEVIEW_FILL_BOTTOMUP = 1;
 39 
 40 /**
 41  * Abstract class for SWTableView cell node
 42  * @class
 43  * @abstract
 44  * @extend cc.Node
 45  *
 46  * @property {Number}   objectId    - The index used internally by SWTableView and its subclasses
 47  */
 48 cc.TableViewCell = cc.Node.extend(/** @lends cc.TableViewCell# */{
 49     _idx:0,
 50     _className:"TableViewCell",
 51 
 52     /**
 53      * The index used internally by SWTableView and its subclasses
 54      */
 55     getIdx:function () {
 56         return this._idx;
 57     },
 58     setIdx:function (idx) {
 59         this._idx = idx;
 60     },
 61 
 62     /**
 63      * Cleans up any resources linked to this cell and resets <code>idx</code> property.
 64      */
 65     reset:function () {
 66         this._idx = cc.INVALID_INDEX;
 67     },
 68 
 69     setObjectID:function (idx) {
 70         this._idx = idx;
 71     },
 72     getObjectID:function () {
 73         return this._idx;
 74     }
 75 });
 76 
 77 window._p = cc.TableViewCell.prototype;
 78 
 79 /** @expose */
 80 _p.objectId;
 81 cc.defineGetterSetter(_p, "objectId", _p.getObjectID, _p.setObjectID);
 82 
 83 delete window._p;
 84 
 85 /**
 86  * Sole purpose of this delegate is to single touch event in this version.
 87  */
 88 cc.TableViewDelegate = cc.ScrollViewDelegate.extend(/** @lends cc.TableViewDelegate# */{
 89     /**
 90      * Delegate to respond touch event
 91      *
 92      * @param {cc.TableView} table table contains the given cell
 93      * @param {cc.TableViewCell} cell  cell that is touched
 94      */
 95     tableCellTouched:function (table, cell) {
 96     },
 97 
 98     /**
 99      * Delegate to respond a table cell press event.
100      *
101      * @param {cc.TableView} table table contains the given cell
102      * @param {cc.TableViewCell} cell  cell that is pressed
103      */
104     tableCellHighlight:function(table, cell){
105     },
106 
107     /**
108      * Delegate to respond a table cell release event
109      *
110      * @param {cc.TableView} table table contains the given cell
111      * @param {cc.TableViewCell} cell  cell that is pressed
112      */
113     tableCellUnhighlight:function(table, cell){
114 
115     },
116 
117     /**
118      * <p>
119      * Delegate called when the cell is about to be recycled. Immediately                     <br/>
120      * after this call the cell will be removed from the scene graph and                      <br/>
121      * recycled.
122      * </p>
123      * @param table table contains the given cell
124      * @param cell  cell that is pressed
125      */
126     tableCellWillRecycle:function(table, cell){
127 
128     }
129 });
130 
131 /**
132  * Data source that governs table backend data.
133  */
134 cc.TableViewDataSource = cc.Class.extend(/** @lends cc.TableViewDataSource# */{
135     /**
136      * cell size for a given index
137      * @param {cc.TableView} table table to hold the instances of Class
138      * @param {Number} idx the index of a cell to get a size
139      * @return {cc.Size} size of a cell at given index
140      */
141     tableCellSizeForIndex:function(table, idx){
142         return this.cellSizeForTable(table);
143     },
144     /**
145      * cell height for a given table.
146      *
147      * @param {cc.TableView} table table to hold the instances of Class
148      * @return {cc.Size} cell size
149      */
150     cellSizeForTable:function (table) {
151         return cc.size(0,0);
152     },
153 
154     /**
155      * a cell instance at a given index
156      * @param {cc.TableView} table table to hold the instances of Class
157      * @param idx index to search for a cell
158      * @return {cc.TableView} cell found at idx
159      */
160     tableCellAtIndex:function (table, idx) {
161         return null;
162     },
163 
164     /**
165      * Returns number of cells in a given table view.
166      * @param {cc.TableView} table table to hold the instances of Class
167      * @return {Number} number of cells
168      */
169     numberOfCellsInTableView:function (table) {
170         return 0;
171     }
172 });
173 
174 /**
175  * UITableView counterpart for cocos2d for iphone.
176  * this is a very basic, minimal implementation to bring UITableView-like component into cocos2d world.
177  *
178  * @class
179  * @extend cc.ScrollView
180  *
181  * @property {cc.TableViewDataSource}   dataSource          - The data source of the table view
182  * @property {cc.TableViewDelegate}     delegate            - The event delegate of the table view
183  * @property {Number}                   verticalFillOrder   - The index to determine how cell is ordered and filled in the view
184  *
185  */
186 cc.TableView = cc.ScrollView.extend(/** @lends cc.TableView# */{
187     _vOrdering:null,
188     _indices:null,
189     _cellsFreed:null,
190     _dataSource:null,
191     _tableViewDelegate:null,
192     _oldDirection:null,
193     _cellsPositions:null,                       //vector with all cell positions
194     _touchedCell:null,
195 
196     ctor:function () {
197         cc.ScrollView.prototype.ctor.call(this);
198         this._oldDirection = cc.SCROLLVIEW_DIRECTION_NONE;
199         this._cellsPositions = [];
200     },
201 
202     __indexFromOffset:function (offset) {
203         var low = 0;
204         var high = this._dataSource.numberOfCellsInTableView(this) - 1;
205         var search;
206         switch (this.getDirection()) {
207             case cc.SCROLLVIEW_DIRECTION_HORIZONTAL:
208                 search = offset.x;
209                 break;
210             default:
211                 search = offset.y;
212                 break;
213         }
214 
215         var locCellsPositions = this._cellsPositions;
216         while (high >= low){
217             var index = 0|(low + (high - low) / 2);
218             var cellStart = locCellsPositions[index];
219             var cellEnd = locCellsPositions[index + 1];
220 
221             if (search >= cellStart && search <= cellEnd){
222                 return index;
223             } else if (search < cellStart){
224                 high = index - 1;
225             }else {
226                 low = index + 1;
227             }
228         }
229 
230         if (low <= 0)
231             return 0;
232         return -1;
233     },
234 
235     _indexFromOffset:function (offset) {
236         var locOffset = {x: offset.x, y: offset.y};
237         var locDataSource = this._dataSource;
238         var maxIdx = locDataSource.numberOfCellsInTableView(this) - 1;
239 
240         if (this._vOrdering === cc.TABLEVIEW_FILL_TOPDOWN)
241             locOffset.y = this.getContainer().getContentSize().height - locOffset.y;
242 
243         var index = this.__indexFromOffset(locOffset);
244         if (index != -1) {
245             index = Math.max(0, index);
246             if (index > maxIdx)
247                 index = cc.INVALID_INDEX;
248         }
249         return index;
250     },
251 
252     __offsetFromIndex:function (index) {
253         var offset;
254         switch (this.getDirection()) {
255             case cc.SCROLLVIEW_DIRECTION_HORIZONTAL:
256                 offset = cc.p(this._cellsPositions[index], 0);
257                 break;
258             default:
259                 offset = cc.p(0, this._cellsPositions[index]);
260                 break;
261         }
262 
263         return offset;
264     },
265 
266     _offsetFromIndex:function (index) {
267         var offset = this.__offsetFromIndex(index);
268 
269         var cellSize = this._dataSource.tableCellSizeForIndex(this, index);
270         if (this._vOrdering === cc.TABLEVIEW_FILL_TOPDOWN)
271             offset.y = this.getContainer().getContentSize().height - offset.y - cellSize.height;
272 
273         return offset;
274     },
275 
276     _updateCellPositions:function(){
277         var cellsCount = this._dataSource.numberOfCellsInTableView(this);
278         var locCellsPositions = this._cellsPositions;
279 
280         if (cellsCount > 0){
281             var currentPos = 0;
282             var cellSize, locDataSource = this._dataSource;
283             for (var i=0; i < cellsCount; i++) {
284                 locCellsPositions[i] = currentPos;
285                 cellSize = locDataSource.tableCellSizeForIndex(this, i);
286                 switch (this.getDirection()) {
287                     case cc.SCROLLVIEW_DIRECTION_HORIZONTAL:
288                         currentPos += cellSize.width;
289                         break;
290                     default:
291                         currentPos += cellSize.height;
292                         break;
293                 }
294             }
295             this._cellsPositions[cellsCount] = currentPos;//1 extra value allows us to get right/bottom of the last cell
296         }
297     },
298 
299     _updateContentSize:function () {
300         var size = cc.size(0, 0);
301 
302         var cellsCount = this._dataSource.numberOfCellsInTableView(this);
303 
304         if(cellsCount > 0){
305             var maxPosition = this._cellsPositions[cellsCount];
306             switch (this.getDirection()) {
307                 case cc.SCROLLVIEW_DIRECTION_HORIZONTAL:
308                     size = cc.size(maxPosition, this._viewSize.height);
309                     break;
310                 default:
311                     size = cc.size(this._viewSize.width, maxPosition);
312                     break;
313             }
314         }
315 
316         this.setContentSize(size);
317 
318         if (this._oldDirection != this._direction) {
319             if (this._direction == cc.SCROLLVIEW_DIRECTION_HORIZONTAL) {
320                 this.setContentOffset(cc.p(0, 0));
321             } else {
322                 this.setContentOffset(cc.p(0, this._getMinContainerOffset().y));
323             }
324             this._oldDirection = this._direction;
325         }
326     },
327 
328     _moveCellOutOfSight:function (cell) {
329         if(this._tableViewDelegate && this._tableViewDelegate.tableCellWillRecycle)
330             this._tableViewDelegate.tableCellWillRecycle(this, cell);
331 
332         this._cellsFreed.addObject(cell);
333         this._cellsUsed.removeSortedObject(cell);
334         cc.arrayRemoveObject(this._indices, cell.getIdx());
335 
336         cell.reset();
337         if (cell.getParent() == this.getContainer()) {
338             this.getContainer().removeChild(cell, true);
339         }
340     },
341 
342     _setIndexForCell:function (index, cell) {
343         cell.setAnchorPoint(0, 0);
344         cell.setPosition(this._offsetFromIndex(index));
345         cell.setIdx(index);
346     },
347 
348     _addCellIfNecessary:function (cell) {
349         if (cell.getParent() != this.getContainer()) {
350             this.getContainer().addChild(cell);
351         }
352         this._cellsUsed.insertSortedObject(cell);
353         var locIndices = this._indices, addIdx = cell.getIdx();
354         if(locIndices.indexOf(addIdx) == -1){
355             locIndices.push(addIdx);
356             //sort
357             locIndices.sort(function(a,b){return a-b;});
358         }
359     },
360 
361     /**
362      * data source
363      */
364     getDataSource:function () {
365         return this._dataSource;
366     },
367     setDataSource:function (source) {
368         this._dataSource = source;
369     },
370 
371     /**
372      * delegate
373      */
374     getDelegate:function () {
375         return this._tableViewDelegate;
376     },
377 
378     setDelegate:function (delegate) {
379         this._tableViewDelegate = delegate;
380     },
381 
382     /**
383      * determines how cell is ordered and filled in the view.
384      */
385     setVerticalFillOrder:function (fillOrder) {
386         if (this._vOrdering != fillOrder) {
387             this._vOrdering = fillOrder;
388             if (this._cellsUsed.count() > 0) {
389                 this.reloadData();
390             }
391         }
392     },
393     getVerticalFillOrder:function () {
394         return this._vOrdering;
395     },
396 
397     initWithViewSize:function (size, container) {
398         if (cc.ScrollView.prototype.initWithViewSize.call(this, size, container)) {
399             this._cellsUsed = new cc.ArrayForObjectSorting();
400             this._cellsFreed = new cc.ArrayForObjectSorting();
401             this._indices = [];
402             this._tableViewDelegate = null;
403             this._vOrdering = cc.TABLEVIEW_FILL_BOTTOMUP;
404             this.setDirection(cc.SCROLLVIEW_DIRECTION_VERTICAL);
405 
406             cc.ScrollView.prototype.setDelegate.call(this, this);
407             return true;
408         }
409         return false;
410     },
411 
412     /**
413      * Updates the content of the cell at a given index.
414      *
415      * @param idx index to find a cell
416      */
417     updateCellAtIndex:function (idx) {
418         if (idx == cc.INVALID_INDEX || idx > this._dataSource.numberOfCellsInTableView(this) - 1)
419             return;
420 
421         var cell = this.cellAtIndex(idx);
422         if (cell)
423             this._moveCellOutOfSight(cell);
424 
425         cell = this._dataSource.tableCellAtIndex(this, idx);
426         this._setIndexForCell(idx, cell);
427         this._addCellIfNecessary(cell);
428     },
429 
430     /**
431      * Inserts a new cell at a given index
432      *
433      * @param idx location to insert
434      */
435     insertCellAtIndex:function (idx) {
436         if (idx == cc.INVALID_INDEX || idx > this._dataSource.numberOfCellsInTableView(this) - 1)
437             return;
438 
439         var newIdx, locCellsUsed = this._cellsUsed;
440         var cell = locCellsUsed.objectWithObjectID(idx);
441         if (cell) {
442             newIdx = locCellsUsed.indexOfSortedObject(cell);
443             for (var i = newIdx; i < locCellsUsed.count(); i++) {
444                 cell = locCellsUsed.objectAtIndex(i);
445                 this._setIndexForCell(cell.getIdx() + 1, cell);
446             }
447         }
448 
449         //insert a new cell
450         cell = this._dataSource.tableCellAtIndex(this, idx);
451         this._setIndexForCell(idx, cell);
452         this._addCellIfNecessary(cell);
453 
454         this._updateCellPositions();
455         this._updateContentSize();
456     },
457 
458     /**
459      * Removes a cell at a given index
460      *
461      * @param idx index to find a cell
462      */
463     removeCellAtIndex:function (idx) {
464         if (idx == cc.INVALID_INDEX || idx > this._dataSource.numberOfCellsInTableView(this) - 1)
465             return;
466 
467         var cell = this.cellAtIndex(idx);
468         if (!cell)
469             return;
470 
471         var locCellsUsed = this._cellsUsed;
472         var newIdx = locCellsUsed.indexOfSortedObject(cell);
473 
474         //remove first
475         this._moveCellOutOfSight(cell);
476         cc.arrayRemoveObject(this._indices, idx);
477         this._updateCellPositions();
478 
479         for (var i = locCellsUsed.count() - 1; i > newIdx; i--) {
480             cell = locCellsUsed.objectAtIndex(i);
481             this._setIndexForCell(cell.getIdx() - 1, cell);
482         }
483     },
484 
485     /**
486      * reloads data from data source.  the view will be refreshed.
487      */
488     reloadData:function () {
489         this._oldDirection = cc.SCROLLVIEW_DIRECTION_NONE;
490         var locCellsUsed = this._cellsUsed, locCellsFreed = this._cellsFreed, locContainer = this.getContainer();
491         for (var i = 0, len = locCellsUsed.count(); i < len; i++) {
492             var cell = locCellsUsed.objectAtIndex(i);
493 
494             if(this._tableViewDelegate && this._tableViewDelegate.tableCellWillRecycle)
495                 this._tableViewDelegate.tableCellWillRecycle(this, cell);
496 
497             locCellsFreed.addObject(cell);
498             cell.reset();
499             if (cell.getParent() == locContainer)
500                 locContainer.removeChild(cell, true);
501         }
502 
503         this._indices = [];
504         this._cellsUsed = new cc.ArrayForObjectSorting();
505 
506         this._updateCellPositions();
507         this._updateContentSize();
508         if (this._dataSource.numberOfCellsInTableView(this) > 0)
509             this.scrollViewDidScroll(this);
510     },
511 
512     /**
513      * Dequeues a free cell if available. nil if not.
514      *
515      * @return {TableViewCell} free cell
516      */
517     dequeueCell:function () {
518         if (this._cellsFreed.count() === 0) {
519             return null;
520         } else {
521             var cell = this._cellsFreed.objectAtIndex(0);
522             this._cellsFreed.removeObjectAtIndex(0);
523             return cell;
524         }
525     },
526 
527     /**
528      * Returns an existing cell at a given index. Returns nil if a cell is nonexistent at the moment of query.
529      *
530      * @param idx index
531      * @return {cc.TableViewCell} a cell at a given index
532      */
533     cellAtIndex:function (idx) {
534         var i = this._indices.indexOf(idx);
535         if (i == -1)
536             return null;
537         return this._cellsUsed.objectWithObjectID(idx);
538     },
539 
540     scrollViewDidScroll:function (view) {
541         var locDataSource = this._dataSource;
542         var countOfItems = locDataSource.numberOfCellsInTableView(this);
543         if (0 === countOfItems)
544             return;
545 
546         if (this._tableViewDelegate != null && this._tableViewDelegate.scrollViewDidScroll)
547             this._tableViewDelegate.scrollViewDidScroll(this);
548 
549         var  idx = 0, locViewSize = this._viewSize, locContainer = this.getContainer();
550         var offset = this.getContentOffset();
551         offset.x *= -1;
552         offset.y *= -1;
553 
554         var maxIdx = Math.max(countOfItems-1, 0);
555 
556         if (this._vOrdering === cc.TABLEVIEW_FILL_TOPDOWN)
557             offset.y = offset.y + locViewSize.height/locContainer.getScaleY();
558         var startIdx = this._indexFromOffset(offset);
559         if (startIdx === cc.INVALID_INDEX)
560             startIdx = countOfItems - 1;
561 
562         if (this._vOrdering === cc.TABLEVIEW_FILL_TOPDOWN)
563             offset.y -= locViewSize.height/locContainer.getScaleY();
564         else
565             offset.y += locViewSize.height/locContainer.getScaleY();
566         offset.x += locViewSize.width/locContainer.getScaleX();
567 
568         var endIdx = this._indexFromOffset(offset);
569         if (endIdx === cc.INVALID_INDEX)
570             endIdx = countOfItems - 1;
571 
572         var cell, locCellsUsed = this._cellsUsed;
573         if (locCellsUsed.count() > 0) {
574             cell = locCellsUsed.objectAtIndex(0);
575             idx = cell.getIdx();
576             while (idx < startIdx) {
577                 this._moveCellOutOfSight(cell);
578                 if (locCellsUsed.count() > 0) {
579                     cell = locCellsUsed.objectAtIndex(0);
580                     idx = cell.getIdx();
581                 } else
582                     break;
583             }
584         }
585 
586         if (locCellsUsed.count() > 0) {
587             cell = locCellsUsed.lastObject();
588             idx = cell.getIdx();
589             while (idx <= maxIdx && idx > endIdx) {
590                 this._moveCellOutOfSight(cell);
591                 if (locCellsUsed.count() > 0) {
592                     cell = locCellsUsed.lastObject();
593                     idx = cell.getIdx();
594                 } else
595                     break;
596             }
597         }
598 
599         var locIndices = this._indices;
600         for (var i = startIdx; i <= endIdx; i++) {
601             if (locIndices.indexOf(i) != -1)
602                 continue;
603             this.updateCellAtIndex(i);
604         }
605     },
606 
607     scrollViewDidZoom:function (view) {
608     },
609 
610     onTouchEnded:function (touch, event) {
611         if (!this.isVisible())
612             return;
613 
614         if (this._touchedCell){
615             var bb = this.getBoundingBox();
616             bb._origin = this._parent.convertToWorldSpace(bb._origin);
617             var locTableViewDelegate = this._tableViewDelegate;
618             if (cc.rectContainsPoint(bb, touch.getLocation()) && locTableViewDelegate != null){
619                 if(locTableViewDelegate.tableCellUnhighlight)
620                     locTableViewDelegate.tableCellUnhighlight(this, this._touchedCell);
621                 if(locTableViewDelegate.tableCellTouched)
622                     locTableViewDelegate.tableCellTouched(this, this._touchedCell);
623             }
624             this._touchedCell = null;
625         }
626         cc.ScrollView.prototype.onTouchEnded.call(this, touch, event);
627     },
628 
629     onTouchBegan:function(touch, event){
630         if (!this.isVisible())
631             return false;
632 
633         var touchResult = cc.ScrollView.prototype.onTouchBegan.call(this, touch, event);
634 
635         if(this._touches.length === 1) {
636             var index, point;
637 
638             point = this.getContainer().convertTouchToNodeSpace(touch);
639 
640             index = this._indexFromOffset(point);
641             if (index === cc.INVALID_INDEX)
642                 this._touchedCell = null;
643             else
644                 this._touchedCell  = this.cellAtIndex(index);
645 
646             if (this._touchedCell && this._tableViewDelegate != null && this._tableViewDelegate.tableCellHighlight)
647                 this._tableViewDelegate.tableCellHighlight(this, this._touchedCell);
648         } else if(this._touchedCell) {
649             if(this._tableViewDelegate != null && this._tableViewDelegate.tableCellUnhighlight)
650                 this._tableViewDelegate.tableCellUnhighlight(this, this._touchedCell);
651             this._touchedCell = null;
652         }
653 
654         return touchResult;
655     },
656 
657     onTouchMoved: function(touch, event){
658         cc.ScrollView.prototype.onTouchMoved.call(this, touch, event);
659 
660         if (this._touchedCell && this.isTouchMoved()) {
661             if(this._tableViewDelegate != null && this._tableViewDelegate.tableCellUnhighlight)
662                 this._tableViewDelegate.tableCellUnhighlight(this, this._touchedCell);
663             this._touchedCell = null;
664         }
665     },
666 
667     onTouchCancelled: function(touch, event){
668         cc.ScrollView.prototype.onTouchCancelled.call(this, touch, event);
669 
670         if (this._touchedCell) {
671             if(this._tableViewDelegate != null && this._tableViewDelegate.tableCellUnhighlight)
672                 this._tableViewDelegate.tableCellUnhighlight(this, this._touchedCell);
673             this._touchedCell = null;
674         }
675     }
676 });
677 
678 window._p = cc.TableView.prototype;
679 
680 /** @expose */
681 _p.dataSource;
682 cc.defineGetterSetter(_p, "dataSource", _p.getDataSource, _p.setDataSource);
683 /** @expose */
684 _p.delegate;
685 cc.defineGetterSetter(_p, "delegate", _p.getDelegate, _p.setDelegate);
686 /** @expose */
687 _p.verticalFillOrder;
688 cc.defineGetterSetter(_p, "verticalFillOrder", _p.getVerticalFillOrder, _p.setVerticalFillOrder);
689 
690 delete window._p;
691 
692 /**
693  * An initialized table view object
694  *
695  * @param {cc.TableViewDataSource} dataSource data source;
696  * @param {cc.Size} size view size
697  * @param {cc.Node} [container] parent object for cells
698  * @return {cc.TableView} table view
699  */
700 cc.TableView.create = function (dataSource, size, container) {
701     var table = new cc.TableView();
702     table.initWithViewSize(size, container);
703     table.setDataSource(dataSource);
704     table._updateCellPositions();
705     table._updateContentSize();
706     return table;
707 };
708