1 /*
  2  *  Copyright (C) 2008-2009 WaveMaker Software, Inc.
  3  *
  4  *  This file is part of the WaveMaker Client Runtime.
  5  *
  6  *  Licensed under the Apache License, Version 2.0 (the "License");
  7  *  you may not use this file except in compliance with the License.
  8  *  You may obtain a copy of the License at
  9  *
 10  *      http://www.apache.org/licenses/LICENSE-2.0
 11  *
 12  *  Unless required by applicable law or agreed to in writing, software
 13  *  distributed under the License is distributed on an "AS IS" BASIS,
 14  *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 15  *  See the License for the specific language governing permissions and
 16  *  limitations under the License.
 17  */
 18 dojo.provide("wm.base.widget.DataGrid");
 19 dojo.require("wm.base.widget.dijit.Grid");
 20 dojo.require("wm.base.widget.Formatters");
 21 dojo.require('wm.base.lib.data');
 22 
 23 /**
 24 	@name dojox.grid.cell
 25 	@class
 26 	An object that describes a cell in the Grid.
 27 	@todo document cell object
 28 	@noindex
 29 */
 30 
 31 /**
 32 	@name wm.DataGrid.Event
 33 	@class
 34 	A browser Event object, with some additional properties.
 35 	@property {wm.dijit.Grid} grid The Dijit Grid that owns the click
 36 	@property {dojox.GridView} sourceView The View (scrolling region) that owns the click
 37 	@property {Number} cellIndex The index of the cell that owns the click (or -1)
 38 	@property {Number} rowIndex The index of the row that owns the click (or -1)
 39 	@noindex
 40 */
 41 
 42 /**
 43 	@name wm.MavericksModel
 44 	@class
 45 */
 46 dojo.declare("wm.MavericksModel", dojox.grid.data.Model, {
 47 	maxObjectDepth: 5,
 48 	allChange: function(){
 49 		this.notify("AllChange", arguments);
 50 		this.notify("Change", arguments);
 51 	},
 52 	setData: function(inData) {
 53 		this.variable = inData;
 54 		this.schemaToFields();
 55 	},
 56 	_schemaToFields: function(inSchema, inName, inRelated, inDepth) {
 57 		if (!inSchema || inDepth > this.maxObjectDepth)
 58 			return;
 59 		var p = inName ? inName + '.' : '', inDepth = inDepth || 0;
 60 		inDepth++;
 61 		for (var i in inSchema) {
 62 			var ti = inSchema[i], n = p + i;
 63 			var field = { name: n.replace(/\./g, "_"), key: n, field: n, compare: wm.data.compare };
 64 			if (ti.isList) {
 65 				this.fields.set(this.fields.values.length, field);
 66 			} else if (wm.typeManager.isStructuredType(ti.type)) {
 67 				if (!inRelated || inRelated && dojo.indexOf(inRelated, n) != -1) {
 68 					// only need depth protection if we do not have related objects
 69 					var d = inRelated ? 0 : inDepth;
 70 					this._schemaToFields(wm.typeManager.getTypeSchema(ti.type), n, inRelated, d);
 71 				}
 72 			} else {
 73 				this.fields.set(this.fields.values.length, field);
 74 			}
 75 		}
 76 	},
 77 	schemaToFields: function() {
 78 		this.fields.clear();
 79 		if (this.variable)
 80 			this._schemaToFields(this.variable._dataSchema, '', this.variable.related);
 81 	},
 82 	getRowCount: function(){
 83 		return this.variable && this.variable.isList && this.variable.getCount();
 84 	},
 85 	measure: function(){
 86 		this.count = this.getRowCount();
 87 		this.allChange();
 88 	},
 89 	getRow: function(inRowIndex){
 90 		//console.debug(this, "getRow called");
 91 		return this.variable.getItem(inRowIndex);
 92 	},
 93 	getDatum: function(inRowIndex, inField){
 94 		var
 95 			i = this.getRow(inRowIndex),
 96 			f = i && (inField >= 0) && this.fields.values[inField];
 97 		return f && (i.data[f.key] || i.getValue(f.key));
 98 	},
 99 	setDatum: function(inDatum, inRowIndex, inField){
100 		if (inDatum !== undefined) {
101 			var i = this.variable.getItem(inRowIndex);
102 			if (i) {
103 				i.beginUpdate();
104 				i.setValue(this.fields.values[inField].key, inDatum);
105 				i.endUpdate();
106 				this.notify("DatumChange", arguments);
107 			}
108 		}
109 	},
110 	beginModifyRow: function(inRowIndex){
111 	},
112 	endModifyRow: function(inRowIndex) {
113 	},
114 	cancelModifyRow: function(inRowIndex) {
115 		this.allChange();
116 	},
117 	// sort
118 	sort: function(/* (+|-)column_index+1, ... */){
119 		this.variable.sort(this.makeComparator(arguments));
120 	},
121 	// FIXME: data will be sorted on field key values;
122 	// any data formatting performed by a grid column expression will be ignored
123 	generateComparator: function(inCompare, inField, inTrueForAscend, inSubCompare){
124 		// we assume that a and b are wm.Variables
125 		return function(a, b){
126 			a = a.getValue(inField);
127 			b = b.getValue(inField);
128 			var ineq = inCompare(a, b);
129 			return ineq ? (inTrueForAscend ? ineq : -ineq) : inSubCompare && inSubCompare(a, b);
130 		}
131 	}
132 });
133 
134 /**
135 	Encapsulates a column in a DataGrid.
136 	@name wm.DataGridColumn
137 	@class
138 	@extends wm.Component
139 */
140 dojo.declare("wm.DataGridColumn", wm.Component, {
141 	/** @lends wm.DataGridColumn.prototype */
142 	/**
143 		This column will be as wide as needed to fill available space.
144 		If there is no space avaiable, the column will be invisible.
145 		If multiple columns are <i>autoSize</i>, the available space 
146 		is divided evenly.
147 	*/
148 	autoSize: false,
149 	/**
150 		This column will be as wide as needed to fill available space.
151 		If there is no space avaiable, the column will be invisible.
152 		If multiple columns are <i>autoSize</i>, the available space 
153 		is divided evenly.
154 	*/
155 	dataExpression: "",
156 	field: "",
157 	caption: "",
158 	columnWidth: "120px",
159 	display: "",
160 	format: "(details)",
161 	index: 0,
162 	showing: true,
163 	addColumn: "(click to add column)",
164 	removeColumn: "(click to remove)",
165 	init: function() {
166 		// BC
167 		delete this.format;
168 		this.caption = this.label || this.caption;
169 		delete this.label;
170 		//
171 		this.inherited(arguments);
172 		this.setDisplay(this.display);
173 	},
174 	// FIXME: in progress attempt to allow deleting of columns natively
175 	/*destroy: function() {
176 		if (this._destroying)
177 			return;
178 		var o = this.owner;
179 		this.inherited(arguments);
180 		if (o) {
181 			this._destroying = true;
182 			o.doRemoveColumn(this);
183 		}
184 	},*/
185 	setField: function(inField) {
186 		this.field = inField;
187 		this.caption = this.caption || this.field;
188 	},
189 	setIndex: function(inIndex) {
190 		this.owner.setColumnIndex(this, inIndex);
191 	},
192 	setColumnWidth: function(inColumnWidth) {
193 		this.columnWidth = inColumnWidth;
194 		this.autoSize = false;
195 	},
196 	formatChanged: function() {
197 		this.owner.columnsChanged();
198 	},
199 	valueChanged: function(inProp, inValue) {
200 		this.inherited(arguments);
201 		this.owner.columnsChanged();
202 	},
203 	getCellProps: function() {
204 		var cell = { 
205 			name: this.caption, 
206 			field: this.field, 
207 			dataExpression: this.dataExpression,
208 			hide: !this.showing
209 		};
210 		if (this.autoSize)
211 			cell.width = "auto";
212 		else if (this.columnWidth)
213 			cell.width = this.columnWidth;
214 		if (this.components.format)
215 			dojo.mixin(cell, this.components.format.getColProps());
216 		if (this.editor) {
217 			cell.editor = this.editor;
218 		}
219 		if (this.selectOptions) {
220 			cell.options = this.selectOptions;
221 		}
222 		if (this.dataExpression)
223 			cell.get = this.getExpressionDatum;
224 		return cell;
225 	},
226 	// cell getter for a column that contains an expression.
227 	// called in context of grid cell
228 	getExpressionDatum: function(inRowIndex) {
229 		return wm.expression.getValue(this.dataExpression, this.grid.model.getRow(inRowIndex));
230 	},
231 	setDisplay: function(inDisplay) {
232 		var c = this.display = inDisplay;
233 		if (c.slice(0, 5) != "wm")
234 			c = "wm." + c + "Formatter";
235 		var ctor = dojo.getObject(c);
236 		if (!ctor) {
237 			this.display = ""
238 			ctor = wm.DataFormatter;
239 		}
240 		wm.fire(this.components.format, "destroy");
241 		new ctor({name: "format", owner: this});
242 	},
243 	/**
244 		Fires when the user clicks the mouse in this cell.
245 		<br/><br/>
246 		@param {Number} inRowIndex The clicked row.
247 		@param {dojox.grid.cell} inCell Various fields describing the clicked cell.
248 		@param {wm.DataGrid.Event} inEvent The browser event object, decorated.
249 	*/
250 	onClick: function(inRowIndex, inCell, inEvent) {
251 	}
252 });
253 
254 wm.DataGridColumn.extend({
255 	listProperties: function() {
256 		var p = this.inherited(arguments);
257 		p.columnWidth.ignore = this.autoSize;
258 		return p;
259 	},
260 	set_index: function(inIndex) {
261 		var reselect = (studio.selected == this);
262 		this.setIndex(inIndex);
263 		if (reselect) {
264 			wm.onidle(this, function() {
265 				studio.select(null);
266 				studio.select(this);
267 			});
268 		}
269 	},
270 	isParentLocked: function() {
271 		return this.owner && this.owner.isParentLocked();
272 	},
273 	isParentFrozen: function() {
274 		return this.owner && this.owner.isParentFrozen();
275 	},
276 	makePropEdit: function(inName, inValue, inDefault) {
277 		switch (inName) {
278 			case "addColumn":
279 			case "removeColumn":
280 				return makeReadonlyButtonEdit(inName, inValue, inDefault);
281 			case "field":
282 				return makeSelectPropEdit(inName, inValue, this.owner._listFields(), inDefault);
283 			case "display":
284 				return makeSelectPropEdit(inName, inValue, [""].concat(wm.formatters), inDefault);
285 			case "columnWidth":
286 				return new wm.propEdit.UnitValue({component: this, name: inName, value: inValue, options: this.owner._sizeUnits});
287 		}
288 		return this.inherited(arguments);
289 	},
290 	editProp: function(inName, inValue, inInspector) {
291 		switch (inName) {
292 			case "removeColumn":
293 				return this.owner.doRemoveColumn(this);
294 			case "addColumn":
295 				return this.owner.doAddColumn();
296 		}
297 		return this.inherited(arguments);
298 	}
299 });
300 
301 /**
302 	Widget for displaying and editing tabulated data.
303 	@name wm.DataGrid
304 	@class
305 	@extends wm.dijit.Grid
306 */
307 dojo.declare("wm.DataGrid", wm.dijit.Grid, {
308 	/** @lends wm.DataGrid.prototype */
309 	addColumn: "(click to add column)",
310 	autoColumns: "(auto add columns)",
311 	clearColumns: "(clear all columns)",
312 	updateNow: "(update now)",
313 	collection: "Columns",
314 	init: function() {
315 		this.inherited(arguments);
316 		this.dijit.canEdit = dojo.hitch(this, "canEdit");
317 		this._columns = [];
318 		this.selectedItem = new wm.Variable({name: "selectedItem", owner: this});
319 		if (this.isDesignLoaded()) {
320 			this.connect(this.dijit, "onHeaderCellClick", this, "headerCellDesignClick");
321 			this.connect(this.dijit, "setCellWidth", this, "setDesignCellWidth");
322 		}
323 		this.connect(this.dijit, "sort", this, "sort");
324 	},
325 	// BC
326 	doSetSizeBc: function() {
327 		// Previously the default units for DataGrid were "flex" so "sizeUnits" was not streamed.
328 		// Fixup here is to default DataGrids with explicit "size" to "flex" units.
329 		if (this.size && !this.sizeUnits)
330 			this.sizeUnits = "flex";
331 		this.inherited(arguments);
332 	},
333 	headerCellDesignClick: function(e){
334 		var c = this._columns[e.cell.index];
335 		if (c) 
336 			studio.select(c);
337 		//this.dijit.constructor.prototype.onHeaderCellClick.call(this.dijit, e);
338 		dojo.stopEvent(e);
339 	},
340 	setDesignCellWidth: function(inIndex, inUnitWidth) {
341 		this._columns[inIndex].columnWidth = inUnitWidth;
342 	},
343 	postInit: function() {
344 		this.inherited(arguments);
345 		this._clearColumns();
346 		for (var i in this.components) {
347 			var c = this.components[i];
348 			if (c instanceof wm.DataGridColumn)
349 				this._columns.push(c);
350 		}
351 		this.renderGrid();
352 	},
353 	// columns and structure
354 	getCollection: function(inName) {
355 		var cn = [];
356 		for (var i in this.components) {
357 			var c = this.components[i];
358 			if (c instanceof wm.DataGridColumn)
359 				cn.push(c);
360 		}
361 		cn.sort(function(a, b) {
362 			return a.index - b.index;
363 		});
364 		return cn;
365 	},
366 	columnsToStructure: function() {
367 		// a subrow is an array of cells
368 		// a view is a structure, with an array of subrows named 'rows'
369 		// a grid layout is an array of views
370 		var subrow = [], rows = [], view = {rows: rows}, s = [view];
371 		// currently we have exactly one subrow
372 		this._columns.sort(this._columnsSorter);
373 		for (var i=0, c; (c=this._columns[i]); i++) {
374 			var cell = c.getCellProps();
375 			this.onSetColumns(cell, i);
376 			subrow.push(cell);
377 		}
378 		this.adjustRowCellProps(subrow);
379 		rows.push(subrow);
380 	// =========================================
381 	// r&d: let's make another subrow for detail
382 	/*	cell = { 
383 			name: ' ', get: dojo.hitch(this, "getDetail"), colSpan: 4, styles: 'padding: 0; margin: 0;'
384 		};
385 		rows.push([cell]);*/
386 		return s;
387 	},
388 	/*getDetail: function(inRowIndex) {
389 		var item = this.dataSet.getItem(inRowIndex);
390 		return new Date(item.getValue("lastUpdate"));
391 		//
392 		var fas = item.getValue("filmActors");
393 		var n = [];
394 		for (var i=0, l=fas.getCount(), f; i<l; i++) {
395 			f = fas.getItem(i).getValue("film")
396 			n.push(f.getValue("title"));
397 		}
398 		return n.join('<br>');
399 	},*/
400 	// =========================================
401 		// ensure cell meets requirements
402 	adjustRowCellProps: function(inRow) {
403 		var flex = 0;
404 		// support translating width in flex to %
405 		// get total flex
406 		dojo.forEach(inRow, function(c) {
407 			var u = wm.splitUnits(c.width);
408 			if (u.units == "flex")
409 				flex += u.value;
410 		});
411 		// convert flex value to %
412 		dojo.forEach(inRow, function(c) {
413 			var u = wm.splitUnits(c.width);
414 			if (flex && u.units == "flex" && u.value)
415 				c.width = Math.round(u.value * 100/ flex) + "%";
416 		});
417 	},
418 	setStructure: function(inStructure) {
419 		this.onSetStructure(inStructure);
420 		this.dijit.setStructure(inStructure && inStructure.length ? inStructure : null);
421 	},
422 	columnsChanged: function() {
423 		if (!this._loading && !this._updating) {
424 			this.setStructure(this.columnsToStructure());
425 			if (this.isDesignLoaded())
426 				studio.refreshComponentOnTree(this);
427 		}
428 	},
429 	setColumnIndex: function(inColumn, inIndex) {
430 		inColumn.index = inIndex - 0.5;
431 		this._columns.sort(this._columnsSorter);
432 		for (var i=0, c; (c=this._columns[i]); i++)
433 			c.index = i;
434 		this.columnsChanged();
435 	},
436 	_columnsSorter: function(inA, inB) {
437 		return inA.index - inB.index;
438 	},
439 	_addFields: function(inList, inSchema, inName, inRelated, inDepth) {
440 		if (!inSchema || inDepth > this.dijit.model.maxObjectDepth)
441 			return;
442 		var p = inName ? inName + '.' : '', inDepth = inDepth || 0;
443 		inDepth++;
444 		for (var i in inSchema) {
445 			var ti = inSchema[i], n = p + i;
446 			// list column not exposed
447 			if (ti.isList) {
448 			} else if (wm.typeManager.isStructuredType(ti.type)) {
449 				// only show field info for available structured objects
450 				if (!inRelated || inRelated && dojo.indexOf(inRelated, n) != -1) {
451 					var d = inRelated ? 0 : inDepth;
452 					this._addFields(inList, wm.typeManager.getTypeSchema(ti.type), n, inRelated, d);
453 				}
454 			} else {
455 				inList.push(n);
456 			}
457 		}
458 	},
459 	_listFields: function(inList, inSchema, inName) {
460 		var list = [ "" ];
461 		if (this.dataSet)
462 			this._addFields(list, this.dataSet._dataSchema, '', (this.dataSet ||0).related);
463 		return list;
464 	},
465 	_clearColumns: function() {
466 		for (var i=0, c; (c=this._columns[i]); i++)
467 			c.destroy();
468 		this._columns = [];
469 	},
470 	_typifyColumn: function(ioColumn, inType) {
471 		var t = wm.typeManager.getPrimitiveType(inType) || inType;
472 		ioColumn.display = dojo.indexOf(wm.formatters, t) != -1 ? t : "";
473 	},
474 	_hasColumnForField: function(inField) {
475 		for (var i=0, columns=this._columns, c; (c=columns[i]); i++)
476 			if (c.field == inField)
477 				return true;
478 	},
479 	_viewToColumns: function(inView, inName) {
480 		var p = inName ? inName + '.' : '', col;
481 		for (var i=0, f, field; f=inView[i]; i++) {
482 			// ignore view field if it's not to be included in lists
483 			field = f.dataIndex;
484 			if (!f.includeLists || this._hasColumnForField(field))
485 				continue;
486 			this._index++;
487 			col = {
488 				name: f.dataIndex.replace(/\.(\S)/g, function(w) {return w.slice(1).toUpperCase();} ),
489 				label: f.caption,
490 				field: field,
491 				owner: this,
492 				index: f.order === undefined ? this._index : f.order,
493 				autoSize: f.autoSize
494 			};
495 			if (!col.autoSize && f.width && f.widthUnits)
496 				col.columnWidth = f.width + f.widthUnits;
497 			this._typifyColumn(col, f.displayType);
498 			this._adjustColumnProps(col);
499 			this._addColumn(col);
500 		}
501 	},
502 	_schemaToColumns: function(inSchema, inName) {
503 		if (!inSchema)
504 			return;
505 		var p = inName ? inName + '.' : '';
506 		for (var i in inSchema) {
507 			var ti = inSchema[i], n = p + i;
508 			if (this._hasColumnForField(n))
509 				continue;
510 			if (ti.isList) {
511 			} else if (wm.typeManager.isStructuredType(ti.type)) {
512 			} else {
513 				this._index++;
514 				var name = n.replace(/\./g, "_");
515 				var col = { name: name, label: n, field: n, owner: this, index: this._index };
516 				this._typifyColumn(col, ti.type);
517 				this._adjustColumnProps(col);
518 				this._addColumn(col);
519 			}
520 		}
521 	},
522 	_adjustColumnProps: function(inColProps) {
523 		// make sure we have a valid name
524 		var name = inColProps.name || "column";
525 		// ensure name ends with a number (to avoid reserved word, e.g. state, id)
526 		name = name.match(/[0-9]$/) ? name : name + 1;
527 		name = this.getUniqueName(name);
528 		inColProps.name = name;
529 	},
530 	_addColumn: function(inColProps) {
531 		this._columns.push(new wm.DataGridColumn(inColProps));
532 	},
533 	_getStartColIndex: function() {
534 		var m = 0;
535 		dojo.forEach(this._columns, function(c) {
536 			m = Math.max(m, c.index);
537 		});
538 		return m == 0 ? m : m++;
539 	},
540 	dataSetToColumns: function() {
541 		this._updating = true;
542 		if (this.dataSet) {
543 			this._index = this._getStartColIndex();
544 			if (this.dataSet.liveView && this.dataSet.liveView.service)
545 				this._viewToColumns(this.dataSet.getViewListFields(), '')
546 			else
547 				this._schemaToColumns(this.dataSet._dataSchema, '');
548 		}
549 		this._updating = false;
550 		if (this.isDesignLoaded())
551 			studio.refreshComponentOnTree(this);
552 	},
553 	createDefaultColumns: function() {
554 		var col = {name: "column1", autoSize: true, owner: this};
555 		this._adjustColumnProps(col);
556 		this._addColumn(col);
557 	},
558 	// 
559 	_hasDefaultColumns: function() {
560 		var c = this._columns[0];
561 		return (!c || (!c.field && !c.dataExpression) && this._columns.length == 1);
562 	},
563 	// virtual binding target
564 	setDataSet: function(inDataSet) {
565 		var d = this.dataSet = inDataSet;
566 		/*if (d && !d.isList)
567 			d = this.dataSet = null;*/
568 		// disable lazy loading for dataSets assigned to grid.
569 		// note: do allow this so that sub-grids can lazy load.
570 		/*
571 		if (d)
572 			d._allowLazyLoad = false;
573 		*/
574 		// always have a model so that grid does not cause rendering error if model is removed (should be fixed in grid).
575 		this.dijit.setModel(new wm.MavericksModel(null, d));
576 		if (d && this._hasDefaultColumns()) {
577 			this._clearColumns();
578 			this.dataSetToColumns();
579 		}
580 		this.renderGrid();
581 	},
582 	preRender: function() {
583 		this.dataSetToSelectedItem();
584 		if (this._columns.length == 0)
585 			this.createDefaultColumns();
586 		// save previous sort / selection info
587 		this._lastSort = this.dijit.sortInfo;
588 		this._lastSelectedIndex = this.getSelectedIndex();
589 		// clear sort indicator
590 		this.dijit.sortInfo = 0;
591 		// handle selection
592 		this.clearSelection();
593 	},
594 	renderGrid: function() {
595 		// make sure to set selectedItem type so that it's not out of sync.
596 		if (!this._loading) {
597 			this.preRender();
598 			this.onBeforeRender();
599 			this.setStructure(this.columnsToStructure());
600 			this.onAfterRender();
601 		}
602 		
603 	},
604 	// selection
605 	/**
606 		Select a row by index.
607 		<br/>
608 		Previous selection is cleared.
609 		@param {Number} inIndex Integer index of row to select. 
610 	*/
611 	select: function(inIndex) {
612 		this.dijit.selection.select(inIndex);
613 	},
614 	/**
615 		Clear the selection such that no rows are selected.
616 	*/
617 	clearSelection: function() {
618 		this.dijit.selection.clear();
619 		this.updateSelected();
620 	},
621 	/**
622 		Returns true if there any selected rows.
623 		@returns @Boolean True if any rows are selected.
624 	*/
625 	hasSelection: function() {
626 		return Boolean(this.dijit.selection.getFirstSelected() != -1);
627 	},
628 	/**
629 		Returns the index of the selected row.
630 		@returns @Number Integer index of selected row.
631 	*/
632 	getSelectedIndex: function() {
633 		return this.dijit.selection.selectedIndex;
634 	},
635 	/**
636 		Returns true if there are no selected rows.
637 		<br/><br/>
638 		This method exists to support the bindable emptySelection 
639 		virtual property (i.e. it implements <code>getValue("emptySelection")</code>.).
640 		<br />
641 		@returns @Boolean True if no rows are selected.
642 	*/
643 	getEmptySelection: function() {
644 		return !this.hasSelection();
645 	},
646 	dataSetToSelectedItem: function() {
647 		this.selectedItem.setLiveView((this.dataSet|| 0).liveView);
648 		this.selectedItem.setType(this.dataSet ? this.dataSet.type : "any");
649 	},
650 	// FIXME: not clear why we have this.selected and this.selectedItem which is a copy
651 	updateSelected: function() {
652 		var s = this.dijit.selection.selectedIndex;
653 		this.selected = this.dataSet && s >= 0 ? this.dataSet.getItem(s) : null;
654 		if (this.selected)
655 			this.selectedItem.setData(this.selected);
656 		else
657 			this.selectedItem.clearData();
658 		// update "emptySelection" property (to trigger bindings)
659 		this.setValue("emptySelection", !this.hasSelection());
660 	},
661 	// events
662 	/**
663 		Fires when the user clicks the mouse in a cell.
664 		<br/><br/>
665 		<i>inEvent</i> object is decorated with information
666 		about the clicked cell.
667 		<br/><br/>
668 		@param {wm.DataGrid.Event} inEvent The browser event object, decorated.
669 	*/
670 	onCellClick: function(inEvent) {
671 		if (inEvent.cell) {
672 			var i = inEvent.cell.index;
673 			var c = this._columns[i];
674 			wm.fire(c, "onClick", [inEvent.rowIndex, inEvent.cell, inEvent]);
675 		}
676 	},
677 	/**
678 		Fires when the user clicks the mouse in the grid header.
679 		<br/><br/>
680 		<i>inEvent</i> object is decorated with information
681 		about the clicked header.
682 		<br/><br/>
683 		@param {wm.DataGrid.Event} inEvent The browser event object, decorated.
684 	*/
685 	onHeaderCellClick: function(inEvent) {
686 	},
687 	_onSelected: function(inIndex) {
688 		// buffer selection
689 		wm.job(this.getRuntimeId(), 250, dojo.hitch(this, "onSelected", inIndex));
690 	},
691 	_onSelectionChanged: function() {
692 		wm.job(this.getRuntimeId() + "-selectionChanged", 250, dojo.hitch(this, "onSelectionChanged"));
693 	},
694 	/**
695 		Event that occurs when the selection is changed.
696 		<br/><br/>
697 		Selections are buffered, so only the last in a series of rapid selections will
698 		trigger this event.
699 		<br />
700 	*/
701 	onSelectionChanged: function() {
702 	},
703 	/**
704 		Event that occurs when a selection is made.
705 		<br/><br/>
706 		Selections are buffered, so only the last in a series of rapid selections will
707 		trigger this event.
708 		<br />
709 		@param {Number} inIndex The index of the selected row.
710 	*/
711 	onSelected: function(inIndex) {
712 		this.updateSelected();
713 	},
714 	_onDeselected: function(inIndex) {
715 		// buffer deselection
716 		wm.job(this.getRuntimeId(), 250, dojo.hitch(this, "onDeselected", inIndex));
717 	},
718 	/**
719 		Event that occurs when a selection is removed.
720 		<br/><br/>
721 		Deselections are buffered, so only the last in a series of rapid deselections will
722 		trigger this event.
723 		<br />
724 		@param {Number} inIndex The index of the de-selected row.
725 	*/
726 	onDeselected: function(inIndex) {
727 		this.updateSelected();
728 	},
729 	/**
730 		Allows user code to customize column objects.
731 		<br/><br/>
732 		This event is fired for each column in the Grid. Custom code
733 		can modify the column object to alter behavior or appearance 
734 		of the Grid.
735 		<br/><br/>
736 		@param {wm.DataGridColumn} inColumn Object whose properties describe the grid column.
737 		@param {Number} inIndex Numeric index of the column 
738 	*/
739 	onSetColumns: function(inColumn, inIndex) {
740 	},
741 	/**
742 		Allows user code to customize grid structure.
743 		<br/><br/>
744 		This event is fired whenever the Grid is initialized. Custom code
745 		can modify the structure object to alter behavior or appearance 
746 		of the Grid.
747 		<br/><br/>
748 		ToDo: document inStructure properties.
749 		<br/><br/>
750 		@param {Object} inStructure Object whose properties describe the grid structure.
751 	*/
752 	onSetStructure: function(inStructure) {
753 	},
754 	/**
755 		Event that fires before the Grid is populated.
756 		<br/><br/>
757 		This event is fired before the Grid is populated, allowing custom code
758 		to cache information or do tasks.
759 	*/
760 	onBeforeRender: function() {
761 	},
762 	/**
763 		Event that fires after the Grid is populated.
764 		<br/><br/>
765 		This event is fired after the Grid is populated, allowing custom code
766 		to cache information or do tasks.
767 	*/
768 	onAfterRender: function() {
769 	},
770 	// server update
771 	/**
772 		Force an update of the data model bound to this grid.
773 	*/
774 	update: function() {
775 		var ds = this.getValueById((this.components.binding.wires["dataSet"] || 0).source);
776 		wm.fire(ds, "update");
777 	},
778 	canEdit: function(inCell, inRowIndex) {
779 		ioEdit = {canEdit: Boolean(inCell.editor)};
780 		this.onCanEdit(ioEdit, inCell, inRowIndex);
781 		return ioEdit.canEdit;
782 	},
783 	onCanEdit: function(ioEdit, inCell, inRowIndex) {
784 	},
785 	sort: function() {
786 		this.onSort(this.dijit.getSortField());
787 	},
788 	onSort: function(inSortInfo) {
789 	}
790 });
791 
792 wm.DataGrid.extend({
793 	set_dataSet: function(inDataSet) {
794 		// support setting dataSet via id from designer
795 		if (inDataSet && !(inDataSet instanceof wm.Variable)) {
796 			var ds = this.getValueById(inDataSet);
797 			if (ds)
798 				this.components.binding.addWire("", "dataSet", ds.getId());
799 		} else
800 			this.setDataSet(inDataSet);
801 	},
802 	doAddColumn: function(inColumn) {
803 		var col = {name: "column1", index: this._columns.length, label: "", owner: this };
804 		if (!this._columns.length)
805 			col.columnWidth = "auto";
806 		this._adjustColumnProps(col);
807 		this._addColumn(col);
808 		this.columnsChanged();
809 		if (this.isDesignLoaded()){
810 			var i = this._columns.length - 1;
811 			var c = (i>=0 ? this._columns[i] : this);
812 			studio.select(c);
813 		}
814 	},
815 	doRemoveColumn: function(inColumn) {
816 		inColumn.destroy();
817 		// FIXME: hack 
818 		this._columns = [];
819 		this.loaded();
820 		// end hack
821 		this.columnsChanged();
822 		if (this.isDesignLoaded()){
823 			var i = inColumn.index - 1;
824 			var c = this._columns[i < 0 ? 0 : i] || this;
825 			studio.select(c);
826 		}
827 	},
828 	doClearColumns: function() {
829 		this._clearColumns();
830 		this.renderGrid();
831 	},
832 	doAutoColumns: function() {
833 		//if (this._hasDefaultColumns())
834 		this._clearColumns();
835 		this.dataSetToColumns();
836 		this.renderGrid();
837 	},
838 	makePropEdit: function(inName, inValue, inDefault) {
839 		switch (inName) {
840 			case "addColumn":
841 			case "autoColumns":
842 			case "clearColumns":
843 			case "updateNow":
844 				return makeReadonlyButtonEdit(inName, inValue, inDefault);
845 			case "dataSet":
846 				return new wm.propEdit.DataSetSelect({component: this, name: inName, value: this.dataSet ? this.dataSet.getId() : "", allowAllTypes: true, listMatch: true});
847 		}
848 		return this.inherited(arguments);
849 	},
850 	editProp: function(inName, inValue, inInspector) {
851 		switch (inName) {
852 			case "addColumn":
853 				return this.doAddColumn();
854 			case "autoColumns":
855 				return this.doAutoColumns();
856 			case "clearColumns":
857 				return this.doClearColumns();
858 			case "updateNow":
859 				return this.update();
860 		}
861 		return this.inherited(arguments);
862 	}
863 });
864 
865 // design-time only
866 wm.Object.extendSchema(wm.DataGridColumn, {
867 	caption: { group: "common", order: 100, focus: 1 },
868 	addColumn: { group: "operation", order: 10},
869 	removeColumn: { group: "operation", order: 20},
870 	autoSize: { group: "layout", order: 10 },
871 	columnWidth: { group: "layout", order: 20 },
872 	index: { group: "layout", order: 30 },
873 	field: { group: "data", order: 10 },
874 	dataExpression: { group: "data", order: 15 },
875 	display: { group: "data", order: 20 },
876 	format: { group: "data", order: 30, categoryParent: "Properties", categoryProps: {component: "format"}},
877 	showing: {ignore: 1}
878 });
879 
880 wm.Object.extendSchema(wm.DataGrid, {
881 	selectedItem: { ignore: true, isObject: true, bindSource: true, simpleBindProp: true },
882 	emptySelection: { ignore: true, bindSource: 1, type: "Boolean" },
883 	dataSet: { readonly: true, group: "data", order: 0, type: "wm.Variable", isList: true, bindTarget: true},
884 	addColumn: { group: "operation", order: 1},
885 	autoColumns: { group: "operation", order: 5},
886 	clearColumns: { group: "operation", order: 10},
887 	updateNow: { group: "operation", order: 15},
888 	collection: { ignore: true },
889 	autoSize: { ignore: true },
890 	disabled: { ignore: true }
891 });
892