/* Copyright (c) 2011, Geert Bergman (geert@scrivo.nl)
 * All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions are met:
 *
 * 1. Redistributions of source code must retain the above copyright notice,
 *    this list of conditions and the following disclaimer.
 * 2. Redistributions in binary form must reproduce the above copyright notice,
 *    this list of conditions and the following disclaimer in the documentation
 *    and/or other materials provided with the distribution.
 * 3. Neither the name of "Scrivo" nor the names of its contributors may be
 *    used to endorse or promote products derived from this software without
 *    specific prior written permission.
 *
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
 * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
 * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
 * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
 * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
 * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
 * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
 * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
 * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
 * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
 * POSSIBILITY OF SUCH DAMAGE.
 *
 * $Id: ListView.js 616 2013-04-22 23:48:38Z geert $
 */

"use strict";

SUI.ListView = SUI.defineClass(
	/** @lends SUI.ListView.prototype */{

	/** @ignore */ baseClass: SUI.Box,

	/**
	 * @class
	 * SUI.ListView is a component to display data in a list with columns. The
	 * list can be used to select one multiple rows. To aid the user's
	 * selection the rows can be sorted per column.
	 *
	 * @augments SUI.Box
	 *
	 * @description
	 * Construct a SUI.ListView object. A large number of variables can be set
	 * to customize the listview to your specific needs.
	 *
	 * @constructs
	 * @param {Object} object in which the follow entries can be set
	 * @param see base class
	 * @param {boolean} arg.multiselect Enable multiple selection
	 * @param {object[]} arg.data The data array of objects in which each
	 *     object contains column (key) / values pairs
	 * @param {String} arg.sort The column key for which the data should be
	 *     sorted initially
	 * @param {int|int[]} arg.selected Array of indexes for the initial row
	 *     selection
	 * @param {int} arg.focussed Index of the row to focus initially
	 * @param {object[]} arg.cols The column definition: an array with objects
	 *     which can contain the following fields:
	 * @param {String} arg.cols[].title The header title
	 * @param {String} arg.cols[].key Key that corresponds with a key in the
	 *     data array
	 * @param {int} arg.cols[].width Width of the header
	 * @param {int} arg.cols[].minWidth Minimum width of the header
	 * @param {int} arg.cols[].maxWidth Maximum width of the header
	 * @param {String} arg.cols[].align Alignment of the header, options are
	 *     "left" (default), "center" and "right"
	 * @param {string|function} arg.cols[].icon false (default) if no icon is
	 *     needed or a location of an icon file or a function that returns one.
	 *     This function takes to parameters, the first is a reference to the
	 *     row and the second is key of the column to generate an icon
	 *     location for.
	 * @param {string|function} arg.cols[].sort a build in sort method or a
	 *     user defined one. "text" (default) and "number" are the build in
	 *     sort methods. The sort function take three parameters: the first
	 *     is a reference to the data array, the second is the key on which to
	 *     sort and the third is the search direction (1 or -1).
	 * @param {Function} arg.cols[].format_func A user defined format
	 *     function, this function takes to parameters, the first is a
	 *     reference to the row and the second is key of the column that
	 *     needs to be formatted.
	 */
	initializer: function(arg) {

		// by default anchor to all sides
		if (!arg.anchor) {
			arg.anchor = { right:true,left:true,top:true,bottom:true };
		}

		SUI.ListView.initializeBase(this, arg);

		// do the heavy stuff
		this._buildControl(arg);
	},

	/**
	 * Top padding of a cell
	 */
	CELL_PADDING_TOP: 1,

	/**
	 * Left and right padding of a cell
	 */
	CELL_PADDING_SIDES: 5,

	/**
	 * Header border (only below the header).
	 */
	HEADER_BORDER_BOTTOM_WIDTH: 1,

	/**
	 * Header height (including bottom border)
	 */
	HEADER_HEIGHT: 21,

	/**
	 * Padding top of the headers
	 */
	HEADER_PADDING_TOP: 2,

	/**
	 * Side padding for a header if a no direction indicator needs to be drawn
	 * at that side.
	 */
	HEADER_PADDING_SHORT: 2,

	/**
	 * Side padding for a header if a direction indicator needs to be drawn
	 * at that side.
	 */
	HEADER_PADDING_LONG: 16,

	/**
	 * Top margin of the sort direction indicator.
	 */
	HEADER_SORT_ICON_MARGIN_TOP: 2,

	/**
	 * Size (width/height) of the icon for the sort direction indicator.
	 */
	HEADER_SORT_ICON_SIZE: 16,

	/**
	 * The half-width of the spacer
	 */
	HEADER_SPACER_WIDTH: 3,

	/**
	 * Size (width/height) of the row icons.
	 */
	ICON_SIZE: 16,

	/**
	 * The vertical margin for the separator (determines the length of the
	 * separator).
	 */
	SEPARATOR_MARGIN_V: 3,

	/**
	 * Width of the separator on the spacer between the headers.
	 */
	SEPARATOR_WIDTH: 2,

	/**
	 * Row border width (top and bottom)
	 */
	ROW_BORDER_WIDTH: 1,

	/**
	 * Row height (including top and bottom border)
	 */
	ROW_HEIGHT: 20,

	/**
	 * Display the listview control. Set the CSS size and position of the list
	 * and all the headers. Then render the rows.
	 */
	display: function() {

		this.setDim();

		// set the CSS dimensions of the header
		this.headervpt.setDim();
		this.header.setDim();

		// set the CSS dimensions of the column headers
		for (var i=0; i<this.colums.length; i++) {
			this.colums[i].header.setDim();
			this.colums[i].headerTitle.setDim();
			this.colums[i].spacer.setDim();
			this.colums[i].separator.setDim();
		}

		// set the CSS of the list
		this.listvpt.setDim();
		this.list.setDim();

		// render the rows, only deal with the focussed row if the list was
		// not yet drawn
		if (this._firstDisplay && this._focussedRow) {
			if (!this._scrollIntoView(this._focussedRow)) {
				this._renderRows();
			}
		} else {
			this._renderRows();
		}
		this._firstDisplay = false;
	},

	/**
	 * Lay out the listview control. Calculate the size and position of the
	 * list and headers.
	 */
	layOut: function() {

		var hdrInnerH = this.HEADER_HEIGHT - this.HEADER_BORDER_BOTTOM_WIDTH;

		// set the size of the header viewport
		this.headervpt.setRect(0, 0, this.width(), this.HEADER_HEIGHT);
		// and create a very wide header bar (so we can set the scroll offset)
		this.header.setRect(0, 0, 10000, hdrInnerH);

		// current left position for the header
		var l = 0;
		// loop through all the columns
		for (var i=0; i<this.colums.length; i++) {

			// set the off the column to the current left
			this.colums[i].left = l;
			// get the width of an header
			var w = this.colums[i].width - this.HEADER_SPACER_WIDTH*2;

			// set the dimensions for the header
			this.colums[i].header.setRect(
				0, l+this.HEADER_SPACER_WIDTH, w, hdrInnerH);
			// set the dimensions for the spacer between the header
			this.colums[i].spacer.setRect(0, l+this.HEADER_SPACER_WIDTH+w,
				this.HEADER_SPACER_WIDTH*2, hdrInnerH);
			// set the dimensions for the separator line in the spacer
			this.colums[i].separator.setRect(this.SEPARATOR_MARGIN_V,
				this.HEADER_SPACER_WIDTH - this.SEPARATOR_WIDTH/2,
				this.SEPARATOR_WIDTH, hdrInnerH - 2*this.SEPARATOR_MARGIN_V);

			// move current left to the next column
			l += this.colums[i].width;

			var pl = 0, pr = 0;

			if (this.colums[i].align === "right") {

				// set the parameters for a right aligned header
				pl = this.HEADER_PADDING_LONG;
				pr = this.HEADER_PADDING_SHORT;
				this.colums[i].header.addClass("sui-lv-header-sort-left");
				this.colums[i].header.el().style.backgroundPosition = "0px " +
					this.HEADER_SORT_ICON_MARGIN_TOP + "px";

			} else {

				// set the parameters for a left aligned header
				pl = this.HEADER_PADDING_SHORT;
				pr = this.HEADER_PADDING_LONG;
				this.colums[i].header.addClass("sui-lv-header-sort-right");
				this.colums[i].header.el().style.backgroundPosition =
					(this.colums[i].width-this.HEADER_SORT_ICON_SIZE-
					this.HEADER_SPACER_WIDTH*2) + "px " +
					this.HEADER_SORT_ICON_MARGIN_TOP + "px";

			}

			// set the dimensions of the text in the header
			this.colums[i].headerTitle.setRect(this.HEADER_PADDING_TOP, pl,
				this.colums[i].header.width() - pr - pl,
				this.colums[i].header.height()-this.HEADER_PADDING_TOP);

		}

		// set the dimensions of the viewport
		this.listvpt.setRect(this.HEADER_HEIGHT, 0, this.width(),
			this.height() - this.HEADER_HEIGHT);
		// and the dimensions of the list
		this.list.setRect(0, 0, l, this.list.height());

		// we want to (re) draw all the rows, so set a new row drawing phase
		this._rowDraw++;
	},

	/**
	 * (Re)load data into the list view.
	 * @param {object[]} data The data array of objects in which each object
	 *     contains column (key) / values pairs
	 * @param {int/int[]} selected Array of indexes for the initial row
	 *     selection
	 * @param {int} focussed Index of the row to focus initially
	 */
	loadData: function(data, selected, focussed) {

		var w = this.list.width();
		// remove what's in the list (other data or loading image) ...
		this.list.removeBox();
		// and create a new box
		this.list = new SUI.Box({parent: this.listvpt});
		// we want the zebra pattern to continue further than the last column
		this.list.el().style.overflow = "visible";

		// remove the "loading" image
		this.listvpt.el().style.backgroundImage = "none";
		this.listvpt.removeClass("sui-lv-loading");

		// and set our data to the data given
		this.data = data ? data : [];

		// we keep the last width but the height setting may be different
		this.list.setRect(0, 0, w, this.data.length * this.ROW_HEIGHT);

		// create an entry for the data we need (references to the cells and
		// rows f.i.) in each data row.
		for (var r=0; r<this.data.length; r++) {
			this.data[r].rowPtr =
				{ "row": null, "drw" : 0, sel : false, "cells" : []};
		}

		// 'normalize' the selected parameter into tmpsel (undefined to [],
		// and int to [int] and [] to []
		var tmpsel = [];
		if (selected !== undefined) {
			if (selected) {
				if (this._multiselect) {
					tmpsel = selected;
				} else {
					tmpsel = [selected[0]];
				}
			}
		}

		// get the focussed row from the index
		var f = focussed !== undefined ? this.data[focussed] : null;
		this._focussedRow = null;

		// set the selected rows
		this.selectedRows = [];
		// the first selected row is always the focussed one
		if (f) {
			this.data[focussed].rowPtr.sel = true;
			this.selectedRows.push(this.data[focussed]);
		}
		// set selected rows (focussed row might be selected already)
		if (this._multiselect || this.selectedRows.length !== 1) {
			for (r=0; r<tmpsel.length; r++) {
				if (r !== focussed) {
					this.data[tmpsel[r]].rowPtr.sel = true;
					this.selectedRows.push(this.data[tmpsel[r]]);
				}
			}
		}

		// sort the data if required
		if (this._sortCol) {
			this._sort();
		}

		// and set the focussed row after sorting (leave it for the display
		// function)
		this._focussedRow = f;

	},

	/**
	 * onClick event handler: is executed when the user clicks on a row.
	 * @param {Object} row A reference to the selected row in the data table
	 */
	onClick: function(row) {
	},

	/**
	 * onDblClick event handler: is executed when the user double clicks on a
	 * row.
	 * @param {Object} row A reference to the selected row in the data table
	 */
	onDblClick: function(row) {
	},

	/**
	 * onContextMenu event handler: is executed when the user uses the context
	 * menu click on a row.
	 * @param {int} x The x location of the click
	 * @param {int} y The y location of the click
	 */
	onContextMenu: function(x, y) {
	},

	/**
	 * onSelectionChange event handler: is executed when the (set of) row
	 * selection(s) changes.
	 * TODO: this event handler is not called properly from the code (f.i.
	 * id not called when rows are deselected).
	 */
	onSelectionChange: function() {
	},

	/**
	 * Replace the listview contents with an "is loading" image. Recommended
	 * when data is loaded from an external resource.
	 */
	setIsLoadingImage: function() {

		// get the width and height of the current box
		var w = this.list.width();
		var h = this.list.height();

		// remove the data in the list ...
		this.list.removeBox();
		// ... and create a new box ...
		this.list = new SUI.Box({parent: this.listvpt});
		// .. and set the width an height to those of the old box
		this.list.setRect(0, 0, w, h);

		// now set the CSS for the loading image
		this.listvpt.addClass("sui-lv-loading");
		this.listvpt.el().style.backgroundImage =
			"url("+SUI.imgDir+"/"+SUI.resource.lvLoading+")";
	},

	// flag to indicate if the display functions was called for the first time
	_firstDisplay: true,

	// the row that has the focus
	_focussedRow: null,

	// is the list focussed or not
	_isFocussed: false,

	// allow multiple selection
	_multiselect: true,

	// first row of a multiple selection
	_multiSelStartRow: null,

	// indicate a row drawing phase: increment to redraw current rows (to
	// invalidate the currently drawn rows)
	_rowDraw: 0,

	// flag to prevent re-entrancy in the renderRows function
	_rowDrawing: false,

	// width of the system scrollbar (needed for size corrections)
	_scrollBarWidth: 0,

	// local storage for current left scroll offset of the list
	_scrollOffsetLeft: 0,

	// local storage for current top scroll offset of the list
	_scrollOffsetTop: 0,

	// column that is currently sorted
	_sortCol: null,

	// flag to indicate if we're sorting up or down
	_sortUp: true,

	/* Add the focus CSS class name to the focussed row
	 */
	_addFocusRectangle: function(e)  {
		this._isFocussed = true;
		if (this._focussedRow && this._focussedRow.rowPtr.row) {
			this._focussedRow.rowPtr.row.addClass("sui-lv-row-focus");
		}
	},

	/* Set event handlers of a header:
	 * click => _sortColumn;
	 * mousedown => _highlightHeader;
	 * mouseout, mouseup => _restoreHeader;
	 * mousedown (of a spacer) => _resizeColumn
	 */
	_addHeaderEventHandlers: function(column) {

		// 'that' and 'column' are two closure variables
		var that = this;

		// Do _sortColumn on the click event of a header.
		SUI.browser.addEventListener(column.header.el(), "click",
			function(e) {
				if (!that._sortColumn(column)) {
					SUI.browser.noPropagation(e);
				}
			}
		);

		// Do _highlightHeader on the mousedown event of a header.
		SUI.browser.addEventListener(column.header.el(), "mousedown",
			function(e) {
				if (!that._highlightHeader(column.header)) {
					SUI.browser.noPropagation(e);
				}
			}
		);

		// Do _resizeColumn on the onmousedown event of a header spacer.
		SUI.browser.addEventListener(column.spacer.el(), "mousedown",
			function(e) {
				if (!that._resizeColumn(new SUI.Event(this, e), column)) {
					SUI.browser.noPropagation(e);
				}
			}
		);

		// Do _restoreHeader on the mouseout event of a header.
		SUI.browser.addEventListener(column.header.el(), "mouseout",
			function(e) {
				if (!that._restoreHeader(column.header)) {
					SUI.browser.noPropagation(e);
				}
			}
		);

		// Do _restoreHeader on the mouseup event of a header.
		SUI.browser.addEventListener(column.header.el(), "mouseup",
			function(e) {
				if (!that._restoreHeader(column.header)) {
					SUI.browser.noPropagation(e);
				}
			}
		);
	},

	/* Set event handlers the list:
	 * focus => _addFocusRectangle;
	 * blur => _removeFocusRectangle;
	 * keydown/keypress => _handleKeyStroke;
	 * scroll => handleScroll
	 */
	_addListEventHandlers: function() {

		var that = this;

		SUI.browser.addEventListener(this.el(), "focus",
			function(e) {
				if (!that._addFocusRectangle(new SUI.Event(this, e))) {
					SUI.browser.noPropagation(e);
				}
			}
		);
		SUI.browser.addEventListener(this.el(), "blur",
			function(e) {
				if (!that._removeFocusRectangle(new SUI.Event(this, e))) {
					SUI.browser.noPropagation(e);
				}
			}
		);
		SUI.browser.addEventListener(this.el(), "keydown",
			function(e) {
				// problems with Gecko's keydown use so keypress for Gecko
				if (!SUI.browser.isGecko) {
					if (!that._handleKeyStroke(new SUI.Event(this, e))) {
					SUI.browser.noPropagation(e);
					}
				}
			}
		);
		SUI.browser.addEventListener(this.listvpt.el(), "keypress",
			function(e) {
				// problems with gecko's keydown use so keypress
				if (SUI.browser.isGecko) {
					if (!that._handleKeyStroke(new SUI.Event(this, e))) {
						SUI.browser.noPropagation(e);
					}
				}
			}
		);
		SUI.browser.addEventListener(this.listvpt.el(), "scroll",
			function(e) {
				if (!that._handleScroll(new SUI.Event(this, e))) {
					SUI.browser.noPropagation(e);
				}
			}
		);

	},

	/* Set event handlers of a row:
	 * click => _handleRowClick;
	 * contextmenu => _handleRowContextMenu;
	 * dblclick => _handleRowDblClick
	 */
	_addRowEventHandlers: function(row) {

		// 'that' and 'row' are two closure variables
		var that = this;

		// Do _handleRowClick on the click event of a row.
		SUI.browser.addEventListener(row.rowPtr.row.el(), "click",
			function(e) {
				if (!that._handleRowClick(new SUI.Event(this, e), row)) {
					SUI.browser.noPropagation(e);
				}
			}
		);

		// Do _handleRowContextMenu on the contextmenu event of a row.
		SUI.browser.addEventListener(row.rowPtr.row.el(), "contextmenu",
			function(e) {
				if (!that._handleRowContextMenu(new SUI.Event(this, e), row)) {
					SUI.browser.noPropagation(e);
				}
			}
		);

		// Do _handleRowDblClick on the dblclick event of a row.
		SUI.browser.addEventListener(row.rowPtr.row.el(), "dblclick",
			function(e) {
				if (!that._handleRowDblClick(new SUI.Event(this, e), row)) {
					SUI.browser.noPropagation(e);
				}
			}
		);

	},

	/* Create the columns for the listview
	 */
	_buildColumns: function(arg) {

		// get the profile for each column
		for (var i=0; i<arg.cols.length; i++) {
			// start with a default profile
			var def = {
				title: "Kolom "+i,
				key: i,
				width: 100,
				minWidth: 10,
				maxWidth: 1000,
				icon: false,
				align: "left",
				sort: "text",
				format_func: null
			};
			for (var prop in arg.cols[i]) {
				// and overwrite the default profile with the entries set
				// in the arguments
				if (arg.cols[i].hasOwnProperty(prop)) {
					def[prop] = arg.cols[i][prop];
				}
			}
			this.colums.push(def);
		}

		// loop through all the columns
		for (i=0; i<this.colums.length; i++) {

			// create the column header
			this.colums[i].header = new SUI.Box({parent: this.header});

			// and a box for the text in the header
			this.colums[i].headerTitle =
				new SUI.TextBox({parent: this.colums[i].header});
			this.colums[i].headerTitle.text(this.colums[i].title);
			this.colums[i].headerTitle.el().style.overflow = "hidden";
			this.colums[i].headerTitle.el().style.whiteSpace = "nowrap";
			this.colums[i].headerTitle.el().style.textAlign =
				this.colums[i].align;

			// end all headers with a spacer
			this.colums[i].spacer = new SUI.Box({parent: this.header});
			this.colums[i].spacer.addClass("sui-lv-header-spacer");
			this.colums[i].spacer.el().style.cursor = "col-resize";

			// each spacer also get a separator line
			this.colums[i].separator =
				new SUI.Box({parent: this.colums[i].spacer});
			this.colums[i].separator.border(
				new SUI.Border(0, this.SEPARATOR_WIDTH / 2 | 0));
			this.colums[i].separator.addClass("sui-lv-header-separator");

			// set the event handlers for the row
			this._addHeaderEventHandlers(this.colums[i]);

			// and if date is sorted ...
			if (arg.sort) {
				// ... set the sort direction indicator for the sorted column
				if (this.colums[i].key === arg.sort) {
					this._sortCol = this.colums[i];
					this.colums[i].header.el().style.backgroundImage =
						"url("+SUI.imgDir+"/"+this.ICON_SORT_UP+")";
				}
			}
		}

	},

	/* Make all required boxes for the control, set event handlers and load
	 * the data.
	 */
	_buildControl: function(arg) {

		// initialize the selectedRows array
		this.selectedRows = [];
		// get the width of the system scrollbar
		this._scrollBarWidth = SUI.style.scrollbarWidth();
		// initialize the columns array
		this.colums = [];

		// if multiselect was set multiple selection on
		if (arg.multiselect !== undefined) {
			this._multiselect = arg.multiselect;
		}

		// incorporate this the listview's main element in the tab flow
		this.el().tabIndex = 1;
		// but do not allow it to be focusable (one row will be focusable)
		this.addClass("no-focus");

		// create a viewport for the header: a box in which the header can
		// scroll to the left or right.
		this.headervpt = new SUI.Box({parent: this});
		this.headervpt.border(
			new SUI.Border(0, 0, this.HEADER_BORDER_BOTTOM_WIDTH, 0));
		this.headervpt.addClass("sui-lv-header");
		// don't want scroll bars, its scrolling is dependent on the scrolling
		// of the list
		this.headervpt.el().style.overflow = "hidden";

		// create the header box, fill it with headers later
		this.header = new SUI.Box({parent: this.headervpt});

		// create a viewport for the list: a box in which the list can
		// scroll to the left or right, or up or down.
		this.listvpt = new SUI.Box({parent: this});
		this.listvpt.addClass("no-focus");
		this.listvpt.addClass("sui-lv-viewport");
		// allow for scrolling in two dimensions
		this.listvpt.el().style.overflow = "auto";
		// take it out of the tab flow
		this.listvpt.el().tabIndex = -1;

		// create the list and add it to the viewport
		this.list = new SUI.Box({parent: this.listvpt});

		// add the general event handlers
		this._addListEventHandlers();

		// create the columns
		this._buildColumns(arg);

		// and load the data into the listview
		this.loadData(arg.data, arg.selected, arg.focussed);
	},

	/* Create a row cell
	 */
	_createCell: function(rowPtr, dataPtr, col, c) {

		// create the cell
		var cell = new SUI.TextBox({parent: rowPtr.row});

		// set the content for cell, using a format function if appropriate
		cell.text(col.format_func
			? col.format_func(dataPtr, col.key)
			: (dataPtr[col.key] ? dataPtr[col.key] : ""));
		// set the cell's alignment
		cell.el().style.textAlign = col.align;
		// we don't want overflow and warp on these cells
		cell.el().style.overflow = "hidden";
		cell.el().style.whiteSpace = "nowrap";

		var that = this;
		// but we do want to show the content if it is truncated
		// that and cell are the tow closure variables
		SUI.browser.addEventListener(cell.el(), "mouseover",
			function(e) {
				if (!that._setTitleOnOverflow(cell)) {
					SUI.browser.noPropagation(e);
				}
			}
		);

		// if there is an icon in this column ...
		if (col.icon) {

			// ... get the icon, using a function if appropriate
			var icn = col.icon instanceof Function
				? col.icon(dataPtr, col.key) : dataPtr[col.icon];
			// ... and set the icon as background image ...
			cell.el().style.backgroundImage = "url("+SUI.imgDir+"/"+icn+")";
			// ... now set the padding for the cell
			cell.padding(new SUI.Padding(
				this.CELL_PADDING_TOP,    this.CELL_PADDING_SIDES, 0,
			 (this.ICON_SIZE+this.CELL_PADDING_SIDES)));

		} else {
			// ... else set normal padding for the cell
			cell.padding(new SUI.Padding(
			 this.CELL_PADDING_TOP, this.CELL_PADDING_SIDES, 0));

		}

		// store a reference to the cell
		rowPtr.cells[c]=cell;
	},

	/* Deselect a given or all selected rows.
	 */
	_deSelectRows: function(dataPtr) {

		// if a row as argument was given ...
		if (dataPtr !== undefined) {

			// ... then deselect that row, remove the CSS classname ...
			if (dataPtr.rowPtr.row) {
				// ... if the row was rendered ...
				dataPtr.rowPtr.row.removeClass("sui-lv-row-selected");
			}
			// ... set its selection marker to false ...
			dataPtr.rowPtr.sel = false;
			// ... and remove it from the selectedRows array
			var i = this.selectedRows.indexOf(dataPtr);
			if(i!==-1) {
				this.selectedRows.splice(i, 1);
			}

		} else {

			// ... else deselect all selected rows ...
			for (i=0; i<this.selectedRows.length; i++) {
				// ... remove the CSS classname
				if (this.selectedRows[i].rowPtr.row) {
					// ... if the row was rendered ...
					this.selectedRows[i].rowPtr.row.removeClass(
						"sui-lv-row-selected");
				}
				// ... and set its selection marker to false
				this.selectedRows[i].rowPtr.sel = false;
			}

			// now clear the selectedRows array
			this.selectedRows = [];

		}
	},

	/* Handle a click on a row
	 */
	_handleRowClick: function(e, row)  {
		this._selectAndFocusRows(row, e.event.ctrlKey, e.event.shiftKey);
		this.callListener("onClick", row);
	},

	/* Handle a right-click (context menu request) on a row
	 */
	_handleRowContextMenu: function(e, row)  {
		if (this.selectedRows.indexOf(row) === -1) {
			this._selectAndFocusRows(row, e.event.ctrlKey, e.event.shiftKey);
		}
		this.callListener("onContextMenu", SUI.browser.getX(e.event),
			SUI.browser.getY(e.event));
	},

	/* Handle a double-click (context menu request) on a row
	 */
	_handleRowDblClick: function(e, row)  {
		this._selectAndFocusRows(row, e.event.ctrlKey, e.event.shiftKey);
		this.callListener("onDblClick", row);
	},

	/* Handle a scroll event: save the scroll offsets and render the rows
	 * in case of vertical scroll.
	 */
	_handleScroll: function(e) {

		// if scrolling in horizontal direction ...
		if (this._scrollOffsetLeft !== e.target.scrollLeft) {
			// ... store left scroll distance ...
			this._scrollOffsetLeft = e.target.scrollLeft;
			// ... and set the scroll offset for the header
			this.headervpt.el().scrollLeft = this._scrollOffsetLeft;
		}

		// if scrolling in vertical direction ...
		if (this._scrollOffsetTop !== e.target.scrollTop) {
			// ... store left scroll distance ...
			this._scrollOffsetTop = e.target.scrollTop;
			// ... and render the rows
			this._renderRows();
		}
	},

	/* End dragging of a columnheader: remove dragger and resize column.
	 */
	_endDrag: function(dragger, column) {
		// remove the dragger form the document tree
		dragger.removeBox();
		// calculate new column width
		column.width = dragger.left() - column.left + this.HEADER_SPACER_WIDTH;
		// and redraw the list view
		this.draw();
	},

	/* Set the currently focussed row
	 */
	_focusRow: function(dataPtr) {

		// Only if row focus changes
		if (this._focussedRow !== dataPtr) {

			// Remove CSS class name if the row was rendered ...
			if (this._focussedRow && this._focussedRow.rowPtr.row) {
				this._focussedRow.rowPtr.row.removeClass("sui-lv-row-focus");
			}
			// ... set the new focussed row ...
			this._focussedRow = dataPtr;
			// ... and ddd the CSS class name if the row was rendered
			if (this._focussedRow && this._focussedRow.rowPtr.row) {
				this._focussedRow.rowPtr.row.addClass("sui-lv-row-focus");
			}
		}
	},

	/* Find the next row if we're scrolling with page up or page down key
	 */
	_getRowIndexNextPage: function(keyCode) {

		// get the index of the current row
		var i = this.data.indexOf(this._focussedRow);
		// and calculate the page size in rows
		var ps = this.listvpt.height() / this.ROW_HEIGHT | 0;

		// if page down was pressed
		if (keyCode === 34) {
			// increase index with page size
			i += ps;
			// but not further than the length of the data array
			if (i > this.data.length-1) {
				i = this.data.length-1;
			}
		}

		// if page down was pressed
		if (keyCode === 33) {
			// decrease index with page size
			i -= ps;
			// but not further that zero
			if (i < 0) {
				i = 0;
			}
		}

		// return the index for new row to focus/select
		return i;
	},

	/* Find the next row if we're scrolling with up or down key
	 */
	_getRowIndexNextRow: function(keyCode) {

		// get the index of the current row
		var i = this.data.indexOf(this._focussedRow);

		// if key down was pressed an we're not at the last row already ...
		if (keyCode === 40 && i < this.data.length-1) {
			// increase the index
			i++;
		}

		// if key up was pressed an we're not at the first row already ...
		if (keyCode === 38 && i > 0) {
			// decrease the index
			i--;
		}

		// return the index for new row to focus/select
		return i;
	},

	/* Set a header to the pressed state.
	 */
	_highlightHeader: function(header) {
		header.addClass("sui-lv-header-button-pressed");
	},

	/* Process a key stroke
	 */
	_handleKeyStroke: function(e)  {

		// enable key processing by the browser
		var r = true;

		// set _multiSelStartRow if necessary
		this._setMultiSelStartRow(e.event.ctrlKey, e.event.shiftKey);

		// keycode madness: there is a difference between keypress and
		// mousedown event
		var keyCode = e.event.keyCode ? e.event.keyCode : e.event.charCode;

		// dispatch to the correct keystroke processing function
		switch(keyCode) {
		case 40:
		case 38:
			// up or down
			r = this._selectNextRow(keyCode, e.event.ctrlKey,
				e.event.shiftKey);
			break;
		case 32:
			// (ctrl) space
			r = this._toggleSelection(keyCode, e.event.ctrlKey);
			break;
		case 34:
		case 33:
			// page up or down
			r = this._selectNextPageRow(keyCode);
			break;
		case 65:
		case 97:
			// (ctrl) a
			r = this._selectAll(keyCode, e.event.ctrlKey);
			break;
		default:
			break;
		}

		// it the keystroke was processed (note: !r) then prevent default
		// action
		if (!r) {
			if (SUI.browser.isIE) {
				e.event.returnValue = false;
			} else {
				e.event.preventDefault();
			}
		}

		// likewise prevent event propagation if the keystroke was processed
		return r;
	},

	/* Remove the focus CSS class name from the focussed row
	 */
	_removeFocusRectangle: function(e)  {
		this._isFocussed = false;
		if (this._focussedRow && this._focussedRow.rowPtr.row) {
			this._focussedRow.rowPtr.row.removeClass("sui-lv-row-focus");
		}
	},

	/* Render the rows currently in the viewport, create the rows if necessary
	 */
	_renderRows: function() {

		// don't re-enter
		if (this._rowDrawing) {
			return;
		}
		this._rowDrawing = true;

		// get the first and number of rows to render ...
		var t = this.listvpt.el().scrollTop / this.ROW_HEIGHT | 0;
		var n = this.height() / this.ROW_HEIGHT | 0;

		// and loop over these rows
		for (var i=t; i<t+n && i<this.data.length; i++) {

			// the current left
			var l = 0;
			// shortcuts: dataPtr is the current row in the data set ...
			var dataPtr = this.data[i];
			// ... and rowPtr point to the DOM row
			var rowPtr = dataPtr.rowPtr;

			// is the row already drawn in the current drawing phase ...
			if (rowPtr.drw !== this._rowDraw) {
				// ... no the draw it

				// if there is no row yet, create it
				if (rowPtr.drw === 0) {

					// create a box for the row
					rowPtr.row = new SUI.Box({parent: this.list});
					// temporary hide its contents
					rowPtr.row.el().style.display="none";
					// add CSS class and border
					rowPtr.row.addClass("sui-lv-row");
					rowPtr.row.border(
					 new SUI.Border(this.ROW_BORDER_WIDTH, 0));
					// store reference
					dataPtr.rowPtr = rowPtr;

					// an add the event handlers to the row
					this._addRowEventHandlers(dataPtr);
				}

				// loop over the columns
				for (var c=0; c<this.colums.length; c++) {

					// if the row is not drawn yet add the cells
					if (rowPtr.drw === 0) {
						this._createCell(rowPtr, dataPtr, this.colums[c], c);
					}
					// set the size of the cells
					var w = this.colums[c].width;
					rowPtr.cells[c].setRect(0, l, w, this.ROW_HEIGHT
						- 2 * this.ROW_BORDER_WIDTH - this.CELL_PADDING_TOP);

					// increase the current left
					l += w;

					// set the CSS dimensions of the cell
					rowPtr.cells[c].setDim();
				}

				// now we're sure that we have a row and all cells have the
				// proper dimensions draw the row, first set the proper CSS
				// class name
				rowPtr.row.removeClass("sui-lv-row-even");
				rowPtr.row.removeClass("sui-lv-row-odd");
				rowPtr.row.addClass(
					"sui-lv-row sui-lv-row-" + (i%2 ? "even" : "odd"));

				// if the row is not as long as the viewport extend it so the
				// zebra pattern is not truncated (and correct for the width of
				// the scroll bar)
				if (l < this.width()) {
					l = this.width()
						- (this.listvpt.height() < this.list.height()
							? this._scrollBarWidth : 0);
				}

				// set the size of the row and the CSS dimensions
				rowPtr.row.setRect(this.ROW_HEIGHT*i, 0, l, this.ROW_HEIGHT);
				rowPtr.row.setDim();

				// if it is the focussed row add CSS class
				if (this._focussedRow && this._isFocussed
						&& rowPtr === this._focussedRow.rowPtr) {
					rowPtr.row.addClass("sui-lv-row-focus");
				}

				// if it is a selected row add CSS class
				if (rowPtr.sel) {
					rowPtr.row.addClass("sui-lv-row-selected");
				}

				// store the drawing phase into the row
				rowPtr.drw = this._rowDraw;

				// now show the row
				rowPtr.row.el().style.display = "block";
			}

		}

		// unlock this function
		this._rowDrawing = false;
	},

	/* Start sizing a column: create and initialize a dragger.
	 */
	_resizeColumn: function(event, column) {

		var left = column.left - this.HEADER_SPACER_WIDTH;

		// find minimum x for dragging
		var minx = left + column.minWidth;
		if (minx < this.headervpt.el().scrollLeft) {
			minx = this.headervpt.el().scrollLeft;
		}

		// find maximum x for dragging
		var maxx = left + column.maxWidth;
		if (maxx > this.headervpt.width() +
			this.headervpt.el().scrollLeft - this.HEADER_SPACER_WIDTH * 2) {
			maxx = this.headervpt.width() +
				this.headervpt.el().scrollLeft - this.HEADER_SPACER_WIDTH * 2;
		}

		// Create  dragger ...
		var dragger = new SUI.Dragger({parent: this.header});
		// ... width the same dimensions as the column spacer
		dragger.setRect(column.spacer);

		// style the dragger
		dragger.addClass("sui-lv-header-dragger");
		dragger.el().style.cursor = column.spacer.el().style.cursor;

		// set the dragging restrictions
		dragger.direction(dragger.HORIZONTAL);
		dragger.xMin(minx);
		dragger.xMax(maxx);

		// set the CSS dimensions of the dragger
		dragger.setDim();

		// set the onEndDrag event handler
		var that = this;
		dragger.addListener("onEndDrag", function() {
			that._endDrag(dragger, column);
		});

		// and start dragging
		dragger.start(event, this);
	},

	/* Set a header to the normal state.
	 */
	_restoreHeader: function(header)  {
		header.removeClass("sui-lv-header-button-pressed");
	},

	/* Scroll the given row into view.
	 */
	 _scrollIntoView: function(row) {

		// assume no need to scroll
		var res = false;

		if (row) {

			// at what position is the row?
			var rownum = this.data.indexOf(row);

			// if calculated distance from top is larger than the viewport
			// height plus top overflow ...
			if (((rownum + 1) * this.ROW_HEIGHT) >
					(this._scrollOffsetTop + this.listvpt.el().clientHeight)) {

				// ... then calculate the new top overflow en set it
				this._scrollOffsetTop = (rownum + 1) * this.ROW_HEIGHT -
					this.listvpt.el().clientHeight;
				this.listvpt.el().scrollTop = this._scrollOffsetTop;

				// render rows into the viewport
				this._renderRows();

				res = true;
			}

			// if calculated distance from top is smaller than the
			// top overflow ...
			if (rownum * this.ROW_HEIGHT < this._scrollOffsetTop) {

				// ... then set the top overflow to that value
				this._scrollOffsetTop = rownum * this.ROW_HEIGHT;
				this.listvpt.el().scrollTop = this._scrollOffsetTop;

				// render rows into the viewport
				this._renderRows();

				res = true;
			}
		}

		return res;
	},

	/* Process letter A key.
	 */
	_selectAll: function(keyCode, ctrl) {

		// Ctrl-A: select all, So only relevant when multiple selection is on
		// and ctrl is pressed
		if (this._multiselect && ctrl) {

			// select all rows
			for (var i=0; i<this.data.length; i++) {
				this._selectRow(this.data[i]);
			}

			// invalidate rows region, so rows will be redrawn
			this._rowDraw++;
			// and redraw the rows
			this._renderRows();

			// disable key processing by the browser
			  return false;
		}

		// enable key processing by the browser
		return true;
	},

	/* Handle a click on a row
	 */
	_selectAndFocusRows: function(dataPtr, ctrl, shift) {

		// set _multiSelStartRow if necessary
		this._setMultiSelStartRow(ctrl, shift);

		if (this._multiselect && shift && this._multiSelStartRow) {

			// multiple selection is on, shift key is pressed and there is a
			// start row: do a range selection. Find the start and end
			var startno = this.data.indexOf(this._multiSelStartRow);
			var endno = this.data.indexOf(dataPtr);
			// swap if necessary
			if (startno > endno) {
				var t = startno; startno = endno; endno = t;
			}
			// deselect the rows
			this._deSelectRows();
			// and create the new selection
			for (var i=startno; i<=endno; i++) {
				this._selectRow(this.data[i]);
			}


		} else if (this._multiselect && ctrl) {

			// multiple selection is on and the ctrl key was pressed, if
			// current row was selected ...
			if (dataPtr.rowPtr.sel) {
				// ... de-select it ...
				this._deSelectRows(dataPtr);
			} else {
				// ... else select it.
				this._selectRow(dataPtr);
			}

		} else {

			// no fancy keys, or no start of multiple selection set yet:
			// deselect all rows an select the requested.
			this._deSelectRows();
			this._selectRow(dataPtr);

		}

		// focus the row was clicked upon
		this._focusRow(dataPtr);
	},

	/* Process page up or page down key.
	 */
	_selectNextPageRow: function(keyCode) {

		// only relevant if there is a focussed row to start
		if (this._focussedRow) {

			// find the index of the new row, ...
			var i = this._getRowIndexNextPage(keyCode);
			// ... deselect the rows and select the new one, ...
			this._deSelectRows();
			this._selectRow(this.data[i]);
			// ... and focus the row, ...
			this._focusRow(this.data[i]);
			// ... and scroll it into view
			this._scrollIntoView(this._focussedRow);

			// disable key processing by the browser
			  return false;
		}

		// enable key processing by the browser
		return true;
	},

	/* Process up or down key.
	 */
	_selectNextRow: function(keyCode, ctrl, shift) {

		// if there is no focussed row ...
		if (!this._focussedRow) {

			// ... then use that keystroke to focus the first row
			// TODO: is this a real case?
			this._focusRow(this.data[0]);

		} else {

			// ... else if there is no selected row ...
			if (this.selectedRows.length === 0) {

				// ... the use that keystroke to select the row
				this._selectRow(this._focussedRow);

			} else {

				var n = this._getRowIndexNextRow(keyCode);

				if (this._multiselect && shift) {

					// multiple selection is on, shift key is pressed and
					// there is a start row: do a range selection. Find the
					// start and end
					var s = this.data.indexOf(this._multiSelStartRow);
					var e = this.data.indexOf(this.data[n]);
					// swap if necessary
					if (s > e) {
						var t = s; s = e; e = t;
					}
					// deselect the rows
					this._deSelectRows();
					// and create the new selection
					for (var i=s; i<=e; i++) {
						this._selectRow(this.data[i]);
					}

				} else if (!this._multiselect || !ctrl) {

					// multiple selection is off or ctrl and shift were not
					// used, deselect all rows an select the requested one.
					this._deSelectRows();
					this._selectRow(this.data[n]);

				}

				// now foucs the selected row
				this._focusRow(this.data[n]);
			}

			// and scroll the row into view
			this._scrollIntoView(this._focussedRow);
		}

		// disable key processing by the browser
		return false;
	},

	/* Set the selected row
	 */
	_selectRow: function(dataPtr) {

		// don't add this row if it was just added? Seems like a stange bug fix
		if (this.selectedRows[this.selectedRows.length-1] !== dataPtr) {

			// add the row to the selected rows ...
			this.selectedRows.push(dataPtr);
			// ... set the CSS class name ...
			if (dataPtr.rowPtr.row) {
				// ... if the row was rendered ...
				dataPtr.rowPtr.row.addClass("sui-lv-row-selected");
			}
			// ... call the onselection changed event handler ...
			this.callListener("onSelectionChange");
			// ... and set the selection marker to true
			dataPtr.rowPtr.sel = true;
		}

	},

	/* For a range selection with shift we need a to remember the first
	 * selected row in a multiple selection.
	 */
	_setMultiSelStartRow: function(ctrl, shift) {

		// shift or ctrl key was pressed ...
		if (this._multiselect && (ctrl || shift)) {
			// ... and this._multiSelStartRow is not yet set ...
			if (!this._multiSelStartRow) {
				// ... remember the first row in a multiple selection, we use
				// that as the first row for a range selection with the shift
				// key
				this._multiSelStartRow = this._focussedRow;
			}
		} else  {
			// ... no multiple select so clear the row
			   this._multiSelStartRow = null;
		}
	},

	/* Set the cell title if the contents of the cell did overflow
	 */
	_setTitleOnOverflow: function(cell) {
		var of = cell.el().clientWidth - cell.el().scrollWidth;
		cell.el().title = of >= 0 ? "" : cell.text().replace(/<[^>]+>/g,"");
	},

	/* Set the sort direction and header icon and sort the data.
	 */
	_sortColumn: function(column) {

		// if the requested column to sort is different that the current
		// sorted column ...
		if (column !== this._sortCol) {
			// ... clear the sort icon from the current column header ...
			if (this._sortCol) {
				this._sortCol.header.el().style.backgroundImage = "";
			}
			// ... set the sort direction to asc ...
			this._sortUp = true;
			// .. set the current sort column the requested one
			this._sortCol = column;
		} else {
			// ... else just reverse the search direction
			this._sortUp = !this._sortUp;
		}

		// now sort the data based on the requested column
		this._sort();
		// and redraw the listview
		this.draw();
	},

	/* Set the sort icon on the header and sort the data
	 */
	_sort: function() {

		// get the key of the data-column
		var k = this._sortCol.key;
		// and the search direction
		var d = this._sortUp ? 1 : -1;

		// set the header icon for the sort
		this._sortCol.header.el().style.backgroundImage = this._sortUp
			? "url("+SUI.imgDir+"/"+SUI.resource.lvSortUp+")"
			: "url("+SUI.imgDir+"/"+SUI.resource.lvSortDown+")";

		// and hide all row data (if the row was rendered)
		for (var i=0; i<this.data.length; i++) {
			if (this.data[i].rowPtr.row) {
				this.data[i].rowPtr.row.el().style.display="none";
			}
		}

		// now sort the data using the specified method
		if ("text" === this._sortCol.sort) {
			// text sort: sort data with a case insensitive comparison method
			this.data.sort(
				function(a, b) {
					var aa = String(a[k]);
					var bb = String(b[k]);
					return d * (
						aa.toLowerCase() < bb.toLowerCase() ? -1 :
						aa.toLowerCase() > bb.toLowerCase() ? 1 : 0
					);
				}
			);
		} else if ("number" === this._sortCol.sort) {
			// number sort: sort data with a number comparison method
			this.data.sort(
				function(a, b) {
					return d*(a[k] - b[k]);
				}
			);
		} else {
			// user sort: sort data with a user provided comparison method
			this._sortCol.sort(this.data, k, d);
		}

		// scroll the currently focussed row into view
		this._scrollIntoView(this._focussedRow);
	},

	/* Process space key.
	 */
	_toggleSelection: function(keyCode, ctrl) {

		// only relevant when multiple selection is on and there is a focussed
		// row and ctrl is pressed
		if (this._multiselect && this._focussedRow && ctrl) {

			// if the focussed row is selected ...
			if (this._focussedRow.rowPtr.sel) {
				// ... then deselect it ...
				this._deSelectRows(this._focussedRow);
			} else {
				// ... else select it
				this._selectRow(this._focussedRow);
			}

			// disable key processing by the browser
			  return false;
		}

		// enable key processing by the browser
		return true;
	}

});