diff --git a/escher/Makefile b/escher/Makefile index 57a76c332..82220c591 100644 --- a/escher/Makefile +++ b/escher/Makefile @@ -21,6 +21,7 @@ objs += $(addprefix escher/src/,\ tab_view.o\ tab_view_cell.o\ tab_view_controller.o\ + table_view.o\ table_view_cell.o\ text_field.o\ text_view.o\ diff --git a/escher/include/escher.h b/escher/include/escher.h index 0d19e8a96..d60d09190 100644 --- a/escher/include/escher.h +++ b/escher/include/escher.h @@ -20,6 +20,7 @@ #include #include #include +#include #include #include #include diff --git a/escher/include/escher/table_view.h b/escher/include/escher/table_view.h new file mode 100644 index 000000000..24ed7ab3c --- /dev/null +++ b/escher/include/escher/table_view.h @@ -0,0 +1,87 @@ +#ifndef ESCHER_TABLE_VIEW_H +#define ESCHER_TABLE_VIEW_H + +#include + +class TableViewDataSource { +public: + virtual int numberOfRows() = 0; + virtual int numberOfColumns() = 0; + virtual void willDisplayCellAtLocation(View * cell, int i, int j); + virtual KDCoordinate columnWidth(int i) = 0; + virtual KDCoordinate rowHeight(int j) = 0; + /* return the number of pixels to include in offset to display the column i at + the top */ + virtual KDCoordinate cumulatedWidthFromIndex(int i) = 0; + virtual KDCoordinate cumulatedHeightFromIndex(int j) = 0; + /* return the number of columns (starting with first ones) that can be fully + * displayed in offsetX pixels. + * Caution: if the offset is exactly the size of n columns, the function + * returns n-1. */ + virtual int indexFromCumulatedWidth(KDCoordinate offsetX) = 0; + virtual int indexFromCumulatedHeight(KDCoordinate offsetY) = 0; + virtual View * reusableCell(int index, int type) = 0; + virtual int reusableCellCount(int type) = 0; + virtual int typeAtLocation(int i, int j) = 0; +}; + +class TableView : public ScrollView { +public: + TableView(TableViewDataSource * dataSource, KDCoordinate topMargin = 0, + KDCoordinate rightMargin = 0, KDCoordinate bottomMargin = 0, KDCoordinate leftMargin = 0); + + void scrollToCell(int i, int j); + View * cellAtLocation(int i, int j); +protected: +#if ESCHER_VIEW_LOGGING + const char * className() const override; +#endif +private: + class ContentView : public View { + public: + ContentView(TableView * tableView, TableViewDataSource * dataSource); + + KDCoordinate height() const; + KDCoordinate width() const; + void scrollToCell(int i, int j) const; + View * cellAtLocation(int i, int j); + protected: +#if ESCHER_VIEW_LOGGING + const char * className() const override; +#endif + private: + int numberOfSubviews() const override; + View * subviewAtIndex(int index) override; + void layoutSubviews() override; + + /* realCellWidth enables to handle list view for which + * TableViewDataSource->cellWidht = 0 */ + KDCoordinate columnWidth(int x) const; + /* These two methods transform an index (of subview for instance) into + * coordinates that refer to the data source entire table */ + int absoluteColumnNumberFromSubviewIndex(int index) const; + int absoluteRowNumberFromSubviewIndex(int index) const; + int numberOfFullyDisplayableRows() const; + int numberOfFullyDisplayableColumns() const; + int numberOfDisplayableRows() const; + int numberOfDisplayableColumns() const; + int rowsScrollingOffset() const; + int columnsScrollingOffset() const; + bool rowAtIndexIsBeforeFullyVisibleRange(int index) const; + bool columnAtIndexIsBeforeFullyVisibleRange(int index) const; + bool rowAtIndexIsAfterFullyVisibleRange(int index) const; + bool columnAtIndexIsAfterFullyVisibleRange(int index) const; + int typeOfSubviewAtIndex(int index) const; + /* This method transform a index (of subview for instance) into an index + * refering to the set of cells of type "type". */ + int typeIndexFromSubviewIndex(int index, int type) const; + TableView * m_tableView; + TableViewDataSource * m_dataSource; + }; + + void layoutSubviews() override; + + ContentView m_contentView; +}; + +#endif diff --git a/escher/src/table_view.cpp b/escher/src/table_view.cpp new file mode 100644 index 000000000..567310328 --- /dev/null +++ b/escher/src/table_view.cpp @@ -0,0 +1,238 @@ +#include +#include + +extern "C" { +#include +} + +#define MIN(x,y) ((x)<(y) ? (x) : (y)) + +void TableViewDataSource::willDisplayCellAtLocation(View * cell, int i, int j) { +} + +TableView::TableView(TableViewDataSource * dataSource, KDCoordinate topMargin, KDCoordinate rightMargin, + KDCoordinate bottomMargin, KDCoordinate leftMargin) : + ScrollView(&m_contentView, topMargin, rightMargin, bottomMargin, leftMargin), + m_contentView(TableView::ContentView(this, dataSource)) +{ +} + +// This method computes the minimal scrolling needed to properly display the +// requested cell. +void TableView::scrollToCell(int i, int j) { + m_contentView.scrollToCell(i, j); +} + +View * TableView::cellAtLocation(int i, int j) { + return m_contentView.cellAtLocation(i, j); +} + +#if ESCHER_VIEW_LOGGING +const char * TableView::className() const { + return "TableView"; +} +#endif + +void TableView::layoutSubviews() { + // We only have to layout our contentView. + // We will size it here, and ScrollView::layoutSubviews will position it. + + KDRect contentViewFrame(0, 0, m_contentView.width(), m_contentView.height()); + m_contentView.setFrame(contentViewFrame); + + ScrollView::layoutSubviews(); +} + +/* TableView::ContentView */ + +TableView::ContentView::ContentView(TableView * tableView, TableViewDataSource * dataSource) : + View(), + m_tableView(tableView), + m_dataSource(dataSource) +{ +} + +KDCoordinate TableView::ContentView::columnWidth(int i) const { + int columnWidth = m_dataSource->columnWidth(i); + columnWidth = columnWidth ? columnWidth : m_tableView->maxContentWidthDisplayableWithoutScrolling(); + return columnWidth; +} + + +KDCoordinate TableView::ContentView::height() const { + return m_dataSource->cumulatedHeightFromIndex(m_dataSource->numberOfRows()); +} + +KDCoordinate TableView::ContentView::width() const { + int result = m_dataSource->cumulatedWidthFromIndex(m_dataSource->numberOfColumns()); + // handle the case of list: cumulatedWidthFromIndex() = 0 + return result ? result : m_tableView->maxContentWidthDisplayableWithoutScrolling(); +} + +void TableView::ContentView::scrollToCell(int x, int y) const { + KDCoordinate contentOffsetX = m_tableView->contentOffset().x(); + KDCoordinate contentOffsetY = m_tableView->contentOffset().y(); + if (columnAtIndexIsBeforeFullyVisibleRange(x)) { + // Let's scroll the tableView to put that cell on the left (while keeping the left margin) + contentOffsetX = m_dataSource->cumulatedWidthFromIndex(x); + } else if (columnAtIndexIsAfterFullyVisibleRange(x)) { + // Let's scroll the tableView to put that cell on the right (while keeping the right margin) + contentOffsetX = m_dataSource->cumulatedWidthFromIndex(x+1)-m_tableView->maxContentWidthDisplayableWithoutScrolling(); + } + if (rowAtIndexIsBeforeFullyVisibleRange(y)) { + // Let's scroll the tableView to put that cell on the top (while keeping the top margin) + contentOffsetY = m_dataSource->cumulatedHeightFromIndex(y); + } else if (rowAtIndexIsAfterFullyVisibleRange(y)) { + // Let's scroll the tableView to put that cell on the bottom (while keeping the bottom margin) + contentOffsetY = m_dataSource->cumulatedHeightFromIndex(y+1) - m_tableView->maxContentHeightDisplayableWithoutScrolling(); + } + m_tableView->setContentOffset(KDPoint(contentOffsetX, contentOffsetY)); +} + +int TableView::ContentView::typeOfSubviewAtIndex(int index) const { + assert(index >= 0); + int i = absoluteColumnNumberFromSubviewIndex(index); + int j = absoluteRowNumberFromSubviewIndex(index); + int type = m_dataSource->typeAtLocation(i, j); + return type; +} + +int TableView::ContentView::typeIndexFromSubviewIndex(int index, int type) const { + int typeIndex = 0; + for (int k = 0; k < index; k++) { + if (typeOfSubviewAtIndex(k) == type) { + typeIndex++; + } + } + assert(typeIndex < m_dataSource->reusableCellCount(type)); + return typeIndex; +} + +View * TableView::ContentView::cellAtLocation(int x, int y) { + int relativeX = x-columnsScrollingOffset(); + int relativeY = y-rowsScrollingOffset(); + int type = m_dataSource->typeAtLocation(x, y); + int index = relativeY*numberOfDisplayableColumns()+relativeX; + int typeIndex = typeIndexFromSubviewIndex(index, type); + return m_dataSource->reusableCell(typeIndex, type); +} + +#if ESCHER_VIEW_LOGGING +const char * TableView::ContentView::className() const { + return "TableView::ContentView"; +} +#endif + +int TableView::ContentView::numberOfSubviews() const { + int result = numberOfDisplayableRows() * numberOfDisplayableColumns(); + return result; +} + +int TableView::ContentView::absoluteColumnNumberFromSubviewIndex(int index) const { + /* "x = i % columns" but we avoid a call to modulo not to implement + * "__aeabi_idivmod" */ + int j = index / numberOfDisplayableColumns(); + int i = index - j * numberOfDisplayableColumns(); + int columnOffset = columnsScrollingOffset(); + return i + columnOffset; +} + +int TableView::ContentView::absoluteRowNumberFromSubviewIndex(int index) const { + int j = index / numberOfDisplayableColumns(); + int rowOffset = rowsScrollingOffset(); + return j + rowOffset; +} + +View * TableView::ContentView::subviewAtIndex(int index) { + int type = typeOfSubviewAtIndex(index); + int typeIndex = typeIndexFromSubviewIndex(index, type); + return m_dataSource->reusableCell(typeIndex, type); +} + +void TableView::ContentView::layoutSubviews() { + for (int index=0; indexrowHeight(j); + KDCoordinate columnWidth = this->columnWidth(i); + KDCoordinate verticalOffset = m_dataSource->cumulatedHeightFromIndex(j); + KDCoordinate horizontalOffset = m_dataSource->cumulatedWidthFromIndex(i); + KDRect cellFrame(horizontalOffset, verticalOffset, + columnWidth, rowHeight); + + cell->setFrame(cellFrame); + + m_dataSource->willDisplayCellAtLocation(cell, i, j); + } +} + + +int TableView::ContentView::numberOfFullyDisplayableRows() const { + // The number of displayable rows taking into accounts margins + int rowOffsetWithMargin = m_dataSource->indexFromCumulatedHeight(m_tableView->contentOffset().y() + + m_tableView->topMargin()); + int displayedHeightWithOffsetAndMargin = m_dataSource->indexFromCumulatedHeight(m_tableView->maxContentHeightDisplayableWithoutScrolling() + + m_tableView->contentOffset().y() + m_tableView->topMargin()); + return displayedHeightWithOffsetAndMargin - rowOffsetWithMargin; +} + +int TableView::ContentView::numberOfFullyDisplayableColumns() const { + // The number of displayable rows taking into accounts margins + int columnOffsetWithMargin = m_dataSource->indexFromCumulatedWidth(m_tableView->contentOffset().x() + + m_tableView->leftMargin()); + int displayedWidthWithOffsetAndMargin = m_dataSource->indexFromCumulatedWidth(m_tableView->maxContentWidthDisplayableWithoutScrolling() + + m_tableView->contentOffset().x() + m_tableView->leftMargin()); + return displayedWidthWithOffsetAndMargin - columnOffsetWithMargin; +} + +int TableView::ContentView::numberOfDisplayableRows() const { + int rowOffset = rowsScrollingOffset(); + int displayedHeightWithOffset = m_dataSource->indexFromCumulatedHeight(m_tableView->bounds().height() + m_tableView->contentOffset().y()); + return MIN( + m_dataSource->numberOfRows(), + displayedHeightWithOffset + 1 + ) - rowOffset; +} + +int TableView::ContentView::numberOfDisplayableColumns() const { + int columnOffset = columnsScrollingOffset(); + int displayedWidthWithOffset = m_dataSource->indexFromCumulatedWidth(m_tableView->bounds().width() + m_tableView->contentOffset().x()); + return MIN( + m_dataSource->numberOfColumns(), + displayedWidthWithOffset + 1 + ) - columnOffset; +} + +int TableView::ContentView::rowsScrollingOffset() const { + /* Here, we want to translate the offset at which our tableView is displaying + * us into an integer offset we can use to ask cells to our data source. */ + return m_dataSource->indexFromCumulatedHeight(m_tableView->contentOffset().y()); +} + +int TableView::ContentView::columnsScrollingOffset() const { + /* Here, we want to translate the offset at which our tableView is displaying + * us into an integer offset we can use to ask cells to our data source. */ + return m_dataSource->indexFromCumulatedWidth(m_tableView->contentOffset().x()); +} + +bool TableView::ContentView::rowAtIndexIsBeforeFullyVisibleRange(int index) const { + return index <= rowsScrollingOffset(); +} + +bool TableView::ContentView::columnAtIndexIsBeforeFullyVisibleRange(int index) const { + return index <= columnsScrollingOffset(); +} + +bool TableView::ContentView::rowAtIndexIsAfterFullyVisibleRange(int index) const { + int minHeightToDisplayRowAtIndex = m_dataSource->cumulatedHeightFromIndex(index+1); + int heightToTheBottomOfTheScreen = m_tableView->contentOffset().y()+m_tableView->maxContentHeightDisplayableWithoutScrolling()+m_tableView->topMargin(); + return minHeightToDisplayRowAtIndex >= heightToTheBottomOfTheScreen; +} + +bool TableView::ContentView::columnAtIndexIsAfterFullyVisibleRange(int index) const { + int minWidthToDisplayColumnAtIndex = m_dataSource->cumulatedWidthFromIndex(index+1); + int widthToTheRightOfTheScreen = m_tableView->contentOffset().x()+m_tableView->maxContentWidthDisplayableWithoutScrolling()+m_tableView->leftMargin(); + return minWidthToDisplayColumnAtIndex >= widthToTheRightOfTheScreen; +}