/* vim:set autoindent shiftwidth=4 tabstop=4 noexpandtab cindent: */

var ui = {};

/* HintBox */

ui.hintbox = {};

ui.hintbox.HintBox = function(params) {
	params = params || {};
	this.content = params.content || null;
	this.style = params.style || {};
	this.anchor = params.anchor || null;
	this.offsetX = params.offsetX || 0;
	this.offsetY = params.offsetY || 0;
	this.align = params.align || 'bottom right';
	this.snap = params.snap || 'top left';

	this.fader = new std.ani.Interpolator();
	this.fader.setOnEvent(std.lang.bind(this, this._onFaderEvent));
}

ui.hintbox.HintBox.prototype.getRootNode = function() {
	this._realize();
	return this.elements.root;
}

ui.hintbox.HintBox.prototype.setAnchorPosition = function(position) {
	this._realize();
	position = std.string.parsePosition(position);

	this.elements.upAnchor.style.visibility = position.top ? 'visible' : 'hidden';
	this.elements.downAnchor.style.visibility = position.top ? 'hidden' : 'visible';
	this.elements.upAnchor.style.left = this.elements.downAnchor.style.left = position.left ? '0' : '';
	this.elements.upAnchor.style.right = this.elements.downAnchor.style.right = position.left ? '' : '0';
}

ui.hintbox.HintBox.prototype.show = function() {
	this._realize();
	this._updatePosition();
	this.fader.reset(1);
}

ui.hintbox.HintBox.prototype.fade = function() {
	this._realize();
	this.fader.start(1, 0, 350, 25);
}

ui.hintbox.HintBox.prototype._updatePosition = function() {
	if (this.anchor) {

		var root = this.elements.root;
		var align = std.string.parsePosition(this.align);
		var snap = std.string.parsePosition(this.snap);

		// update UI and re-calculate UI dimensions
		var oDisplay = root.style.display;
		root.style.display = '';

		this.setAnchorPosition(std.string.printPosition({ top: align.bottom, bottom: align.top, left: align.right, right: align.left }));

		var rootDim = std.dom.getDimensions(root);
		var anchorDim = std.dom.getDimensions(this.elements.upAnchor);
		var anchorShift = Math.round(anchorDim.width/2);

		root.style.display = oDisplay;

		// anchor position
		var pos = {};
		if (this.anchor.length == 2 && typeof(this.anchor[0]) == 'number') {
			pos.top = pos.bottom = this.anchor;
		} else {
			var bounds = std.dom.getBounds(this.anchor);
			var x = Math.round((bounds.left+bounds.right)/2);
			pos.top = [ x, bounds.top ];
			pos.bottom = [ x, bounds.bottom ];
		}

		// calculate offset
		var offset = [];
		if (align.right) {
			offset[0] = this.offsetX + pos.top[0] - anchorShift;
		} else {
			offset[0] = this.offsetX + pos.top[0] - rootDim.width + anchorShift;
		}
		if (align.bottom) {
			offset[1] = this.offsetY + pos.bottom[1];
		} else {
			offset[1] = this.offsetY + pos.top[1] - rootDim.height;
		}

		// set box position
		var bodyDim = std.dom.getBodyExtent();
		if (snap.right) {
			root.style.left = '';
			root.style.right = bodyDim.width - offset[0] - rootDim.width + 'px';
		} else {
			root.style.left = offset[0] + 'px';
			root.style.right = '';
		}
		if (snap.bottom) {
			root.style.top = '';
			root.style.bottom = bodyDim.height - offset[1] - rootDim.height + 'px';
		} else {
			root.style.top = offset[1] + 'px';
			root.style.bottom = '';
		}
	}
}

ui.hintbox.HintBox.prototype._onFaderEvent = function(fader) {
	std.dom.opacity(this.elements.root, fader.getCurPoint());
	this.elements.root.style.display = fader.getCurPoint() <= 0 ? 'none' : '';
}

ui.hintbox.HintBox.prototype._onRelayout = function() {
	if (this.elements.root.style.display != 'none') this._updatePosition();
}

ui.hintbox.HintBox.prototype._realize = function() {
	if (!this.elements) {

		this.elements = {};

		var builder = new std.dom.Builder();

		this.elements.root = builder.push('div', { className: 'HintBox' }, { display: 'none' });

		builder.push('div', { className: 'HintBoxUp' });
		this.elements.upAnchor = builder.push('div', { className: 'HintBoxAnchor' });
		builder.push('div');
		builder.peek().appendChild(document['create'+'Comment'](' '));
		builder.pop();
		builder.pop();
		builder.pop();

		builder.push('div', { className: 'HintBoxBorderOuter' });
		builder.push('div', { className: 'HintBoxBorderInner' });
		this.elements.content = builder.append('div', { className: 'HintBoxContent' });
		builder.pop();
		builder.pop();

		builder.push('div', { className: 'HintBoxDown' });
		this.elements.downAnchor = builder.push('div', { className: 'HintBoxAnchor' });
		builder.push('div');
		builder.peek().appendChild(document['create'+'Comment'](' '));
		builder.pop();
		builder.pop();
		builder.pop();

		if (this.content) this.elements.content.appendChild(this.content);
		std.css.setStyle(this.elements.root, this.style);

		document.body.appendChild(this.elements.root);

		std.viewport.addResizeListener(std.lang.bind(this, this._onRelayout));

	}
}

/* ui.menu */

ui.menu = {};

/* Slide Menu */

ui.menu.SlideMenu = function(data, parent, anchorY) {
	this.data = data;
	this.parent = parent;
	this.anchorY = anchorY
	this.onActionInvoked = new std.func.Delegate();
	var nodes = this.nodes = {};
	var container = nodes.container = std.dom.newElement('div', parent, { className: 'SlideContainer' });
	container.style.width  = '100%';

	var builder = new std.dom.Builder(container);
	nodes.pane = builder.push  ('div', { className: 'SlideMenuPane' });
	nodes.head = builder.append('div', { className: 'SlideMenuHead' });
	nodes.body = builder.push  ('div', { className: 'SlideMenuBody' }, { overflowX: 'hidden' });
	nodes.cols = [];

	var totalWidth = 0;
	this.start = [];
	for (var i=0; i<data.columns.length; ++i) {
		this.start[i] = totalWidth;
		totalWidth += data.columns[i]
	}
	builder.push('table', { cellPadding: 0, cellSpacing: 0 }, { tableLayout: 'fixed', borderCollapse: 'collapse', width: totalWidth + 'px' });
	builder.push('tr');
	for (var i=0; i < data.columns.length; ++i) {
		nodes.cols[i] = builder.append('td', { className: 'SlideMenuColumn', vAlign: 'top' }, { width: data.columns[i] + 'px' });
	}
	builder.append('td', { vAlign: 'top' }, { width: '1000px' }); // sentinel
	builder.pop();
	builder.pop();
	builder.pop();
	builder.pop();
	
	this.items = {};
	nodes.items  = {};
	nodes.layers = {};
	nodes.layers.root = this._construct(data, 0);
	this.setColumn(0, nodes.layers.root);
}

ui.menu.SlideMenu.prototype.layout = function() {
	var nodes = this.nodes;
	var container = nodes.container;
	if (nodes.layers) {
		var lh = nodes.layers.root.offsetHeight;
		var h = nodes.head.offsetHeight + lh;

		for (var i in nodes.layers)
			nodes.layers[i].style.height = lh + 'px';

		container.style.height = h + 'px';
		container.style.top = (window.innerHeight - this.anchorY - h) + 'px';
	}
}

ui.menu.SlideMenu.prototype._construct = function(data, level) {
	var items = data.items;
	var nodes = this.nodes;
	var layer = std.dom.newElement('div', null, { className: 'SlideMenu noselect' }, { overflowY: 'auto' });
	var builder = new std.dom.Builder(layer);
	builder.push('table', { cellPadding: 0, cellSpacing: 0, width: "100%" }, { tableLayout: 'fixed', borderCollapse: 'collapse' });
	for (var i=0; i<items.length; ++i) {
		var node, item = items[i];
		this.items[item.id] = item;
		builder.push('tr');
		builder.push('td', { className: 'SlideMenuItem' });
		if (item.display == '-') {
			builder.append('div', { className: 'SlideMenuItemSeparator' });
		} else {
			node = nodes.items[item.id] = builder.push('a');
			builder.push('table', { cellPadding: 0, cellSpacing: 0, width: "100%" }, { tableLayout: 'fixed', borderCollapse: 'collapse' });
			builder.push('tr');
			if (item.expandable) {
				builder.append('td').appendChild(std.dom.newText(item.display));
				builder.append('td', { align: 'right' }).appendChild(std.dom.newText('>'));
			} else {
				builder.append('td', { colSpan: 2 }).appendChild(std.dom.newText(item.display));
			}
			builder.pop();
			builder.pop();
			builder.pop();

			std.event.observe(node, 'click', this._itemClickHandler(item, items, level));
			if (item.expandable) {
				nodes.layers[item.id] = this._construct(data.children[item.id], level+1);
			}
		}
		builder.pop();
		builder.pop();
	}
	builder.pop();
	return layer;
}

ui.menu.SlideMenu.prototype._itemClickHandler = function(curr, items, level) {
	return std.lang.bind(this, function() {

		if (std.lang.evaluate(curr.disabled))
			return;

		var nodes = this.nodes;
		for (var i=0; i<items.length; ++i) {
			var item = items[i];
			if (item.display != '-') {
				if (item.id == curr.id)
					std.css.includeClass(nodes.items[item.id], 'selected');
				else
					std.css.excludeClass(nodes.items[item.id], 'selected');
			}
		}
		this.clearColumnsAfter(level);
		if (curr.expandable) {
			this.setColumn(level+1, nodes.layers[curr.id]);
			nodes.body.scrollLeft = this.start[level];
		} else if (curr.action) {
			curr.action();
			this.onActionInvoked.call(curr);
		}
	});
}

ui.menu.SlideMenu.prototype.setVisible = function(visible) {
	if (this.visible != visible) {
		this.visible = visible
		this.layout();
		this.nodes.container.style.visibility = visible ? 'visible' : 'hidden';
	}
}

ui.menu.SlideMenu.prototype.clearColumnsAfter = function(idx) {
	var cols = this.nodes.cols;
	for (var i=idx + 1; i<cols.length; ++i) {
		if (cols[i].firstChild)
			cols[i].removeChild(cols[i].firstChild);
	}
}

ui.menu.SlideMenu.prototype.reset = function() {
	this.useColumn(0);
	this.clearColumnsAfter(0);
	this.repaint();
}

ui.menu.SlideMenu.prototype.repaint = function() {
	var nodes = this.nodes.items;
	var items = this.items;
	for (var i in nodes) {
		std.css.excludeClass(nodes[i], 'selected');
		if (std.lang.evaluate(items[i].disabled))
			std.css.includeClass(nodes[i], 'disabled');
		else
			std.css.excludeClass(nodes[i], 'disabled');
	}
}

ui.menu.SlideMenu.prototype.useColumn = function(idx) {
	this.nodes.body.scrollLeft = this.start[idx];
}

ui.menu.SlideMenu.prototype.setColumn = function(idx, layer) {
	var cols = this.nodes.cols;
	if (cols[idx].firstChild)
		cols[idx].replaceChild(layer, cols[idx].firstChild);
		cols[idx].appendChild(layer);
}

/**
 * Constructor.
 * @param {object} options A hash of options.
 * @param {ui.menu.Item[]} itemList The initial menu items to populate the item group with.
 * @class The Item Group component for grouping Menu Items.
 * <pre>
 * Example usage:
 *
 * itemgroup.get('new'); // get the menu item with id 'new' from the group
 * itemgroup.remove('new'); // remove the menu item with id 'new' from the group
 * </pre>
 */
ui.menu.ItemGroup = function(options, itemList) {
	options = options || {};
	this.setHeader(options.header || null);
	this.setHeaderStyle(options.headerStyle || {});
	this._itemList = itemList || [];
}

/**
 * Sets a header for the group. The header will be displayed in the menu as a section heading.
 * @param {string} header The header.
 */
ui.menu.ItemGroup.prototype.setHeader = function(header) {
	this._header = header;
}

ui.menu.ItemGroup.prototype.getHeader = function() {
	return this._header;
}

ui.menu.ItemGroup.prototype.setHeaderStyle = function(headerStyle) {
	this._headerStyle = headerStyle;
}

ui.menu.ItemGroup.prototype.getHeaderStyle = function() {
	return this._headerStyle;
}

/**
 * Gets a list of all menu items in the group.
 * @return {ui.menu.MenuItem[]} A list of all menu items.
 */
ui.menu.ItemGroup.prototype.getList = function() {
	return this._itemList;
}

ui.menu.ItemGroup.prototype._indexOf = function(ref) {
	if (typeof(ref) == 'string') ref = this.get(ref);
	return std.list.findIndex(this._itemList, ref);
}

/**
 * Gets a menu item by position or by id.
 * @param pos The position, or menu item id.
 */
ui.menu.ItemGroup.prototype.get = function(pos) {
	return typeof(pos) == 'number' ? this._itemList[pos] : std.list.find(this._itemList, function(item) { return item.getId() == pos });
}

/**
 * Adds a menu item to the end of the group.
 * @param {ui.menu.MenuItem} item The menu item to add.
 */
ui.menu.ItemGroup.prototype.add = function(item) {
	this._itemList.push(item);
}

/**
 * Inserts a menu item at the specific position.
 * @param pos The position to insert at, which could be a menu item (object), a menu item index (number), or an ID (string)
 * @param {ui.menu.MenuItem} item The menu item to add.
 */
ui.menu.ItemGroup.prototype.insert = function(pos, item) {
	this._itemList.splice(typeof(pos) == 'number' ? pos : this._indexOf(pos), 0, item);
}

/**
 * Removes a menu item at the specific position.
 * @param pos The position to remove from, which could be a menu item (object), a menu item index (number), or an ID (string)
 */
ui.menu.ItemGroup.prototype.remove = function(ref) {
	this._itemList.splice(typeof(ref) == 'number' ? ref : this._indexOf(ref), 1);
}


/**
 * Constructor.
 * Different types of Menu Item can be constructed by specifying the type.
 * Which could be button, checkbox, radio, menu or menubar
 * @param type The type of menu item to create.
 * @param {object} options The options hash.
 * @class The menu item component.
 */
ui.menu.MenuItem = function(type, options) {
	options = options || {};

	if (typeof(type) != 'undefined' && type != 'button' && type != 'checkbox' && type != 'radio' && type != 'menu' && type != 'menubar')
		throw "Invalid MenuItem type: " + type;

	this._type = type;
	this.setId(options.id || null);
	this.setIcon(options.icon || null);
	this.setText(options.text || null);
	this.setMnemonic(options.mnemonic || null);
	this.setStyle(options.style || {});
	this.setDisabled(options.disabled || null);
	this.setHidden(options.hidden || false);
	this.setAction(options.action || null);
	this.setValue(options.value || null);
	this.setShortcut(options.shortcut || null);
	this.setGroup(options.group || null);
}

/**
 * Gets the menu item type.
 * @return The type.
 */
ui.menu.MenuItem.prototype.getType = function() {
	return this._type;
}

/**
 * Gets the menu item id.
 * @return The id.
 */
ui.menu.MenuItem.prototype.getId = function() {
	return this._id;
}

/**
 * Sets the menu item id.
 * @param id The id.
 */
ui.menu.MenuItem.prototype.setId = function(id) {
	this._id = id;
}

ui.menu.MenuItem.prototype.setIcon = function(icon) {
	this._icon = icon;
}

ui.menu.MenuItem.prototype.getIcon = function() {
	return std.lang.evaluate(this._firstSet(this._icon, this._action.getIcon()));
}

/**
 * Sets the menu item text, for displaying.
 * @param text The text.
 */
ui.menu.MenuItem.prototype.setText = function(text) {
	this._text = text;
}

/**
 * Gets the menu item text.
 * @return The text.
 */
ui.menu.MenuItem.prototype.getText = function() {
	return std.lang.evaluate(this._firstSet(this._text, this._action.getText()));
}

/**
 * Sets the menu item mnemonic.
 * A mnemonic is a single key (alphanumeric) that fires the item directly.
 * @param mnemonic The mnemonic character.
 */
ui.menu.MenuItem.prototype.setMnemonic = function(mnemonic) {
	this._mnemonic = mnemonic;
}

/**
 * Gets the menu item mnemonic.
 * @return The mnemonic character.
 */
ui.menu.MenuItem.prototype.getMnemonic = function() {
	return std.lang.evaluate(this._firstSet(this._mnemonic, this._action.getMnemonic()));
}

ui.menu.MenuItem.prototype.setStyle = function(style) {
	this._style = style;
}

ui.menu.MenuItem.prototype.getStyle = function() {
	return std.lang.evaluate(this._style);
}

/**
 * Sets whether the menu item is disabled.
 * @param bool Disable or not.
 */
ui.menu.MenuItem.prototype.setDisabled = function(bool) {
	this._disabled = bool;
}

/**
 * Checks if the menu item is disabled.
 * @return true if is disabled.
 */
ui.menu.MenuItem.prototype.isDisabled = function(bool) {
	return std.lang.evaluate(this._firstSet(this._disabled, this._action.isDisabled()));
}

/**
 * Sets whether the menu item is hidden.
 * @param bool To be hidden or not.
 */
ui.menu.MenuItem.prototype.setHidden = function(bool) {
	this._hidden = bool;
}

/**
 * Checks if the menu item is hidden.
 * @return true if is hidden.
 */
ui.menu.MenuItem.prototype.isHidden = function() {
	return std.lang.evaluate(this._hidden);
}

ui.menu.MenuItem.prototype.setShortcut = function(shortcut) {
	this._shortcut = shortcut;
}

ui.menu.MenuItem.prototype.getShortcut = function() {
	return std.lang.evaluate(this._firstSet(this._shortcut, (this._action.getShortcutList() || [])[0]));
}

ui.menu.MenuItem.prototype.setGroup = function(group) {
	this._group = group;
}

ui.menu.MenuItem.prototype.getGroup = function() {
	return std.lang.evaluate(this._group);
}

ui.menu.MenuItem.prototype.setParent = function(parent) {
	this._parent = parent;
}

ui.menu.MenuItem.prototype.getParent = function() {
	return this._parent;
}

ui.menu.MenuItem.prototype.getRoot = function() {
	var root = this._parent;
	while (root && root._parent) root = root._parent;
	return root;
}

/**
 * Sets the action associated with this menu item.
 * By default, the menu item will inherits all attributes from the action.
 * @param action The action.
 */
ui.menu.MenuItem.prototype.setAction = function(action) {
	this._action = action || new std.action.Nil();
}

ui.menu.MenuItem.prototype.setValue = function(value) {
	this._value = value;
}

ui.menu.MenuItem.prototype.isSelected = function() {
	return this._action && this._action.isSelected();
}

ui.menu.MenuItem.prototype.setSelected = function(value) {
	if (this._action) this._action.setSelected(value);
}

ui.menu.MenuItem.prototype.performAction = function() {
	switch (this._type) {
		case 'checkbox':
		case 'radio':
			this._action.setSelected(!this._action.isSelected());
	}
	this._action.setValue(std.lang.evaluate(this._value));
	this._action.performAction();
	if (this._parent.onChildActionPerformed) this._parent.onChildActionPerformed();
}

ui.menu.MenuItem.prototype._firstSet = function() {
	for (var i = 0; i < arguments.length; i++) {
		var arg = arguments[i];
		if ((typeof(arg) != "undefined") && (arg != null)) return arg;
	}
	return null;
}


ui.menu.AbstractMenu = function(type, options) {
	options = options || {};
	ui.menu.MenuItem.call(this, type, options);
}

ui.menu.AbstractMenu.prototype = new ui.menu.MenuItem();

/**
 * Constructor.
 * @param {object} options A hash of options.
 * @param {ui.menu.ItemGroup[]} itemGroups The initial menu item groups to populate the menu with.
 * @class The Menu component.
 * <pre>
 * Example usage:
 *
 * menu.getItem('new'); // get the menu item with id 'new' from the menu
 * menu.get(0); // get the first menu item group from the menu
 * </pre>
 */
ui.menu.Menu = function(options, itemGroups) {
	options = options || {};
	ui.menu.AbstractMenu.call(this, "menu", options);

	this._delay = options.delay || 250;
	this._visible = false;
	// new options to allow setting width and hide the hack_iframe
	this._width = options.width || null;
	this._noiframe = options.noiframe || false;

	this._itemGroupList = [];
	if (itemGroups) std.list.iterate(itemGroups, std.lang.bind(this, this.add));

	std.event.keyObserve(document, 'keydown', std.event.listener(this, this._onKeyDown));

	this._focusDelegator = std.customfocus.getManager().newDelegator(this, std.lang.bind(this, this._onFocus), std.lang.bind(this, this._onBlur));
}

ui.menu.Menu.prototype = new ui.menu.AbstractMenu();

ui.menu.Menu.prototype._createView = function() {
	var nodes = {};

	var builder = new std.dom.Builder();
	nodes.root = builder.push('div', { className: 'PopupMenu' });
	if (!this._noiframe && std.browser.isIE && std.browser.major < 7.0) {
		// iframe hack to hide form control on IE 5,6. Set src = "javascript:'';" to prevent security warning when loaded with SSL.
		nodes.hackIFrame = builder.append('iframe', { frameBorder: 0, scrolling: 'no', src: "javascript:'';" });
	}
	nodes.pane = builder.push('div', { className: 'PopupMenuPane' });
	builder.push('div', { className: 'PopupMenuShadow' });
	nodes.cavity = builder.push('div', { className: 'PopupMenuBorder' });

	std.event.observe(nodes.pane, 'mousedown', std.event.listener(this, this._onMenuMouseDown));
	std.event.observe(nodes.pane, 'mouseover', std.event.listener(this, this._onButtonMouseOver));
	std.event.observe(nodes.pane, 'mouseout', std.event.listener(this, this._onButtonMouseOut));
	std.event.observe(nodes.pane, 'mousedown', std.event.listener(this, this._onButtonMouseDown));
	std.event.observe(nodes.pane, 'click', std.event.listener(this, this._onButtonClick));

	if (this._width) { std.css.setStyle(nodes.root, { width: this._width + 'px' }); }

	return nodes;
}

ui.menu.Menu.prototype._getNodes = function() {
	return this._nodes || (this._nodes = this._createView());
}

/**
 * Redraws the UI
 */
ui.menu.Menu.prototype.updateUI = function() {

	this._elementList = [];

	var html = [];
	html.push('<table><tbody>');
	
	var curIndex = 0;

	for (var i = 0; i < this._itemGroupList.length; i++) {

		var itemGroup = this._itemGroupList[i];
		var itemList = itemGroup.getList();

		if (i > 0) {
			html.push('<tr class="PopupMenuSep">');
			html.push('<td colspan="5"><div><!-- --></div></td>');
			html.push('</tr>');
		}

		if (itemGroup.getHeader()) {
			html.push('<tr class="PopupMenuSection">');
			html.push('<td colspan="5">' + std.string.fullEscapeHTML(itemGroup.getHeader()) + '</td>');
			html.push('</tr>');
		}

		for (var j = 0; j < itemList.length; j++) {

			var item = itemList[j];

			if (item.isHidden())
				continue;

			var element = {
				item: item,
				type: item.getType(),
				index: curIndex++,
				iconStyle: null,
				nodes: {}
			};

			this._elementList.push(element);
			item.setParent(this);

			var shortcut = item.getShortcut();

			// NOTE: modified by hclee for customized html tables structure for supporting cell backgrounds
			html.push('<tr class="' + (item.isDisabled() ? 'PopupMenuDisabledItem' : 'PopupMenuItem') + '"><td class="RowHolder"><table><tr class="ContentHolder">');

			// build icon td

			html.push('<td class="PopupMenuItemIcon">');

			var iconClassName = null;
			switch (element.type) {
				case "checkbox":
					iconClassName = item.isSelected() ? 'PopupMenuIcon PopupMenuIconCheckBox' : 'PopupMenuIcon';
					break;
				case "radio":
					iconClassName = item.isSelected() ? 'PopupMenuIcon PopupMenuIconRadio' : 'PopupMenuIcon';
					break;
				default:
					var icon = item.getIcon();
					if (icon) {
						iconClassName = 'PopupMenuIcon';
						element.iconStyle = (typeof(icon) == 'string') ? { backgroundImage: 'url(' + icon + ')' } : icon;
					}
					break;
			}

			html.push('<div' + (iconClassName ? (' class="' + iconClassName + '"') : '') + '><!-- --></div>');

			html.push('</td>');

			// build name td

			html.push('<td class="PopupMenuItemName"' + (shortcut ? '' : ' colspan="2"') + '>');

			var text = item.getText();
			var mnemonic = item.getMnemonic();
			var cutIndex = mnemonic ? text.toLowerCase().indexOf(mnemonic.toLowerCase()) : text.length;

			var prefix = cutIndex >= 0 ? text.substr(0, cutIndex) : '';
			var midfix = cutIndex >= 0 ? text.substr(cutIndex, 1) : '';
			var suffix = text.substr(cutIndex + 1);

			if (prefix) html.push(std.string.fullEscapeHTML(prefix));
			if (midfix) html.push('<span style="text-decoration: underline">' + std.string.fullEscapeHTML(midfix) + '</span>');
			if (suffix) html.push(std.string.fullEscapeHTML(suffix));

			html.push('</td>');

			// build command td
			
			if (shortcut) {
				html.push('<td class="PopupMenuItemShortcut">');
				html.push(std.string.fullEscapeHTML(shortcut.toString() || ''));
				html.push('</td>');
			}

			// build expand td

			html.push('<td class="PopupMenuItemExpand">');
			if (element.type == "menu") {
				html.push('<div class="PopupMenuIcon PopupMenuIconExpand"><!-- --></div>');
			}
			html.push('</td>');

			// NOTE: modified by hclee for customized html tables structure for supporting cell backgrounds
			html.push('</tr></table></td></tr>');

		}

	}

	html.push('</tbody></table>');

	var cavity = this._getNodes().cavity;
	cavity.innerHTML = html.join('');

	var elementList = this._elementList;
	var index = 0;

	std.list.iterate(cavity.getElementsByTagName('tr'), function(row) {

		// NOTE: modified by hclee for customized html tables structure for supporting cell backgrounds
		//if (row.className == 'PopupMenuItem' || row.className == 'PopupMenuDisabledItem') {
		if (row.className == 'ContentHolder') {

			row._index = index;

			var element = elementList[index];
			element.nodes.root = row;

			std.list.iterate(row.getElementsByTagName('td'), function(td) { std.dom.noselect(td) });

			if (element.iconStyle) {
				std.list.iterate(row.getElementsByTagName('div'), function(div) {
					if (div.className.indexOf('PopupMenuIcon') == 0) std.dom.setStyles(div, element.iconStyle);
				});
			}

			if (element.item.getStyle()) {
				std.dom.setStyles(row, element.item.getStyle());
			}

			index++;

		}

	});

}

/**
 * Gets a list of all the menu item groups.
 * @return {ui.menu.ItemGroup[]} The list of menu item groups.
 */
ui.menu.Menu.prototype.getList = function() {
	return this._itemGroupList;
}

/**
 * Gets a menu item by id.
 * @param id the menu item's id.
 * @return {ui.menu.MenuItem} The menu item.
 */
ui.menu.Menu.prototype.getItem = function(id) {
	for (var i = 0; i < this._itemGroupList.length; i++) {
		var item = this._itemGroupList[i].get(id);
		if (item) return item;
	}
	return null;
}

ui.menu.Menu.prototype._indexOf = function(ref) {
	return std.list.findIndex(this._itemGroupList, ref);
}

/**
 * Gets the menu item group at the specific position.
 * @param pos The position.
 * @return {ui.menu.ItemGroup} The menu item group.
 */
ui.menu.Menu.prototype.get = function(pos) {
	return this._itemGroupList[pos];
}

/**
 * Adds a menu item group to the end.
 * @param {ui.menu.ItemGroup} itemGroup The item group to be added.
 */
ui.menu.Menu.prototype.add = function(itemGroup) {
	this._itemGroupList.push(itemGroup);
}

/**
 * Inserts a menu item group at the specific position.
 * @param pos The position.
 * @param {ui.menu.ItemGroup} itemGroup The menu item group.
 */
ui.menu.Menu.prototype.insert = function(pos, itemGroup) {
	this._itemGroupList.splice(typeof(pos) == 'number' ? pos : this._indexOf(pos), 0, itemGroup);
}

/**
 * Removes a menu item group from the specific position.
 * @param pos The postion.
 */
ui.menu.Menu.prototype.remove = function(pos) {
	this._itemGroupList.splice(typeof(pos) == 'number' ? pos : this._indexOf(pos), 1);
}

ui.menu.Menu.prototype.select = function(index) {
	var element = typeof(index) == "number" ? this._elementList[index] : null;
	// NOTE: modified by hclee for customized html tables structure for supporting cell backgrounds
	if (this._selectedElement && this._selectedElement != element) {
	   	var holder = std.dom.backtrack(this._selectedElement.nodes.root.parentNode, function(ele) { return ele.tagName != 'TR' });
		holder.className = this._selectedElement.item.isDisabled() ? "PopupMenuDisabledItem" : "PopupMenuItem";
	}
	this._selectedElement = element;
	if (this._selectedElement) {
	   	var holder = std.dom.backtrack(this._selectedElement.nodes.root.parentNode, function(ele) { return ele.tagName != 'TR' });
		ui.menu.getMenuManager().setActiveMenu(this);
		holder.className = this._selectedElement.item.isDisabled() ? "PopupMenuDisabledItemHover" : "PopupMenuItemHover";
	}
}

ui.menu.Menu.prototype.selectOffset = function(offset) {
	var index = this._selectedElement ? this._selectedElement.index : 0;
	index = (index + offset + this._elementList.length) % this._elementList.length;
	this.select(index);
}

ui.menu.Menu.prototype.getSelectedItem = function() {
	return this._selectedElement ? this._selectedElement.item : null;
}

ui.menu.Menu.prototype.setExpanded = function(bool) {
	var expandedElement = this._getExpandedElement();
	if (expandedElement && (!bool || expandedElement != this._selectedElement)) {
		expandedElement.item.hide();
		expandedElement.item.setParent(null);
	}
	if (bool && this._selectedElement && this._selectedElement.item.getType() == 'menu' && expandedElement != this._selectedElement ) {
		var bounds = std.dom.getBounds(this._selectedElement.nodes.root);
		this._selectedElement.item.setParent(this);
		this._selectedElement.item.show(bounds.right, bounds.top, 'top left');
	}
}

ui.menu.Menu.prototype._getExpandedElement = function() {
	for (var i = 0; i < this._elementList.length; i++) {
		var element = this._elementList[i];
		if (element.item.getType() == "menu" && element.item.isVisible())
			return element;
	}
	return null;
}

ui.menu.Menu.prototype.getMnemonicIndex = function(mnemonic) {
	for (var i = 0; i < this._elementList.length; i++) {
		if (this._elementList[i].item.getMnemonic().toLowerCase() == mnemonic.toLowerCase())
			return i;
	}
	return -1;
}

ui.menu.Menu.prototype.isVisible = function() {
	return this._visible;
}

ui.menu.Menu.prototype.onChildActionPerformed = function() {
	if (this.getParent()) {
		this.getParent().onChildActionPerformed();
	} else {
		this.hide();
	}
}

ui.menu.Menu.prototype._onFocus = function() {
	ui.menu.getMenuManager().menuFocused();
}

ui.menu.Menu.prototype._onBlur = function() {
	ui.menu.getMenuManager().menuBlurred();
	this.hide();
}

ui.menu.Menu.prototype.getActivator = function() {
	return this._activator || (this._activator = new ui.menu.Activator(this));
}

ui.menu.Menu.prototype._findEventElement = function(e) {
	return std.dom.backtrack(std.event.element(e), function(ele) { return ele.tagName != 'TR' });
}

ui.menu.Menu.prototype._onMenuMouseDown = function(e) {
	if (this._visible) { // this condition is important for proper handling of onclick event
		std.event.vanish(e);
	}
}

ui.menu.Menu.prototype._onButtonMouseOver = function(e) {
	var node = this._findEventElement(e);
	if (node) {
		this.select(node._index);
		this.buttonHoverTimer = setTimeout(std.lang.bind(this, this._onButtonHover), this._delay);
	}
}

ui.menu.Menu.prototype._onButtonMouseOut = function(e) {
	var node = this._findEventElement(e);
	if (node) {
		if (!this._getExpandedElement()) this.select(null);
		clearTimeout(this.buttonHoverTimer);
	}
}

ui.menu.Menu.prototype._onButtonMouseDown = function(e) {
	var node = this._findEventElement(e);
	if (node) {
		this.setExpanded(true);
	}
}

ui.menu.Menu.prototype._onButtonClick = function(e) {
	var node = this._findEventElement(e);
	if (node) {
		var item = this._elementList[node._index].item;
		switch (item.getType()) {
			case "button":
			case "radio":
			case "checkbox":
				if (!item.isDisabled()) item.performAction();
		}
	}
}

ui.menu.Menu.prototype._onButtonHover = function() {
	this.setExpanded(true);
}

ui.menu.Menu.prototype._onKeyDown = function(ke) {
	if (ke.keyIdentifier == std.event.KeyCodes.generic.KEY_ESCAPE) this.hide();
}

ui.menu.Menu.prototype.relocate = function(x, y, alignment) {

	var nodes = this._getNodes();

	var dim = std.dom.getDimensions(nodes.pane);
	var posX = x - (/right/.test(alignment) ? (dim.width - 1) : 0);
	var posY = y - (/bottom/.test(alignment) ? (dim.height - 1) : 0);

	nodes.root.style.left = posX + 'px';
	nodes.root.style.top = posY + 'px';

	if (nodes.hackIFrame) {
		nodes.hackIFrame.style.width = dim.width + 2 + 'px';
		nodes.hackIFrame.style.height = dim.height + 2 +'px';
	}

}

ui.menu.Menu.prototype.show = function(x, y, alignment) {

	if (!this._visible) {

		this._visible = true;

		var root = this._getNodes().root;

		this.updateUI();

		root.style.left = '0px'; // prevent scrolling before well positioned
		root.style.top = '0px';
		root.style.visibility = 'hidden';
		document.body.appendChild(root);
		this.relocate(x, y, alignment);

		root.style.visibility = '';

		ui.menu.getMenuManager().setActiveMenu(this);

		if (!this._parent || this._parent.getType() != 'menu') // focus is handled by the root menu
			this._focusDelegator.focus();

	} else {

		this.relocate(x, y, alignment);

	}
}

ui.menu.Menu.prototype.hide = function() {

	if (this._visible) {

		this._visible = false;

		if (this._getExpandedElement()) this._getExpandedElement().item.hide();
		this._selectedElement = null;

		document.body.removeChild(this._getNodes().root);

		this._focusDelegator.blur();
		this._focusDelegator.detach();

	}

}

ui.menu.Menu.prototype.toggle = function(x, y, alignment) {
	if (this._visible) this.hide();
	else this.relocate(x, y, alignment);
}

ui.menu.Menu.prototype.populateWith = function(data) { // deprecated, for backward compatibility

	for (var i = 0; i < data.items.length; i++) {
		var itemList = data.items[i];
		var itemGroup = new ui.menu.ItemGroup();
		this.add(itemGroup);

		for (var j = 0; j < itemList.length; j++) {
			var item = itemList[j];

			if (data.children && data.children[item.id]) {
				var childMenu = new ui.menu.Menu({ id: item.id, text: item.display, mnemonic: item.mnemonic })
				itemGroup.add(childMenu);
				childMenu.populateWith(data.children[item.id]);
			} else {
				var action = null;
				switch (typeof(item.action)) { // auto-wrap with appropriate action
					case "string":
						  action = new std.action.Url({ url: item.action });
						  break;
					case "function":
						  action = new std.action.Script({ script: item.action });
						  break;
					case "object":
						  action = item.action || new std.action.Nil();
						  break;
				}
				itemGroup.add(new ui.menu.MenuItem("button", {
					id: item.id,
					text: item.display,
					mnemonic: item.mnemonic,
					style: item.style,
					disabled: item.disabled,
					hidden: item.hidden,
					action: action,
					value: item.value,
					group: item.group
				}));
			}
		}

	}
}


ui.menu.Activator = function(menu) {
	this._menu = menu;
}

ui.menu.Activator.prototype.getMenu = function() {
	return this._menu;
}

ui.menu.Activator.prototype.getSrcDomNode = function() {
	return this._srcDomNode;
}

// onActivate(activator) returns an array of [x, y, alignment] for ui.menu.show()
ui.menu.Activator.prototype.observe = function(domNode, eventName, onActivate) {
	domNode = std.dom.element(domNode);
	std.event.observe(domNode, eventName, std.event.listener(this, this._onEventObserved, domNode, onActivate));
}

ui.menu.Activator.prototype._onEventObserved = function(e, domNode, onActivate) {

	var lastDomNode = this._srcDomNode;
	this._srcDomNode = domNode;

	if (!this._menu.isVisible() || lastDomNode != domNode) {
		setTimeout(std.lang.bind(this, this._showMenu, onActivate), 0);
	}

}

ui.menu.Activator.prototype._showMenu = function(onActivate) {
	var ret = onActivate(this);
	if (ret) { // skip showing menu if ret is null or undefined or false
		var x = ret.shift();
		var y = ret.shift();
		var alignment = ret.shift();
		this._menu.show(x, y, alignment);
	}
}


/**
 * Constructor.
 * @param {object} options A hash of options.
 * @class The Menu Bar component.
 * <pre>
 * Example usage:
 *
 * menubar.get('edit'); // get the menu with id 'edit'
 * menubar.get(1); // get the 2nd menu
 * menubar.remove('edit'); // remove the menu with id 'edit'
 * menubar.remove(1); // remove the 2nd menu
 * </pre>
 */
ui.menu.MenuBar = function(options) {
	options = options || {};

	ui.menu.AbstractMenu.call(this, "menubar", options);

	this._itemList = [];

	std.event.observe(document, 'mousedown', std.lang.bind(this, this.select, null));
	std.event.keyObserve(document, 'keydown', std.event.listener(this, this._onKeyDown, null));

}

ui.menu.MenuBar.prototype = new ui.menu.AbstractMenu();

ui.menu.MenuBar.prototype._createView = function() {
	var nodes = {};
	nodes.root = std.dom.newElement('div');
	nodes.cavity = nodes.root;
	return nodes;
}

ui.menu.MenuBar.prototype._getNodes = function() {
	if (!this._nodes) {
		this._nodes = this._createView();
		this.updateUI();
	}
	return this._nodes;
}

ui.menu.MenuBar.prototype.getRoot = function() {
	return this._getNodes().root;
}

/**
 * Redraws the UI
 */
ui.menu.MenuBar.prototype.updateUI = function() {

	this._elementList = [];

	var cavity = this._getNodes().cavity;
	std.dom.clearChildren(cavity);

	var builder = new std.dom.Builder(cavity);

	for (var i = 0, index = 0; i < this._itemList.length; i++) {

		var item = this._itemList[i];

		if (!item.isHidden()) {

			var element = {
				item: item,
				index: index,
				nodes: {}
			};

			this._elementList.push(element);

			element.nodes.root = builder.push('div', { className: 'MenuBarItem' });
			element.nodes.root._index = index;
			index++;

			element.nodes.icon = builder.push('div');
			builder.appendElement(std.dom.newComment(' ')); // TODO handle icon
			builder.pop();

			element.nodes.text = builder.push('div');
			builder.appendElement(std.dom.newText(element.item.getText()));
			builder.pop();

			std.dom.noselect(element.nodes.icon);
			std.dom.noselect(element.nodes.text);

			std.event.observe(element.nodes.root, 'mouseover', std.event.listener(this, this._onButtonMouseOver, element));
			std.event.observe(element.nodes.root, 'mouseout', std.event.listener(this, this._onButtonMouseOut, element));
			std.event.observe(element.nodes.root, 'mousedown', std.event.listener(this, this._onButtonMouseDown, element));

			if (element.item.getStyle()) {
				std.dom.setStyles(element.nodes.root, element.item.getStyle());
			}

			builder.pop();

		}

	}

}

ui.menu.MenuBar.prototype.getType = function() {
	return "menubar";
}

ui.menu.MenuBar.prototype.select = function(index) {
	var oldElement = this._selectedElement;
	var newElement = typeof(index) == "number" ? this._elementList[index] : null;

	if (oldElement != newElement) {

		var expanded = this.isExpanded();

		if (oldElement) {
			if (expanded) {
				oldElement.item.hide();
				oldElement.item.setParent(null);
			}
			oldElement.nodes.root.className = 'MenuBarItem';
		}

		if (newElement) {
			if (expanded) {
				var bounds = std.dom.getBounds(newElement.nodes.root);
				newElement.item.setParent(this);
				newElement.item.show(bounds.left, bounds.bottom, 'top left');
				newElement.nodes.root.className = 'MenuBarItemActive';
			} else {
				newElement.nodes.root.className = 'MenuBarItemHover';
			}
		}

		this._selectedElement = newElement;
	}
}

ui.menu.MenuBar.prototype.selectOffset = function(offset) {
	var index = this._selectedElement ? this._selectedElement.index : 0;
	index = (index + offset + this._elementList.length) % this._elementList.length;
	this.select(index);
}

ui.menu.MenuBar.prototype.setExpanded = function(bool) {
	if (this._selectedElement) {
		var expanded = this.isExpanded();
		if (bool && !expanded) {
			var bounds = std.dom.getBounds(this._selectedElement.nodes.root);
			this._selectedElement.item.setParent(this);
			this._selectedElement.item.show(bounds.left, bounds.bottom, 'top left');
			this._selectedElement.nodes.root.className = 'MenuBarItemActive';
		} else if (!bool && expanded) {
			this._selectedElement.item.hide();
			this._selectedElement.item.setParent(null);
			this._selectedElement.nodes.root.className = 'MenuBarItemHover';
		}
	}
}

ui.menu.MenuBar.prototype.getSelectedItem = function() {
	return this._selectedElement ? this._selectedElement.item : null;
}

ui.menu.MenuBar.prototype.isExpanded = function() {
	return this._selectedElement && this._selectedElement.item.isVisible();
}

ui.menu.MenuBar.prototype.onChildActionPerformed = function() {
	this.setExpanded(false);
	this.select(null);
}

ui.menu.MenuBar.prototype._onButtonMouseOver = function(e, element) {
	this.select(element.index);
}

ui.menu.MenuBar.prototype._onButtonMouseOut = function(e, element) {
	if (!this.isExpanded()) this.select(null);
}

ui.menu.MenuBar.prototype._onButtonMouseDown = function(e, element) {
	this.setExpanded(!this.isExpanded());
	std.event.vanish(e);
}

ui.menu.MenuBar.prototype._onKeyDown = function(ke) {
	if (ke.keyIdentifier == std.event.KeyCodes.generic.KEY_ESCAPE) this.select(null);
}

/**
 * Gets a list of all the menus.
 * @return {ui.menu.Menu[]} The list of menus.
 */
ui.menu.MenuBar.prototype.getList = function() {
	return this._itemList;
}

ui.menu.MenuBar.prototype._indexOf = function(ref) {
	if (typeof(ref) == 'string') ref = this.get(ref);
	return std.list.findIndex(this._itemList, ref);
}

/**
 * Gets a menu by id or by position.
 * @param pos The position, or the item id of the menu.
 * @return {ui.menu.Menu} The menu.
 */
ui.menu.MenuBar.prototype.get = function(pos) {
	return typeof(pos) == 'number' ? this._itemList[pos] : std.list.find(this._itemList, function(item) { return item.getId() == pos });
}

/**
 * Adds a menu to the end.
 * @params {ui.menu.Menu} item The menu to add.
 */
ui.menu.MenuBar.prototype.add = function(item) {
	this._itemList.push(item);
}

/**
 * Inserts a menu at the specific position.
 * @params pos The position to insert at, which can be a menu item (object), a position index (number), or an item id (string).
 * @params {ui.menu.Menu} item The menu to insert.
 */
ui.menu.MenuBar.prototype.insert = function(pos, item) {
	this._itemList.splice(typeof(pos) == 'number' ? pos : this._indexOf(pos), 0, item);
}

/**
 * Removes a menu from the specific position.
 * @params pos The position to remove from, which can be a menu item (object), a position index (number), or an item id (string).
 */
ui.menu.MenuBar.prototype.remove = function(pos) {
	this._itemList.splice(typeof(pos) == 'number' ? pos : this._indexOf(pos), 1);
}

ui.menu.MenuBar.prototype.populateWith = function(data) { // deprecated, for backward compatibility
	for (var i = 0; i < data.items.length; i++) {
		var item = data.items[i];
		var childMenu = new ui.menu.Menu({
			id: item.id,
			text: item.display,
			mnemonic: item.mnemonic,
			style: item.style,
			disabled: item.disabled,
			hidden: item.hidden
		})
		this.add(childMenu);
		childMenu.populateWith(data.children[item.id]);
	}
}


ui.menu.MenuManager = function() {
	std.event.keyObserve(document, 'keypress', std.lang.bind(this, this._onKeyPress));
}

ui.menu.MenuManager.prototype.setActiveMenu = function(menu) {
	this._activeMenu = menu;
}

ui.menu.MenuManager.prototype.select = function(direction) {
	if (this._activeMenu && this._activeMenu.isVisible()) {

		var menu = this._activeMenu;
		var selectedItem = menu.getSelectedItem();

		switch (direction) {

			case "up":
				menu.selectOffset(-1);
				break;

			case "down":
				selectedItem ? menu.selectOffset(1) : menu.select(0);
				break;

			case "left":
				if (menu.getParent()) {
					var parentMenu = menu.getParent();
					if (parentMenu.getType() == "menu") {
						menu.hide();
						parentMenu.selectOffset(0);
					} else if (parentMenu.getType() == "menubar") {
						parentMenu.selectOffset(-1);
						parentMenu.setExpanded(true);
						parentMenu.getSelectedItem().select(0);
					}
				}
				break;

			case "right":
				if (!selectedItem) {
					menu.select(0);
				} else if (selectedItem.getType() == "menu") {
					menu.setExpanded(true);
					selectedItem.select(0);
				} else if (menu.getRoot() && menu.getRoot().getType() == "menubar") {
						var rootMenu = menu.getRoot();
						rootMenu.selectOffset(1);
						rootMenu.setExpanded(true);
						rootMenu.getSelectedItem().select(0);
				}
				break;

		}

	}
}

ui.menu.MenuManager.prototype.doAction = function() {
	if (this._activeMenu && this._activeMenu.isVisible()) {
		var menu = this._activeMenu;
		var selectedItem = menu.getSelectedItem();
		switch (selectedItem.getType()) {
			case "menu":
				menu.setExpanded(true);
				selectedItem.select(0);
				break;
			default:
				if (!selectedItem.isDisabled())
					selectedItem.performAction();
				break;
		}
	}
}

ui.menu.MenuManager.prototype._onKeyPress = function(ke) {
	if (this._activeMenu && this._activeMenu.isVisible() && this._hasFocusedMenu) {
		var k = ke.keyIdentifier;
		var e = ke.evt;
		var keyCodes = std.event.KeyCodes.generic;
		switch (k) {
			case keyCodes.KEY_LEFT_ARROW:
				this.select('left');
				std.event.vanish(e);
				break;
			case keyCodes.KEY_RIGHT_ARROW:
				this.select('right');
				std.event.vanish(e);
				break;
			case keyCodes.KEY_UP_ARROW:
				this.select('up');
				std.event.vanish(e);
				break;
			case keyCodes.KEY_DOWN_ARROW:
				this.select('down');
				std.event.vanish(e);
				break;	
			case keyCodes.KEY_ENTER:
				this.doAction();
				std.event.vanish(e);
				break;
			default:
				var mnemonic = std.event.getChar(e);
				var index = this._activeMenu.getMnemonicIndex(mnemonic);
				if (index >= 0) {
					this._activeMenu.select(index);
					this.doAction();
				}
				std.event.vanish(e);
				break;
		}
	}
}

ui.menu.MenuManager.prototype.menuFocused = function() {
	this._hasFocusedMenu = true;
}

ui.menu.MenuManager.prototype.menuBlurred = function() {
	this._hasFocusedMenu = false;
}

ui.menu.getMenuManager = function() {
	if (!this._menuManager) {
		this._menuManager = new ui.menu.MenuManager();
	}
	return this._menuManager;
}


ui.menu2 = ui.menu; // alias to maintain backward compability (menu2 is changed to menu)


/* Notif - Notification Handling */

ui.notif = {};

ui.notif.Notif = function(baseUri, name) {
	this.baseUri = baseUri.replace(/\/?$/, '/');
	this.name = name;
}

ui.notif.Notif.prototype.accept = function() {
	std.ajax.ping(this.baseUri + 'notif/accept/' + this.name);
}

ui.notif.Notif.prototype.ifShallShow = function(callback) {
	new std.ajax.Request(this.baseUri + 'notif/shallshow/' + this.name, { method: 'post', asynchronous: true, onComplete: std.lang.bind(this, this._onShallShowComplete, callback) });
}

ui.notif.Notif.prototype._onShallShowComplete = function(callback, resp) {
	if (resp.responseText == '1') callback();
}

// simple box

ui.component = {}
ui.component.Box = function(domNode) {
	this.domNode  = domNode;
}

ui.component.Box.prototype.setBounds = function(x, y, w, h) {
	this.setPosition(x, y);
	this.setSize(w, h);	
}

ui.component.Box.prototype.setPosition = function(x, y) {
	this.setLeft(x);
	this.setTop(y);
}

ui.component.Box.prototype.setSize = function(w, h) {
	this.setWidth(w);
	this.setHeight(h);
}

ui.component.Box.prototype.setLeft   = function(x) { this.domNode.style.left = x + 'px' }
ui.component.Box.prototype.setTop    = function(y) { this.domNode.style.top = y + 'px'  }
ui.component.Box.prototype.setWidth  = function(w) { this.domNode.style.width = w + 'px'  }
ui.component.Box.prototype.setHeight = function(h) { this.domNode.style.height = h + 'px' }

ui.component.Box.prototype.getLeft   = function() { return parseInt(this.domNode.offsetLeft)   }
ui.component.Box.prototype.getTop    = function() { return parseInt(this.domNode.offsetTop)    }
ui.component.Box.prototype.getWidth  = function() { return std.dom.getDimensions(this.domNode).width  }
ui.component.Box.prototype.getHeight = function() { return std.dom.getDimensions(this.domNode).height }

// composite box

ui.component.CompositeBox = function(boxes) {
	this.boxes = boxes;
}

ui.component.CompositeBox.prototype.setLeft   = function(x) { std.list.iterate(this.boxes, std.lang.bind(this, function(box) { box.setLeft(x)   })) }
ui.component.CompositeBox.prototype.setTop    = function(y) { std.list.iterate(this.boxes, std.lang.bind(this, function(box) { box.setTop(y)    })) }
ui.component.CompositeBox.prototype.setWidth  = function(w) { std.list.iterate(this.boxes, std.lang.bind(this, function(box) { box.setWidth(w)  })) }
ui.component.CompositeBox.prototype.setHeight = function(h) { std.list.iterate(this.boxes, std.lang.bind(this, function(box) { box.setHeight(h) })) }

ui.component.CompositeBox.prototype.getLeft   = function() { return this.boxes[0].getLeft() }
ui.component.CompositeBox.prototype.getTop    = function() { return this.boxes[0].getTop()  }
ui.component.CompositeBox.prototype.getWidth  = function() { return this.boxes[0].getWidth()  }
ui.component.CompositeBox.prototype.getHeight = function() { return this.boxes[0].getHeight() }

// behaviour helpers
// TODO: enable/disable behaviour, disable/uninstall plugins

ui.behaviour = {};
ui.behaviour.BehaviourImpl = function() {}
ui.behaviour.BehaviourImpl.prototype.installPlugin = function(plugin) {
	if (!this._behaviourEvent)
		 this._behaviourEvent = [];
	std.list.iterate(this._behaviourEvent, std.lang.bind(this, function(evt) { std.topic.subscribe(this, evt, std.lang.bind(plugin, plugin.processEvent)) }));
	return this;
}

ui.behaviour.BehaviourImpl.prototype.defineEvent = function(eventType) {
	if (!this._behaviourEvent)
		 this._behaviourEvent = [];
	this._behaviourEvent.push(eventType);
	std.topic.define(this, eventType);
}

ui.behaviour.BehaviourImpl.prototype.fireEvent = function(eventType, ctx, nativeEventObject) {
	ctx.eventType = eventType;
	ctx.cancel = false;
	ctx.evt = nativeEventObject;
	ctx.instance = this;
	std.topic.publish(this, eventType, ctx);
}

// drag

ui.behaviour.drag = {};
ui.behaviour.drag.Draggable = function(domNode) {

	var clz = this._clz = ui.behaviour.drag.Draggable;
	if (!clz._classInited)
		 clz._classInit();

	this._dragData = { state: clz.STATE_INACTIVE };
	this.domNode = domNode;

	this.defineEvent('onDragMouseDown');
	this.defineEvent('onDragBegin');
	this.defineEvent('onBeforeDrag');
	this.defineEvent('onDrag');
	this.defineEvent('onBeforeDragEnd');
	this.defineEvent('onDragEnd');

	std.event.observe(domNode, 'mousedown', std.event.listener(this, this._onmousedown));
}

ui.behaviour.drag.Draggable.prototype = new ui.behaviour.BehaviourImpl();
ui.behaviour.drag.Draggable.STATE_INACTIVE = '';
ui.behaviour.drag.Draggable.STATE_ARMED = 'armed';
ui.behaviour.drag.Draggable.STATE_ACTIVE = 'active';

ui.behaviour.drag.Draggable._classInit = function() {
	var clz = ui.behaviour.drag.Draggable;
	std.event.observe(document, 'mousemove', std.event.listener(clz, clz._classOnmousemove));
	std.event.observe(document, 'mouseup'  , std.event.listener(clz, clz._classOnmouseup));
	clz._classCancelOnmousedown = std.event.listener(clz, function(e) { std.event.vanish(e) });
	clz._classInited = true;
}

ui.behaviour.drag.Draggable._classOnmousemove = function(e) {
	if (this._currentInstance) {
		this._currentInstance._onmousemove(e);
	}
}

ui.behaviour.drag.Draggable._classOnmouseup = function(e) {
	if (this._currentInstance) {
		this._currentInstance._onmouseup(e);
		this._currentInstance = null;

		if (document.releaseCapture)
			document.releaseCapture();
	}
}

ui.behaviour.drag.Draggable.prototype._onmousedown = function(e) {

	if (!std.event.isLeftClick(e)) { return }

	var clz = this._clz;
	var dd = this._dragData;
	if (dd.state == clz.STATE_ACTIVE) { return }

	dd.domNode = this.domNode;
	dd.box = this.box = new ui.component.Box(dd.domNode);
	dd.begin = {
		x: this.box.getLeft(),
		y: this.box.getTop(),
		mouseOffset: {
			x: e.clientX - this.box.getLeft(),
			y: e.clientY - this.box.getTop ()
		}
	};
	this.fireEvent('onDragMouseDown', dd, e);
	if (dd.cancel) { return }
	dd.state = clz.STATE_ARMED;
	clz._currentInstance = this;

	if (dd.domNode.setCapture) {
		dd.domNode.setCapture();
	}
	std.event.observe(document, 'mousedown', clz._classCancelOnmousedown);
}

ui.behaviour.drag.Draggable.prototype._onmousemove = function(e) {
	var clz = this._clz;
	var dd  = this._dragData;
	if (dd.state == clz.STATE_ARMED) {
		this.fireEvent('onDragBegin', dd, e);
		if (dd.cancel) { return }
		dd.state = clz.STATE_ACTIVE;
	}
	if (dd.state == clz.STATE_ACTIVE) {
		dd.mx = e.clientX;
		dd.my = e.clientY;
		dd.x  = dd.mx - dd.begin.mouseOffset.x;
		dd.y  = dd.my - dd.begin.mouseOffset.y;

		this.fireEvent('onBeforeDrag', dd, e);
		dd.box.setPosition(dd.x, dd.y);
		this.fireEvent('onDrag', dd, e);

	}
	std.event.vanish(e);
}

ui.behaviour.drag.Draggable.prototype._onmouseup = function(e) {
	var clz = this._clz;
	var dd = this._dragData;

	if (dd.state == clz.STATE_ACTIVE) {
		this.fireEvent('onBeforeDragEnd', dd, e);
	}
	if (dd.domNode.releaseCapture) {
		dd.domNode.releaseCapture();
	}
	std.event.stopObserving(document, 'mousedown', clz._classCancelOnmousedown);

	if (dd.state == clz.STATE_ACTIVE && !dd.cancel) {
		dd.state = clz.STATE_INACTIVE;
		dd.box.setPosition(dd.x, dd.y);
		this.fireEvent('onDragEnd', dd, e);
	} else {
		dd.state = clz.STATE_INACTIVE;
	}
}

// plugin
// XXX: plugins don't always work well together

ui.plugin = {};

// plugin for optional drag behaviours

ui.plugin.drag = {};
ui.plugin.drag.Axis = function(direction) {
	this._direction = direction || 'x';
}
ui.plugin.drag.Axis.prototype.processEvent = function(dd) {
	switch (dd.eventType) {
		case 'onBeforeDrag':
			if (this._direction == 'x') { dd.y = dd.begin.y } else
			if (this._direction == 'y') { dd.x = dd.begin.x }
			break;
	}
}

ui.plugin.drag.Handle = function(handle) {
	this._handle = std.dom.element(std.lang.evaluate(handle));
	this._handle.style.cursor = 'default';
}
ui.plugin.drag.Handle.prototype.processEvent = function(dd) {
	switch (dd.eventType) {
		case 'onDragMouseDown':
			var node = std.dom.getEventTarget(dd.evt);
			topMost = dd.domNode;
			while (node && node != topMost && node != this._handle) {
				node = node.parentNode;
			}
			if (node != this._handle) {
				dd.cancel = true;
			}
			break;
	}
}

ui.plugin.drag.CheckNode = function(args) {
	this._reject = args.reject;
	this._accept = args.accept;
}
ui.plugin.drag.CheckNode.prototype.processEvent = function(dd) {
	switch (dd.eventType) {
		case 'onDragMouseDown':
			var node = std.dom.getEventTarget(dd.evt);
			topMost = dd.domNode;
			
			var accept = this._accept || {};
			var reject = this._reject || {};

			while (node && node != topMost) {
				if (this._accept && this._accept(node)) { return true }
				if (this._reject && this._reject(node)) { dd.cancel = true; return false }
				node = node.parentNode;
			}
			break;
	}
}

ui.plugin.drag.Helper = function(helper) {
	this._helper = std.dom.element(std.lang.evaluate(helper));
}
ui.plugin.drag.Helper.prototype.processEvent = function(dd) {
	switch (dd.eventType) {
		case 'onDragBegin':
			var boxOld = this._box = dd.box;
			var boxNew = dd.box = new ui.component.Box(this._helper);
			var sty = this._helper.style;

			boxNew.setBounds(boxOld.getLeft(), boxOld.getTop(), boxOld.getWidth(), boxOld.getHeight());
			this._display = sty.display;
			sty.display = 'block';
			break;
		case 'onBeforeDragEnd':
			dd.box = this._box;
			this._helper.style.display = this._display;
			break;
	}
}

ui.plugin.drag.Inside = function(container, args) {
	this._container = std.dom.element(std.lang.evaluate(container));

	args = args || {};
	this._margin = args.margin || 0;
}
ui.plugin.drag.Inside.prototype.processEvent = function(dd) {
	switch (dd.eventType) {
		case 'onDragBegin':
			this._containerBox = new ui.component.Box(this._container);
			break;
		case 'onBeforeDrag':
			var inner = dd.box;
			var outer = this._containerBox;
			var m = this._margin;
			var ix = dd.x, iy = dd.y, iw = inner.getWidth(), ih = inner.getHeight();
			var ox = outer.getLeft(), oy = outer.getTop(), ow = outer.getWidth(), oh = outer.getHeight();
	
			if (m < 0) {
				ox -= iw + m; ow += (iw + m) * 2;
				oy -= ih + m; oh += (ih + m) * 2;
			} else {
				ox -= m; ow += m * 2;
				oy -= m; oh += m * 2;
			}

			if (ix < ox) dd.x = ox;
			if (iy < oy) dd.y = oy;
			if (ix + iw > ox + ow) dd.x = ox + ow - iw;
			if (iy + ih > oy + oh) dd.y = oy + oh - ih;
			break;
	}
}

ui.plugin.drag.Cursor = function(cursor) {
	this._cursorNew = cursor;
}
ui.plugin.drag.Cursor.prototype.processEvent = function(dd) {
	switch (dd.eventType) {
		case 'onDragBegin':
			var sty = dd.domNode.style;
			this._cursorOld = sty.cursor;
			sty.cursor = this._cursorNew;
			break;
		case 'onBeforeDragEnd':
			var sty = dd.domNode.style;
			this._cursorNew = sty.cursor;
			sty.cursor = this._cursorOld;
			break;
	}
}

ui.plugin.drag.Snap = function(distance, args) {
	this._distance = distance;

	var args = args || {};
	this._relX = (args.relative || args.relativeX) ? 1 : 0;
	this._relY = (args.relative || args.relativeY) ? 1 : 0;
}
ui.plugin.drag.Snap.prototype.processEvent = function(dd) {
	switch (dd.eventType) {
		case 'onBeforeDrag':
			var d = this._distance;
			dd.x = dd.x + this._relX * (dd.begin.x % d) - dd.x % d;
			dd.y = dd.y + this._relY * (dd.begin.y % d) - dd.y % d;
			break;
	}
}

ui.plugin.drag.Opacity = function(opacity) {
	this._opacity = opacity;
}
ui.plugin.drag.Opacity.prototype.processEvent = function(dd) {
	switch (dd.eventType) {
		case 'onDragBegin': std.dom.opacity(dd.domNode, 0.3); break;
		case 'onDragEnd'  : std.dom.opacity(dd.domNode, 1);   break;
	}
}

// TODO: quick & dirty stacking order management
//       consider writing a better version

ui.util = {};

ui.util.StackingOrderManager = function (args) {
	args = args || {};
	this._inc    = args.increment || 50; // example use: slipping iframe hacks behind
	this._zIndex = args.zIndex;
	this._list   = [{ id: null, zIndex: this._zIndex }];
}

ui.util.StackingOrderManager.prototype._peek = function () {
	return this._list[this._list.length - 1];
}

ui.util.StackingOrderManager.prototype._push = function (id, zIndex) {
	this._list[this._list.length] = { id: id, zIndex: zIndex };
}

ui.util.StackingOrderManager.prototype.alloc = function (id) {
	var peek = this._peek();
	if (id != peek.id) {
		this.free(id);
		this._push(id, peek.zIndex + this._inc);
	}
	return this._peek().zIndex;
}

ui.util.StackingOrderManager.prototype.free = function (id) {
	var temp = [];
	for (var i = 0; i < this._list.length; i++) {
		if (this._list[i].id != id) {
			temp[temp.length] = this._list[i];
		}
	}
	this._list = temp;
}

