summaryrefslogtreecommitdiff
path: root/webapps/qooxdoo-0.6.3-sdk/frontend/framework/source/class/qx/ui/table/RemoteTableModel.js
diff options
context:
space:
mode:
Diffstat (limited to 'webapps/qooxdoo-0.6.3-sdk/frontend/framework/source/class/qx/ui/table/RemoteTableModel.js')
-rw-r--r--webapps/qooxdoo-0.6.3-sdk/frontend/framework/source/class/qx/ui/table/RemoteTableModel.js435
1 files changed, 435 insertions, 0 deletions
diff --git a/webapps/qooxdoo-0.6.3-sdk/frontend/framework/source/class/qx/ui/table/RemoteTableModel.js b/webapps/qooxdoo-0.6.3-sdk/frontend/framework/source/class/qx/ui/table/RemoteTableModel.js
new file mode 100644
index 0000000000..ebd1be8f53
--- /dev/null
+++ b/webapps/qooxdoo-0.6.3-sdk/frontend/framework/source/class/qx/ui/table/RemoteTableModel.js
@@ -0,0 +1,435 @@
+/* ************************************************************************
+
+ qooxdoo - the new era of web development
+
+ http://qooxdoo.org
+
+ Copyright:
+ 2006 by STZ-IDA, Germany, http://www.stz-ida.de
+
+ License:
+ LGPL 2.1: http://www.gnu.org/licenses/lgpl.html
+
+ Authors:
+ * Til Schneider (til132)
+
+************************************************************************ */
+
+/* ************************************************************************
+
+#module(ui_table)
+
+************************************************************************ */
+
+/**
+ * A table model that loads its data from a backend.
+ * <p>
+ * Only those rows are loaded that are near the area the user is currently
+ * viewing. If the user scrolls, the rows he will see soon are loaded
+ * asynchroniously in the background. All loaded data is managed in a cache that
+ * automatically removes the last resently used rows when it gets full.
+ * <p>
+ * This class is abstract: The actual loading of row data must be done by
+ * subclasses.
+ */
+qx.OO.defineClass("qx.ui.table.RemoteTableModel", qx.ui.table.AbstractTableModel,
+function() {
+ qx.ui.table.AbstractTableModel.call(this);
+
+ this._sortColumnIndex = -1;
+ this._sortAscending = true;
+ this._rowCount = -1;
+
+ this._lruCounter = 0;
+ this._firstLoadingBlock = -1;
+ this._firstRowToLoad = -1;
+ this._lastRowToLoad = -1;
+ this._ignoreCurrentRequest = false;
+
+ this._rowBlockCache = {};
+ this._rowBlockCount = 0;
+});
+
+
+/** The number of rows that are stored in one cache block. */
+qx.OO.addProperty({ name:"blockSize", type:"number", defaultValue:50, allowNull:false });
+
+/** The maximum number of row blocks kept in the cache. */
+qx.OO.addProperty({ name:"maxCachedBlockCount", type:"number", defaultValue:15, allowNull:false });
+
+/**
+ * Whether to clear the cache when some rows are removed.
+ * If false the rows are removed locally in the cache.
+ */
+qx.OO.addProperty({ name:"clearCacheOnRemove", type:"boolean", defaultValue:false, allowNull:false });
+
+
+// overridden
+qx.Proto.getRowCount = function() {
+ if (this._rowCount == -1) {
+ this._loadRowCount();
+
+ // NOTE: _loadRowCount may set this._rowCount
+ return (this._rowCount == -1) ? 0 : this._rowCount;
+ } else {
+ return this._rowCount;
+ }
+}
+
+
+/**
+ * Loads the row count from the server.
+ * <p>
+ * Implementing classes have to call {@link _onRowDataLoaded()} when the server
+ * response arrived. That method has to be called! Even when there was an error.
+ */
+qx.Proto._loadRowCount = function() {
+ throw new Error("_loadRowCount is abstract");
+};
+
+
+/**
+ * Sets the row count.
+ * <p>
+ * Has to be called by {@link _loadRowCount()}.
+ *
+ * @param rowCount {int} the number of rows in this model or null if loading.
+ */
+qx.Proto._onRowCountLoaded = function(rowCount) {
+ this.debug("row count loaded: " + rowCount);
+ if (rowCount == null) {
+ rowCount = 0;
+ }
+ this._rowCount = rowCount;
+
+ // Inform the listeners
+ var data = { firstRow:0, lastRow:rowCount - 1, firstColumn:0, lastColumn:this.getColumnCount() - 1 };
+ this.dispatchEvent(new qx.event.type.DataEvent(qx.ui.table.TableModel.EVENT_TYPE_DATA_CHANGED, data), true);
+};
+
+
+/**
+ * Reloads the model and clears the local cache.
+ */
+qx.Proto.reloadData = function() {
+ this.clearCache();
+
+ // If there is currently a request on its way, then this request will bring
+ // obsolete data -> Ignore it
+ if (this._firstLoadingBlock != -1) {
+ this._ignoreCurrentRequest = true;
+ }
+
+ // NOTE: This will inform the listeners as soon as the new row count is known
+ this._loadRowCount();
+};
+
+
+/**
+ * Clears the cache.
+ */
+qx.Proto.clearCache = function() {
+ this._rowBlockCache = {};
+ this._rowBlockCount = 0;
+};
+
+
+// overridden
+qx.Proto.prefetchRows = function(firstRowIndex, lastRowIndex) {
+ // this.debug("Prefetch wanted: " + firstRowIndex + ".." + lastRowIndex);
+ if (this._firstLoadingBlock == -1) {
+ var blockSize = this.getBlockSize();
+ var totalBlockCount = Math.ceil(this._rowCount / blockSize);
+
+ // There is currently no request running -> Start a new one
+ // NOTE: We load one more block above and below to have a smooth
+ // scrolling into the next block without blank cells
+ var firstBlock = parseInt(firstRowIndex / blockSize) - 1;
+ if (firstBlock < 0) {
+ firstBlock = 0;
+ }
+ var lastBlock = parseInt(lastRowIndex / blockSize) + 1;
+ if (lastBlock >= totalBlockCount) {
+ lastBlock = totalBlockCount - 1;
+ }
+
+ // Check which blocks we have to load
+ var firstBlockToLoad = -1;
+ var lastBlockToLoad = -1;
+ for (var block = firstBlock; block <= lastBlock; block++) {
+ if (this._rowBlockCache[block] == null || this._rowBlockCache[block].isDirty) {
+ // We don't have this block
+ if (firstBlockToLoad == -1) {
+ firstBlockToLoad = block;
+ }
+ lastBlockToLoad = block;
+ }
+ }
+
+ // Load the blocks
+ if (firstBlockToLoad != -1) {
+ this._firstRowToLoad = -1;
+ this._lastRowToLoad = -1;
+
+ this._firstLoadingBlock = firstBlockToLoad;
+
+ this.debug("Starting server request. rows: " + firstRowIndex + ".." + lastRowIndex + ", blocks: " + firstBlockToLoad + ".." + lastBlockToLoad);
+ this._loadRowData(firstBlockToLoad * blockSize, (lastBlockToLoad + 1) * blockSize - 1);
+ }
+ } else {
+ // There is already a request running -> Remember this request
+ // so it can be executed after the current one is finished.
+ this._firstRowToLoad = firstRowIndex;
+ this._lastRowToLoad = lastRowIndex;
+ }
+};
+
+
+/**
+ * Loads some row data from the server.
+ * <p>
+ * Implementing classes have to call {@link _onRowDataLoaded()} when the server
+ * response arrived. That method has to be called! Even when there was an error.
+ *
+ * @param firstRow {int} The index of the first row to load.
+ * @param lastRow {int} The index of the last row to load.
+ */
+qx.Proto._loadRowData = function(firstRow, lastRow) {
+ throw new Error("_loadRowCount is abstract");
+};
+
+
+/**
+ * Sets row data.
+ * <p>
+ * Has to be called by {@link _loadRowData()}.
+ *
+ * @param rowDataArr {Map[]} the loaded row data or null if there was an error.
+ */
+qx.Proto._onRowDataLoaded = function(rowDataArr) {
+ if (rowDataArr != null && ! this._ignoreCurrentRequest) {
+ var blockSize = this.getBlockSize();
+ var blockCount = Math.ceil(rowDataArr.length / blockSize);
+ if (blockCount == 1) {
+ // We got one block -> Use the rowData directly
+ this._setRowBlockData(this._firstLoadingBlock, rowDataArr);
+ } else {
+ // We got more than one block -> We've to split the rowData
+ for (var i = 0; i < blockCount; i++) {
+ var rowOffset = i * blockSize;
+ var blockRowData = [];
+ var mailCount = Math.min(blockSize, rowDataArr.length - rowOffset);
+ for (var row = 0; row < mailCount; row++) {
+ blockRowData.push(rowDataArr[rowOffset + row]);
+ }
+
+ this._setRowBlockData(this._firstLoadingBlock + i, blockRowData);
+ }
+ }
+ this.debug("Got server answer. blocks: " + this._firstLoadingBlock + ".." + (this._firstLoadingBlock + blockCount - 1) + ". mail count: " + rowDataArr.length + " block count:" + blockCount);
+
+ // Inform the listeners
+ var data = {
+ firstRow:this._firstLoadingBlock * blockSize,
+ lastRow:(this._firstLoadingBlock + blockCount + 1) * blockSize - 1,
+ firstColumn:0,
+ lastColumn:this.getColumnCount() - 1
+ };
+ this.dispatchEvent(new qx.event.type.DataEvent(qx.ui.table.TableModel.EVENT_TYPE_DATA_CHANGED, data), true);
+ }
+
+ // We're not loading any blocks any more
+ this._firstLoadingBlock = -1;
+ this._ignoreCurrentRequest = false;
+
+ // Check whether we have to start a new request
+ if (this._firstRowToLoad != -1) {
+ this.prefetchRows(this._firstRowToLoad, this._lastRowToLoad);
+ }
+};
+
+
+/**
+ * Sets the data of one block.
+ *
+ * @param block {int} the index of the block.
+ * @param rowDataArr {var[][]} the data to set.
+ */
+qx.Proto._setRowBlockData = function(block, rowDataArr) {
+ if (this._rowBlockCache[block] == null) {
+ // This is a new block -> Check whether we have to remove another block first
+ this._rowBlockCount++;
+
+ while (this._rowBlockCount > this.getMaxCachedBlockCount()) {
+ // Find the last recently used block
+ // NOTE: We never remove block 0 and 1
+ var lruBlock;
+ var minLru = this._lruCounter;
+ for (var currBlock in this._rowBlockCache) {
+ var currLru = this._rowBlockCache[currBlock].lru;
+ if (currLru < minLru && currBlock > 1) {
+ minLru = currLru;
+ lruBlock = currBlock;
+ }
+ }
+
+ // Remove that block
+ this.debug("Removing block: " + lruBlock + ". current LRU: " + this._lruCounter);
+ delete this._rowBlockCache[lruBlock];
+ this._rowBlockCount--;
+ }
+ }
+
+ this._rowBlockCache[block] = { lru:++this._lruCounter, rowDataArr:rowDataArr };
+};
+
+
+/**
+ * Removes a rows from the model.
+ *
+ * @param rowIndex {int} the index of the row to remove.
+ */
+qx.Proto.removeRow = function(rowIndex) {
+ if (this.getClearCacheOnRemove()) {
+ this.clearCache();
+
+ // Inform the listeners
+ var data = { firstRow:0, lastRow:rowCount - 1, firstColumn:0, lastColumn:this.getColumnCount() - 1 };
+ this.dispatchEvent(new qx.event.type.DataEvent(qx.ui.table.TableModel.EVENT_TYPE_DATA_CHANGED, data), true);
+ } else {
+ var blockSize = this.getBlockSize();
+ var blockCount = Math.ceil(this.getRowCount() / blockSize);
+ var startBlock = parseInt(rowIndex / blockSize);
+
+ // Remove the row and move the rows of all following blocks
+ for (var block = startBlock; block <= blockCount; block++) {
+ var blockData = this._rowBlockCache[block];
+ if (blockData != null) {
+ // Remove the row in the start block
+ // NOTE: In the other blocks the first row is removed
+ // (This is the row that was)
+ var removeIndex = 0;
+ if (block == startBlock) {
+ removeIndex = rowIndex - block * blockSize;
+ }
+ blockData.rowDataArr.splice(removeIndex, 1);
+
+ if (block == blockCount - 1) {
+ // This is the last block
+ if (blockData.rowDataArr.length == 0) {
+ // It is empty now -> Remove it
+ delete this._rowBlockCache[block];
+ }
+ } else {
+ // Try to copy the first row of the next block to the end of this block
+ // so this block can stays clean
+ var nextBlockData = this._rowBlockCache[block + 1];
+ if (nextBlockData != null) {
+ blockData.rowDataArr.push(nextBlockData.rowDataArr[0]);
+ } else {
+ // There is no row to move -> Mark this block as dirty
+ blockData.isDirty = true;
+ }
+ }
+ }
+ }
+
+ if (this._rowCount != -1) {
+ this._rowCount--;
+ }
+
+ // Inform the listeners
+ if (this.hasEventListeners(qx.ui.table.TableModel.EVENT_TYPE_DATA_CHANGED)) {
+ var data = { firstRow:rowIndex, lastRow:this.getRowCount() - 1, firstColumn:0, lastColumn:this.getColumnCount() - 1 };
+ this.dispatchEvent(new qx.event.type.DataEvent(qx.ui.table.TableModel.EVENT_TYPE_DATA_CHANGED, data), true);
+ }
+ }
+};
+
+
+/**
+ * <p>See overridden method for details.</p>
+ *
+ * @param rowIndex {int} the model index of the row.
+ * @return {Object} Map containing a value for each column.
+ */
+qx.Proto.getRowData = function(rowIndex) {
+ var blockSize = this.getBlockSize();
+ var block = parseInt(rowIndex / blockSize);
+ var blockData = this._rowBlockCache[block];
+ if (blockData == null) {
+ // This block is not (yet) loaded
+ return null;
+ } else {
+ var rowData = blockData.rowDataArr[rowIndex - (block * blockSize)];
+
+ // Update the last recently used counter
+ if (blockData.lru != this._lruCounter) {
+ blockData.lru = ++this._lruCounter;
+ }
+
+ return rowData;
+ }
+};
+
+
+// overridden
+qx.Proto.getValue = function(columnIndex, rowIndex) {
+ var rowData = this.getRowData(rowIndex);
+ if (rowData == null) {
+ return null;
+ } else {
+ var columnId = this.getColumnId(columnIndex);
+ return rowData[columnId];
+ }
+};
+
+
+/**
+ * Sets whether a column is sortable.
+ *
+ * @param columnIndex {int} the column of which to set the sortable state.
+ * @param sortable {boolean} whether the column should be sortable.
+ */
+qx.Proto.setColumnSortable = function(columnIndex, sortable) {
+ if (sortable != this.isColumnSortable(columnIndex)) {
+ if (this._sortableColArr == null) {
+ this._sortableColArr = [];
+ }
+ this._sortableColArr[columnIndex] = sortable;
+
+ this.createDispatchEvent(qx.ui.table.TableModel.EVENT_TYPE_META_DATA_CHANGED);
+ }
+}
+
+
+// overridden
+qx.Proto.isColumnSortable = function(columnIndex) {
+ return this._sortableColArr ? (this._sortableColArr[columnIndex] == true) : false;
+}
+
+
+// overridden
+qx.Proto.sortByColumn = function(columnIndex, ascending) {
+ if (this._sortColumnIndex != columnIndex || this._sortAscending != ascending) {
+ this._sortColumnIndex = columnIndex;
+ this._sortAscending = ascending;
+
+ this.clearCache();
+
+ // Inform the listeners
+ this.createDispatchEvent(qx.ui.table.TableModel.EVENT_TYPE_META_DATA_CHANGED);
+ }
+};
+
+
+// overridden
+qx.Proto.getSortColumnIndex = function() {
+ return this._sortColumnIndex;
+}
+
+
+// overridden
+qx.Proto.isSortAscending = function() {
+ return this._sortAscending;
+}