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