Skip to content

Instantly share code, notes, and snippets.

@mieko
Created September 11, 2012 09:00
Show Gist options
  • Star 2 You must be signed in to star a gist
  • Fork 6 You must be signed in to fork a gist
  • Save mieko/3697056 to your computer and use it in GitHub Desktop.
Save mieko/3697056 to your computer and use it in GitHub Desktop.
CCTableView with variable-size cells
/****************************************************************************
Copyright (c) 2012 cocos2d-x.org
Copyright (c) 2012 Mike Owens <mike@filespanker>
Copyright (c) 2010 Sangwoo Im
http://www.cocos2d-x.org
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
****************************************************************************/
#include "cocos2d.h"
#include "CCTableView.h"
#include "CCTableViewCell.h"
#include "menu_nodes/CCMenu.h"
#include "support/CCPointExtension.h"
#include "CCSorting.h"
#include "layers_scenes_transitions_nodes/CCLayer.h"
NS_CC_EXT_BEGIN
bool CCTableViewDataSource::hasFixedCellSize()
{
return true;
}
CCSize CCTableViewDataSource::cellSizeForIndex(CCTableView *table, unsigned int idx)
{
return cellSizeForTable(table);
}
CCTableView* CCTableView::create(CCTableViewDataSource* dataSource, CCSize size)
{
return CCTableView::create(dataSource, size, NULL);
}
CCTableView* CCTableView::create(CCTableViewDataSource* dataSource, CCSize size, CCNode *container)
{
CCTableView *table = new CCTableView();
table->initWithViewSize(size, container);
table->autorelease();
table->setDataSource(dataSource);
table->_updateContentSize();
return table;
}
bool CCTableView::initWithViewSize(CCSize size, CCNode* container/* = NULL*/)
{
if (CCScrollView::initWithViewSize(size,container))
{
m_pCellsUsed = new CCArrayForObjectSorting();
m_pCellsFreed = new CCArrayForObjectSorting();
m_pIndices = new std::set<unsigned int>();
m_pTableViewDelegate = NULL;
m_eVordering = kCCTableViewFillBottomUp;
this->setDirection(kCCScrollViewDirectionVertical);
CCScrollView::setDelegate(this);
return true;
}
return false;
}
CCTableView::CCTableView()
: m_pIndices(NULL)
, m_pCellsUsed(NULL)
, m_pCellsFreed(NULL)
, m_pDataSource(NULL)
, m_pTableViewDelegate(NULL)
, m_eOldDirection(kCCScrollViewDirectionNone)
{
}
CCTableView::~CCTableView()
{
CC_SAFE_DELETE(m_pIndices);
CC_SAFE_RELEASE(m_pCellsUsed);
CC_SAFE_RELEASE(m_pCellsFreed);
}
void CCTableView::setVerticalFillOrder(CCTableViewVerticalFillOrder fillOrder)
{
if (m_eVordering != fillOrder) {
m_eVordering = fillOrder;
if (m_pCellsUsed->count() > 0) {
this->reloadData();
}
}
}
CCTableViewVerticalFillOrder CCTableView::getVerticalFillOrder()
{
return m_eVordering;
}
void CCTableView::reloadData()
{
CCObject* pObj = NULL;
CCARRAY_FOREACH(m_pCellsUsed, pObj)
{
CCTableViewCell* cell = (CCTableViewCell*)pObj;
m_pCellsFreed->addObject(cell);
cell->reset();
if (cell->getParent() == this->getContainer())
{
this->getContainer()->removeChild(cell, true);
}
}
m_pIndices->clear();
m_pCellsUsed->release();
m_pCellsUsed = new CCArrayForObjectSorting();
this->_updateContentSize();
if (m_pDataSource->numberOfCellsInTableView(this) > 0)
{
this->scrollViewDidScroll(this);
}
}
CCTableViewCell *CCTableView::cellAtIndex(unsigned int idx)
{
return this->_cellWithIndex(idx);
}
void CCTableView::updateCellAtIndex(unsigned int idx)
{
if (idx == CC_INVALID_INDEX || idx > m_pDataSource->numberOfCellsInTableView(this)-1)
{
return;
}
CCTableViewCell *cell;
cell = this->_cellWithIndex(idx);
if (cell) {
this->_moveCellOutOfSight(cell);
}
cell = m_pDataSource->tableCellAtIndex(this, idx);
this->_setIndexForCell(idx, cell);
this->_addCellIfNecessary(cell);
}
void CCTableView::insertCellAtIndex(unsigned int idx)
{
if (idx == CC_INVALID_INDEX || idx > m_pDataSource->numberOfCellsInTableView(this)-1) {
return;
}
CCTableViewCell *cell;
int newIdx;
cell = (CCTableViewCell*)m_pCellsUsed->objectWithObjectID(idx);
if (cell)
{
newIdx = m_pCellsUsed->indexOfSortedObject(cell);
for (unsigned int i=newIdx; i<m_pCellsUsed->count(); i++)
{
cell = (CCTableViewCell*)m_pCellsUsed->objectAtIndex(i);
this->_setIndexForCell(cell->getIdx()+1, cell);
}
}
// [m_pIndices shiftIndexesStartingAtIndex:idx by:1];
//insert a new cell
cell = m_pDataSource->tableCellAtIndex(this, idx);
this->_setIndexForCell(idx, cell);
this->_addCellIfNecessary(cell);
this->_updateContentSize();
}
void CCTableView::removeCellAtIndex(unsigned int idx)
{
if (idx == CC_INVALID_INDEX || idx > m_pDataSource->numberOfCellsInTableView(this)-1) {
return;
}
CCTableViewCell *cell;
unsigned int newIdx;
cell = this->_cellWithIndex(idx);
if (!cell) {
return;
}
newIdx = m_pCellsUsed->indexOfSortedObject(cell);
//remove first
this->_moveCellOutOfSight(cell);
m_pIndices->erase(idx);
// [m_pIndices shiftIndexesStartingAtIndex:idx+1 by:-1];
for (unsigned int i=m_pCellsUsed->count()-1; i > newIdx; i--) {
cell = (CCTableViewCell*)m_pCellsUsed->objectAtIndex(i);
this->_setIndexForCell(cell->getIdx()-1, cell);
}
}
CCTableViewCell *CCTableView::dequeueCell()
{
CCTableViewCell *cell;
if (m_pCellsFreed->count() == 0) {
cell = NULL;
} else {
cell = (CCTableViewCell*)m_pCellsFreed->objectAtIndex(0);
cell->retain();
m_pCellsFreed->removeObjectAtIndex(0);
cell->autorelease();
}
return cell;
}
void CCTableView::_addCellIfNecessary(CCTableViewCell * cell)
{
if (cell->getParent() != this->getContainer())
{
this->getContainer()->addChild(cell);
}
m_pCellsUsed->insertSortedObject(cell);
m_pIndices->insert(cell->getIdx());
// [m_pIndices addIndex:cell.idx];
}
void CCTableView::_updateContentSize()
{
CCSize size;
if (m_pDataSource->hasFixedCellSize())
{
CCSize cellSize = m_pDataSource->cellSizeForTable(this);
unsigned int cellCount = m_pDataSource->numberOfCellsInTableView(this);
switch (this->getDirection())
{
case kCCScrollViewDirectionHorizontal:
size = CCSizeMake(cellCount * cellSize.width, cellSize.height);
break;
default:
size = CCSizeMake(cellSize.width, cellCount * cellSize.height);
break;
}
}
else
{
float w = 0, h = 0;
unsigned int cellCount = m_pDataSource->numberOfCellsInTableView(this);
for (size_t i = 0; i != cellCount; ++i) {
CCSize cellSize = m_pDataSource->cellSizeForIndex(this, i);
switch (this->getDirection())
{
case kCCScrollViewDirectionHorizontal:
h = fmax(h, cellSize.height);
w += cellSize.width;
break;
default:
h += cellSize.height;
w = fmax(w, cellSize.width);
break;
}
}
size = CCSizeMake(w, h);
}
this->setContentSize(size);
if (m_eOldDirection != m_eDirection)
{
if (m_eDirection == kCCScrollViewDirectionHorizontal)
{
this->setContentOffset(ccp(0,0));
}
else
{
this->setContentOffset(ccp(0,this->minContainerOffset().y));
}
m_eOldDirection = m_eDirection;
}
}
CCPoint CCTableView::_offsetFromIndex(unsigned int index)
{
CCPoint offset = this->__offsetFromIndex(index);
const CCSize cellSize = m_pDataSource->cellSizeForIndex(this, index);
if (m_eVordering == kCCTableViewFillTopDown) {
offset.y = this->getContainer()->getContentSize().height - offset.y - cellSize.height;
}
return offset;
}
CCPoint CCTableView::__offsetFromIndex(unsigned int index)
{
CCPoint offset;
CCSize cellSize;
if (m_pDataSource->hasFixedCellSize()) {
cellSize = m_pDataSource->cellSizeForTable(this);
switch (this->getDirection()) {
case kCCScrollViewDirectionHorizontal:
offset = ccp(cellSize.width * index, 0.0f);
break;
default:
offset = ccp(0.0f, cellSize.height * index);
break;
}
} else {
float w = 0, h = 0;
for (size_t i = 0; i != index; ++i) {
CCSize cellSize = m_pDataSource->cellSizeForIndex(this, i);
w += cellSize.width;
h += cellSize.height;
}
switch (this->getDirection()) {
case kCCScrollViewDirectionHorizontal:
offset = ccp(w, 0.0f);
break;
default:
offset = ccp(0.0f, h);
break;
}
}
return offset;
}
unsigned int CCTableView::_indexFromOffset(CCPoint offset)
{
/* FIXME: merge once tested */
if (m_pDataSource->hasFixedCellSize())
{
int index = 0;
const int maxIdx = m_pDataSource->numberOfCellsInTableView(this)-1;
const CCSize cellSize = m_pDataSource->cellSizeForTable(this);
if (m_eVordering == kCCTableViewFillTopDown) {
offset.y = this->getContainer()->getContentSize().height - offset.y - cellSize.height;
}
index = MAX(0, this->__indexFromOffset(offset));
index = MIN(index, maxIdx);
return index;
} else {
int maxIdx = m_pDataSource->numberOfCellsInTableView(this) - 1;
if (m_eVordering == kCCTableViewFillTopDown) {
float extra = maxIdx > 0 ? m_pDataSource->cellSizeForIndex(this, maxIdx).height : 0;
offset.y = this->getContainer()->getContentSize().height - offset.y - extra;
}
int index = MAX(0, __indexFromOffset(offset));
index = MIN(index, maxIdx);
return index;
}
}
int CCTableView::__indexFromOffset(CCPoint offset)
{
if (m_pDataSource->hasFixedCellSize()) {
int index = 0;
CCSize cellSize;
cellSize = m_pDataSource->cellSizeForTable(this);
switch (this->getDirection()) {
case kCCScrollViewDirectionHorizontal:
index = offset.x/cellSize.width;
break;
default:
index = offset.y/cellSize.height;
break;
}
return index;
} else {
float target = getDirection() == kCCScrollViewDirectionHorizontal ? offset.x : offset.y;
float off = 0;
for (size_t i = 0; i != m_pDataSource->numberOfCellsInTableView(this); ++i) {
if (off > target) {
CCAssert(i >= 0, "weird index");
return MAX(i - 1, 0);
}
switch(this->getDirection()) {
case kCCScrollViewDirectionHorizontal:
off += m_pDataSource->cellSizeForIndex(this, i).width;
break;
default:
off += m_pDataSource->cellSizeForIndex(this, i).height;
break;
}
}
return m_pDataSource->numberOfCellsInTableView(this)-1;
}
}
CCTableViewCell* CCTableView::_cellWithIndex(unsigned int cellIndex)
{
CCTableViewCell *found;
found = NULL;
// if ([m_pIndices containsIndex:cellIndex])
if (m_pIndices->find(cellIndex) != m_pIndices->end())
{
found = (CCTableViewCell *)m_pCellsUsed->objectWithObjectID(cellIndex);
}
return found;
}
void CCTableView::_moveCellOutOfSight(CCTableViewCell *cell)
{
m_pCellsFreed->addObject(cell);
m_pCellsUsed->removeSortedObject(cell);
m_pIndices->erase(cell->getIdx());
// [m_pIndices removeIndex:cell.idx];
cell->reset();
if (cell->getParent() == this->getContainer()) {
this->getContainer()->removeChild(cell, true);;
}
}
void CCTableView::_setIndexForCell(unsigned int index, CCTableViewCell *cell)
{
cell->setAnchorPoint(ccp(0.0f, 0.0f));
cell->setPosition(this->_offsetFromIndex(index));
cell->setIdx(index);
}
void CCTableView::scrollViewDidScroll(CCScrollView* view)
{
unsigned int startIdx = 0, endIdx = 0, idx = 0, maxIdx = 0;
CCPoint offset;
offset = ccpMult(this->getContentOffset(), -1);
maxIdx = MAX(m_pDataSource->numberOfCellsInTableView(this)-1, 0);
const CCSize cellSize = m_pDataSource->cellSizeForTable(this);
if (m_eVordering == kCCTableViewFillTopDown) {
float extra = 0;
if (m_pDataSource->hasFixedCellSize()) {
extra = cellSize.height;
} else {
extra = maxIdx > 0 ? m_pDataSource->cellSizeForIndex(this, maxIdx).height : 0;
}
offset.y = offset.y + m_tViewSize.height/this->getContainer()->getScaleY() - extra;
}
startIdx = this->_indexFromOffset(offset);
if (m_eVordering == kCCTableViewFillTopDown)
{
offset.y -= m_tViewSize.height/this->getContainer()->getScaleY();
}
else
{
offset.y += m_tViewSize.height/this->getContainer()->getScaleY();
}
offset.x += m_tViewSize.width/this->getContainer()->getScaleX();
endIdx = this->_indexFromOffset(offset);
#if 0 // For Testing.
CCObject* pObj;
int i = 0;
CCARRAY_FOREACH(m_pCellsUsed, pObj)
{
CCTableViewCell* pCell = (CCTableViewCell*)pObj;
CCLog("cells Used index %d, value = %d", i, pCell->getIdx());
i++;
}
CCLog("---------------------------------------");
i = 0;
CCARRAY_FOREACH(m_pCellsFreed, pObj)
{
CCTableViewCell* pCell = (CCTableViewCell*)pObj;
CCLog("cells freed index %d, value = %d", i, pCell->getIdx());
i++;
}
CCLog("~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~");
#endif
if (m_pCellsUsed->count() > 0)
{
CCTableViewCell* cell = (CCTableViewCell*)m_pCellsUsed->objectAtIndex(0);
idx = cell->getIdx();
while(idx < startIdx)
{
this->_moveCellOutOfSight(cell);
if (m_pCellsUsed->count() > 0)
{
cell = (CCTableViewCell*)m_pCellsUsed->objectAtIndex(0);
idx = cell->getIdx();
}
else
{
break;
}
}
}
if (m_pCellsUsed->count() > 0)
{
CCTableViewCell *cell = (CCTableViewCell*)m_pCellsUsed->lastObject();
idx = cell->getIdx();
while(idx <= maxIdx && idx > endIdx)
{
this->_moveCellOutOfSight(cell);
if (m_pCellsUsed->count() > 0)
{
cell = (CCTableViewCell*)m_pCellsUsed->lastObject();
idx = cell->getIdx();
}
else
{
break;
}
}
}
for (unsigned int i=startIdx; i <= endIdx; i++)
{
//if ([m_pIndices containsIndex:i])
if (m_pIndices->find(i) != m_pIndices->end())
{
continue;
}
this->updateCellAtIndex(i);
}
}
void CCTableView::ccTouchEnded(CCTouch *pTouch, CCEvent *pEvent)
{
if (!this->isVisible()) {
return;
}
if (m_pTouches->count() == 1 && !this->isTouchMoved()) {
unsigned int index;
CCTableViewCell *cell;
CCPoint point;
point = this->getContainer()->convertTouchToNodeSpace(pTouch);
if (m_eVordering == kCCTableViewFillTopDown) {
float extra = m_pDataSource->cellSizeForTable(this).height;
unsigned cell_ct = m_pDataSource->numberOfCellsInTableView(this);
if (! m_pDataSource->hasFixedCellSize()) {
if (cell_ct < 1) {
extra = 0;
} else {
extra = m_pDataSource->cellSizeForIndex(this, cell_ct-1).height;
}
}
point.y -= extra;
}
index = this->_indexFromOffset(point);
cell = this->_cellWithIndex(index);
if (cell) {
m_pTableViewDelegate->tableCellTouched(this, cell);
}
}
CCScrollView::ccTouchEnded(pTouch, pEvent);
}
NS_CC_EXT_END
/****************************************************************************
Copyright (c) 2012 cocos2d-x.org
Copyright (c) 2012 Mike Owens <mike@filespanker>
Copyright (c) 2010 Sangwoo Im
http://www.cocos2d-x.org
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
****************************************************************************/
#ifndef __CCTABLEVIEW_H__
#define __CCTABLEVIEW_H__
#include "CCScrollView.h"
#include "CCTableViewCell.h"
#include <set>
NS_CC_EXT_BEGIN
class CCTableView;
class CCArrayForObjectSorting;
typedef enum {
kCCTableViewFillTopDown,
kCCTableViewFillBottomUp
} CCTableViewVerticalFillOrder;
/**
* Sole purpose of this delegate is to single touch event in this version.
*/
class CCTableViewDelegate : public CCScrollViewDelegate
{
public:
/**
* Delegate to respond touch event
*
* @param table table contains the given cell
* @param cell cell that is touched
*/
virtual void tableCellTouched(CCTableView* table, CCTableViewCell* cell) = 0;
};
/**
* Data source that governs table backend data.
*/
class CCTableViewDataSource
{
public:
/**
* cell height for a given table.
*
* @param table table to hold the instances of Class
* @return cell size
*/
virtual CCSize cellSizeForTable(CCTableView *table) = 0;
/**
* a cell instance at a given index
*
* @param idx index to search for a cell
* @return cell found at idx
*/
virtual CCTableViewCell* tableCellAtIndex(CCTableView *table, unsigned int idx) = 0;
/**
* Returns number of cells in a given table view.
*
* @return number of cells
*/
virtual unsigned int numberOfCellsInTableView(CCTableView *table) = 0;
/**
* Asserts that each cell has a constant size.
* Returning false from this method enables a table scan, and may have
* performance penalties for larger data sets. The per-cell size will
* be calculated by calling cellSizeForIndex
* Defaults to true.
*
* @return true iff cells in the table will be of constant size
*/
virtual bool hasFixedCellSize();
/**
* Called to determine the size of a cell at index. This is only
* useful if hasFixedCellSize() returns false. By default, returns
* cellSizeForTable()
*/
virtual CCSize cellSizeForIndex(CCTableView *table, unsigned int idx);
};
/**
* UITableView counterpart for cocos2d for iphone.
*
* this is a very basic, minimal implementation to bring UITableView-like component into cocos2d world.
*
*/
class CCTableView : public CCScrollView, public CCScrollViewDelegate
{
public:
CCTableView();
virtual ~CCTableView();
/**
* An intialized table view object
*
* @param dataSource data source
* @param size view size
* @return table view
*/
static CCTableView* create(CCTableViewDataSource* dataSource, CCSize size);
/**
* An initialized table view object
*
* @param dataSource data source;
* @param size view size
* @param container parent object for cells
* @return table view
*/
static CCTableView* create(CCTableViewDataSource* dataSource, CCSize size, CCNode *container);
/**
* data source
*/
CCTableViewDataSource* getDataSource() { return m_pDataSource; }
void setDataSource(CCTableViewDataSource* source) { m_pDataSource = source; }
/**
* delegate
*/
CCTableViewDelegate* getDelegate() { return m_pTableViewDelegate; }
void setDelegate(CCTableViewDelegate* pDelegate) { m_pTableViewDelegate = pDelegate; }
/**
* determines how cell is ordered and filled in the view.
*/
void setVerticalFillOrder(CCTableViewVerticalFillOrder order);
CCTableViewVerticalFillOrder getVerticalFillOrder();
bool initWithViewSize(CCSize size, CCNode* container = NULL);
/**
* Updates the content of the cell at a given index.
*
* @param idx index to find a cell
*/
void updateCellAtIndex(unsigned int idx);
/**
* Inserts a new cell at a given index
*
* @param idx location to insert
*/
void insertCellAtIndex(unsigned int idx);
/**
* Removes a cell at a given index
*
* @param idx index to find a cell
*/
void removeCellAtIndex(unsigned int idx);
/**
* reloads data from data source. the view will be refreshed.
*/
void reloadData();
/**
* Dequeues a free cell if available. nil if not.
*
* @return free cell
*/
CCTableViewCell *dequeueCell();
/**
* Returns an existing cell at a given index. Returns nil if a cell is nonexistent at the moment of query.
*
* @param idx index
* @return a cell at a given index
*/
CCTableViewCell *cellAtIndex(unsigned int idx);
virtual void scrollViewDidScroll(CCScrollView* view);
virtual void scrollViewDidZoom(CCScrollView* view) {}
virtual void ccTouchEnded(CCTouch *pTouch, CCEvent *pEvent);
protected:
/**
* vertical direction of cell filling
*/
CCTableViewVerticalFillOrder m_eVordering;
/**
* index set to query the indexes of the cells used.
*/
std::set<unsigned int>* m_pIndices;
//NSMutableIndexSet *indices_;
/**
* cells that are currently in the table
*/
CCArrayForObjectSorting* m_pCellsUsed;
/**
* free list of cells
*/
CCArrayForObjectSorting* m_pCellsFreed;
/**
* weak link to the data source object
*/
CCTableViewDataSource* m_pDataSource;
/**
* weak link to the delegate object
*/
CCTableViewDelegate* m_pTableViewDelegate;
CCScrollViewDirection m_eOldDirection;
int __indexFromOffset(CCPoint offset);
unsigned int _indexFromOffset(CCPoint offset);
CCPoint __offsetFromIndex(unsigned int index);
CCPoint _offsetFromIndex(unsigned int index);
void _updateContentSize();
CCTableViewCell* _cellWithIndex(unsigned int cellIndex);
void _moveCellOutOfSight(CCTableViewCell *cell);
void _setIndexForCell(unsigned int index, CCTableViewCell *cell);
void _addCellIfNecessary(CCTableViewCell * cell);
};
NS_CC_EXT_END
#endif /* __CCTABLEVIEW_H__ */
@dumganhar
Copy link

Hey @mieko,
Do you still focus on this thread(http://www.cocos2d-x.org/boards/6/topics/15559?r=19304#message-19304)?
Some developers has asked for this feature.
So do you have any time to make this demo and send it to us now?

Best Regards.
James

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment