API Docs for: 2.0.0

public/src/SAGE2_Display.js

// SAGE2 is available for use under the SAGE2 Software License
//
// University of Illinois at Chicago's Electronic Visualization Laboratory (EVL)
// and University of Hawai'i at Manoa's Laboratory for Advanced Visualization and
// Applications (LAVA)
//
// See full text, terms and conditions in the LICENSE.txt included file
//
// Copyright (c) 2014-15

/* global ignoreFields, hostAlias, SAGE2WidgetControlInstance */
/* global makeSvgBackgroundForWidgetConnectors, addStyleElementForTitleColor */
/* global removeStyleElementForTitleColor */
/* global clearConnectorColor, moveWidgetToAppConnector */
/* global showWidgetToAppConnectors, getWidgetControlInstanceById */
/* global mapMoveToSlider, getPropertyHandle */
/* global insertTextIntoTextInputWidget, removeWidgetToAppConnector */
/* global hideWidgetToAppConnectors */
/* global createWidgetToAppConnector, getTextFromTextInputWidget */
/* global SAGE2_Partition, require */
/* global SAGE2RemoteSitePointer */


/* global require */

"use strict";

/**
 * SAGE2 Display, client side rendering
 *
 * @module client
 * @submodule SAGE2_Display
 * @class SAGE2_Display
 */

window.URL = (window.URL || window.webkitURL || window.msURL || window.oURL);

var clientID;
var wsio;

var isMaster;
var hostAlias = {};

var itemCount = 20;
var controlItems   = {};
var controlObjects = {};
var lockedControlElements = {};
var widgetConnectorRequestList = {};

var applications = {};
var partitions = {};
var dependencies = {};
var dataSharingPortals = {};

// Maintain the file list available on the server
var storedFileList = null;
var storedFileListEventHandlers = [];

// UI object to build the element on the wall
var ui;
var uiTimer = null;
var uiTimerDelay;

// Global variables for screenshot functionality
var makingScreenshotDialog = null;

// Explicitely close web socket when web browser is closed
window.onbeforeunload = function() {
	if (wsio !== undefined) {
		wsio.close();
	}
};

/**
 * When the page loads, SAGE2 starts
 *
 */
window.addEventListener('load', function(event) {
	SAGE2_init();
});

// Get Browser-Specifc Prefix
function getBrowserPrefix() {
	// Check for the unprefixed property.
	if ('hidden' in document) {
		return null;
	}
	// All the possible prefixes.
	var browserPrefixes = ['moz', 'ms', 'o', 'webkit'];

	for (var i = 0; i < browserPrefixes.length; i++) {
		var prefix = browserPrefixes[i] + 'Hidden';
		if (prefix in document) {
			return browserPrefixes[i];
		}
	}
	// The API is not supported in browser.
	return null;
}

// Get Browser Specific Hidden Property
function hiddenProperty(prefix) {
	if (prefix) {
		return prefix + 'Hidden';
	}
	return 'hidden';
}

// Get Browser Specific Visibility State
function visibilityState(prefix) {
	if (prefix) {
		return prefix + 'VisibilityState';
	}
	return 'visibilityState';
}

// Get Browser Specific Event
function visibilityEvent(prefix) {
	if (prefix) {
		return prefix + 'visibilitychange';
	}
	return 'visibilitychange';
}

/**
 * setupFocusHandlers
 *
 * @method setupFocusHandlers
 */
function setupFocusHandlers() {
	window.addEventListener("focus", function(evt) {
		if (window.__SAGE2__ && __SAGE2__.browser.isMobile) {
			location.reload();
		}
	}, false);
	window.addEventListener("blur", function(evt) {
		if (window.__SAGE2__ && __SAGE2__.browser.isMobile) {
			if (wsio !== undefined) {
				setTimeout(function() {
					wsio.close();
				}, 200);
				document.getElementById('background').style.display = 'none';
			}
		}
	}, false);

	// Get Browser Prefix
	var prefix   = getBrowserPrefix();
	var hidden   = hiddenProperty(prefix);
	// var visState = visibilityState(prefix);
	var visEvent = visibilityEvent(prefix);

	document.addEventListener(visEvent, function(event) {
		if (window.__SAGE2__ && __SAGE2__.browser.isMobile) {
			if (document[hidden]) {
				setTimeout(function() {
					wsio.close();
				}, 200);
				document.getElementById('background').style.display = 'none';
			} else {
				location.reload();
			}
		}
	});

	if (__SAGE2__.browser.isElectron) {
		// Display warning messages from the 'Main' Electron process
		require('electron').ipcRenderer.on('warning', function(event, message) {
			var problemDialog = ui.buildMessageBox('problemDialog', message);
			ui.main.appendChild(problemDialog);
			document.getElementById('problemDialog').style.display = "block";
			// close the warning after 2.5 second
			setTimeout(function() {
				deleteElement('problemDialog');
			}, 2500);
		});

		// Receive hardware info from the main process (electron node)
		require('electron').ipcRenderer.on('hardwareData', function(event, message) {
			if (wsio !== undefined) {
				// and send it to the server
				wsio.emit('displayHardware', message);
			}
		});
		// Receive hardware info from the main process (electron node)
		require('electron').ipcRenderer.on('performanceData', function(event, message) {
			if (wsio !== undefined) {
				// and send it to the server
				wsio.emit('performanceData', message);
			}
		});
	}
}

/**
 * Add a stored file list event handler
 *
 * @method addStoredFileListEventHandler
 */
function addStoredFileListEventHandler(callback) {
	// Register the event handler and call it if we already have a stored file list available
	storedFileListEventHandlers.push(callback);
	if (storedFileList) {
		callback(storedFileList);
	}
}

/**
 * Remove a stored file list event handler
 *
 * @method removeStoredFileListEventHandler
 */
function removeStoredFileListEventHandler(callback) {
	var index = storedFileListEventHandlers.indexOf(callback);
	if (index > -1) {
		storedFileListEventHandlers.splice(index, 1);
	}
}


/**
 * Idle function, show and hide the UI, triggered at uiTimerDelay sec delay
 *
 * @method resetIdle
 */
function resetIdle() {
	if (uiTimer) {
		clearTimeout(uiTimer);
		ui.showInterface();
		uiTimer = setTimeout(function() {
			ui.hideInterface();
		}, uiTimerDelay * 1000);
	}
}

/**
 * Entry point of the application
 *
 * @method SAGE2_init
 */
function SAGE2_init() {
	clientID = parseInt(getParameterByName("clientID")) || 0;
	console.log("clientID: " + clientID);

	wsio = new WebsocketIO();
	console.log("Connected to server: ", window.location.origin);

	// Detect the current browser
	SAGE2_browser();

	// Setup focus events
	setupFocusHandlers();

	isMaster = false;

	wsio.open(function() {
		console.log("Websocket opened");

		setupListeners();

		// Get the cookie for the session, if there's one
		var session = getCookie("session");

		var clientDescription = {
			clientType: "display",
			clientID: clientID,
			requests: {
				config: true,
				version: true,
				time: true,
				console: false
			},
			browser: __SAGE2__.browser,
			session: session
		};
		wsio.emit('addClient', clientDescription);
	});

	// Socket close event (ie server crashed)
	wsio.on('close', function(evt) {
		if (ui) {
			ui.showError();
		}
		var refresh = setInterval(function() {
			// make a dummy request to test the server every 2 sec
			var xhr = new XMLHttpRequest();
			xhr.open("GET", "/", true);
			xhr.onreadystatechange = function() {
				if (xhr.readyState === 4 && xhr.status === 200) {
					console.log("server ready");
					// when server ready, clear the interval callback
					clearInterval(refresh);
					// and reload the page
					window.location.reload();
				}
			};
			xhr.send();
		}, 2000);
	});
}

function setupListeners() {
	wsio.on('initialize', function(data) {
		var startTime  = new Date(data.start);

		// Global initialization
		SAGE2_initialize(startTime);

		// Request list of assets
		wsio.emit('requestStoredFiles');
	});

	wsio.on('setAsMasterDisplay', function() {
		isMaster = true;
	});

	wsio.on('broadcast', function(data) {
		var app = applications[data.app];
		if (app === undefined) {
			// should have better way to determine if app is loaded
			//   or already killed
			setTimeout(function() {
				if (app && app[data.func]) {
					// Send the call to the application
					app.callback(data.func, data.data);
				}
			}, 500);
		} else {
			// Send the call to the application
			app.callback(data.func, data.data);
		}
	});

	wsio.on('addScript', function(script_data) {
		var js = document.createElement('script');
		js.type = "text/javascript";
		js.src = script_data.source;
		document.head.appendChild(js);
	});



	wsio.on('setupDisplayConfiguration', function(json_cfg) {
		var i;
		var http_port;
		var https_port;

		http_port = json_cfg.port === 80 ? "" : ":" + json_cfg.port;
		https_port = json_cfg.secure_port === 443 ? "" : ":" + json_cfg.secure_port;
		hostAlias["http://"  + json_cfg.host + http_port]  = window.location.origin;
		hostAlias["https://" + json_cfg.host + https_port] = window.location.origin;
		for (i = 0; i < json_cfg.alternate_hosts.length; i++) {
			hostAlias["http://"  + json_cfg.alternate_hosts[i] + http_port]  = window.location.origin;
			hostAlias["https://" + json_cfg.alternate_hosts[i] + https_port] = window.location.origin;
		}

		// Build the elements visible on the wall
		ui = new UIBuilder(json_cfg, clientID);
		ui.build();
		ui.background();
		if (json_cfg.ui.auto_hide_ui) {
			// default delay is 30s if not specified
			uiTimerDelay = json_cfg.ui.auto_hide_delay ? parseInt(json_cfg.ui.auto_hide_delay, 10) : 30;
			uiTimer = setTimeout(function() {
				ui.hideInterface();
			}, uiTimerDelay * 1000);
		}
		makeSvgBackgroundForWidgetConnectors(ui.main.style.width, ui.main.style.height);
	});

	wsio.on('hideui', function(param) {
		if (param) {
			clearTimeout(uiTimer);
			ui.showInterface();
			uiTimerDelay = param.delay;
			uiTimer = setTimeout(function() {
				ui.hideInterface();
			}, uiTimerDelay * 1000);
		} else {
			if (ui.uiHidden === true) {
				clearTimeout(uiTimer);
				uiTimer = null;
				ui.showInterface();
			} else {
				ui.hideInterface();
			}
		}
	});

	wsio.on('setupSAGE2Version', function(version) {
		ui.updateVersionText(version);
	});

	wsio.on('setSystemTime', function(data) {
		var m = moment(data.date);
		var local = new Date();
		var offset = local.getTimezoneOffset() - data.offset;
		m.add(offset, 'minutes');
		ui.setTime(m);
	});

	wsio.on('addRemoteSite', function(data) {
		ui.addRemoteSite(data);
	});

	wsio.on('toggleHelp', function(data) {
		ui.toggleHelp();
	});

	wsio.on('connectedToRemoteSite', function(data) {
		if (window.ui) {
			ui.connectedToRemoteSite(data);
		} else {
			setTimeout(function() {
				ui.connectedToRemoteSite(data);
			}, 1000);
		}
	});

	wsio.on('drawingInit', function(data) {
		ui.drawingInit(data);
	});

	wsio.on('drawingUpdate', function(data) {
		ui.updateObject(data);
	});

	wsio.on('drawingRemove', function(data) {
		ui.removeObject(data);
	});

	wsio.on('createSagePointer', function(pointer_data) {
		if (window.ui) {
			ui.createSagePointer(pointer_data);
		} else {
			setTimeout(function() {
				ui.createSagePointer(pointer_data);
			}, 1000);
		}
	});

	wsio.on('showSagePointer', function(pointer_data) {
		ui.showSagePointer(pointer_data);
		resetIdle();
		var uniqueID = pointer_data.id.slice(0, pointer_data.id.lastIndexOf("_"));
		var re = /\.|:/g;
		var stlyeCaption = uniqueID.split(re).join("");
		addStyleElementForTitleColor(stlyeCaption, pointer_data.color);
	});

	wsio.on('hideSagePointer', function(pointer_data) {
		SAGE2RemoteSitePointer.notifyAppsPointerIsHidden(pointer_data);
		ui.hideSagePointer(pointer_data);
		var uniqueID = pointer_data.id.slice(0, pointer_data.id.lastIndexOf("_"));
		var re = /\.|:/g;
		var stlyeCaption = uniqueID.split(re).join("");
		removeStyleElementForTitleColor(stlyeCaption, pointer_data.color);
	});

	wsio.on('updateSagePointerPosition', function(pointer_data) {
		if (ui) {
			ui.updateSagePointerPosition(pointer_data);
		}
		resetIdle();
	});

	wsio.on('changeSagePointerMode', function(pointer_data) {
		ui.changeSagePointerMode(pointer_data);
		resetIdle();
	});

	wsio.on('createRadialMenu', function(menu_data) {
		ui.createRadialMenu(menu_data);
	});

	wsio.on('updateRadialMenu', function(menu_data) {
		ui.updateRadialMenu(menu_data);
	});

	wsio.on('updateRadialMenuPosition', function(menu_data) {
		ui.updateRadialMenuPosition(menu_data);
	});

	wsio.on('radialMenuEvent', function(menu_data) {
		ui.radialMenuEvent(menu_data);
		resetIdle();
	});

	wsio.on('updateRadialMenuDocs', function(menu_data) {
		ui.updateRadialMenuDocs(menu_data);
		resetIdle();
	});

	wsio.on('updateRadialMenuApps', function(menu_data) {
		ui.updateRadialMenuApps(menu_data);
		resetIdle();
	});

	wsio.on('loadApplicationState', function(data) {
		var app = applications[data.id];
		if (app !== undefined && app !== null) {
			app.SAGE2Load(data.state, new Date(data.date));
		}
	});

	wsio.on('loadApplicationOptions', function(data) {
		var fullSync = true;
		var windowTitle = document.getElementById(data.id + "_title");
		var windowIconSync = document.getElementById(data.id + "_iconSync");
		var windowIconUnSync = document.getElementById(data.id + "_iconUnSync");
		var app = applications[data.id];
		if (app !== undefined && app !== null) {
			app.SAGE2LoadOptions(data.options);

			if (fullSync === true) {
				if (data.options[Object.keys(data.options)[0]]._sync === true) {
					windowTitle.style.backgroundColor = "#39C4A6";
					windowIconSync.style.display = "block";
					windowIconUnSync.style.display = "none";
				} else {
					windowTitle.style.backgroundColor = "#666666";
					windowIconSync.style.display = "none";
					windowIconUnSync.style.display = "block";
				}
			}
		}
	});

	wsio.on('storedFileList', function(data) {
		// Save the cached stored file list and update all the listeners
		storedFileList = data;
		for (var i = 0; i < storedFileListEventHandlers.length; i++) {
			storedFileListEventHandlers[i](storedFileList);
		}
	});

	wsio.on('updateMediaStreamFrame', function(data) {
		wsio.emit('receivedMediaStreamFrame', {id: data.id});

		var app = applications[data.id];
		if (app !== undefined && app !== null) {
			app.SAGE2Load(data.state, new Date(data.date));
		}

		// update clones in data-sharing portals
		var key;
		for (key in dataSharingPortals) {
			app = applications[data.id + "_" + key];
			if (app !== undefined && app !== null) {
				app.SAGE2Load(data.state, new Date(data.date));
			}
		}
	});

	wsio.on('updateMediaBlockStreamFrame', function(data) {
		var appId     = byteBufferToString(data);
		var blockIdx  = byteBufferToInt(data.subarray(appId.length + 1, appId.length + 3));
		var date      = byteBufferToInt(data.subarray(appId.length + 3, appId.length + 11));
		var yuvBuffer = data.subarray(appId.length + 11, data.length);

		if (applications[appId] !== undefined && applications[appId] !== null) {
			applications[appId].textureData(blockIdx, yuvBuffer);
			if (applications[appId].receivedBlocks.every(isTrue) === true) {
				applications[appId].refresh(new Date(date));
				applications[appId].setValidBlocksFalse();
				wsio.emit('receivedMediaBlockStreamFrame', {id: appId});
			}
		}
	});

	wsio.on('updateVideoFrame', function(data) {
		var appId     = byteBufferToString(data);
		var blockIdx  = byteBufferToInt(data.subarray(appId.length + 1, appId.length + 3));
		var date      = byteBufferToInt(data.subarray(appId.length + 7, appId.length + 15));
		var yuvBuffer = data.subarray(appId.length + 15, data.length);

		if (applications[appId] !== undefined && applications[appId] !== null) {
			applications[appId].textureData(blockIdx, yuvBuffer);
			if (applications[appId].receivedBlocks.every(isTrue) === true) {
				applications[appId].refresh(new Date(date));
				applications[appId].setValidBlocksFalse();
				wsio.emit('requestVideoFrame', {id: appId});
			}
		}
	});

	wsio.on('updateFrameIndex', function(data) {
		var app = applications[data.id];
		if (app !== undefined && app !== null) {
			app.setVideoFrame(data.frameIdx);
		}
	});

	wsio.on('videoEnded', function(data) {
		var app = applications[data.id];
		if (app !== undefined && app !== null) {
			app.videoEnded();
		}
	});

	wsio.on('updateValidStreamBlocks', function(data) {
		if (applications[data.id] !== undefined && applications[data.id] !== null) {
			applications[data.id].validBlocks = data.blockList;
			applications[data.id].setValidBlocksFalse();
		}
	});

	wsio.on('updateWebpageStreamFrame', function(data) {
		wsio.emit('receivedWebpageStreamFrame', {id: data.id, client: clientID});

		var webpage = document.getElementById(data.id + "_webpage");
		webpage.src = "data:image/jpeg;base64," + data.src;
	});

	wsio.on('createAppWindow', function(data) {
		createAppWindow(data, ui.main.id, ui.titleBarHeight, ui.titleTextSize, ui.offsetX, ui.offsetY);
	});


	/* Partition wsio calls */
	wsio.on('createPartitionWindow', function(data) {
		partitions[data.id] = new SAGE2_Partition(data);
	});
	wsio.on('deletePartitionWindow', function(data) {
		partitions[data.id].deletePartition();
		delete partitions[data.id];
	});
	wsio.on('partitionMoveAndResizeFinished', function(data) {
		partitions[data.id].updatePositionAndSize(data);
	});
	wsio.on('partitionWindowTitleUpdate', function(data) {
		partitions[data.id].updateTitle(data.title);
	});
	wsio.on('updatePartitionBorders', function(data) {
		if (data && partitions.hasOwnProperty(data.id)) {
			partitions[data.id].updateSelected(data.highlight);
		} else {
			for (var p in partitions) {
				// console.log(p);
				partitions[p].updateSelected(false);
			}
		}
	});
	wsio.on('updatePartitionColor', function(data) {
		if (data && partitions.hasOwnProperty(data.id)) {
			partitions[data.id].updateColor(data.color);
		}
	});
	wsio.on('updatePartitionSnapping', function(data) {
		if (data && partitions.hasOwnProperty(data.id)) {
			partitions[data.id].setSnappedBorders(data.snapping);
			partitions[data.id].setAnchoredBorders(data.anchor);
			partitions[data.id].updateBorders();
		}
	});

	wsio.on('createAppWindowInDataSharingPortal', function(data) {
		var portal = dataSharingPortals[data.portal];

		createAppWindow(data.application, portal.id, portal.titleBarHeight, portal.titleTextSize, 0, 0);
	});

	wsio.on('deleteElement', function(elem_data) {
		resetIdle();

		// Tell the application it is over
		var app = applications[elem_data.elemId];
		if (app) {
			app.terminate();
		}

		// Remove the app from the list
		delete applications[elem_data.elemId];

		// Clean up the DOM
		var deleteElemTitle = document.getElementById(elem_data.elemId + "_title");
		var deleteElem = document.getElementById(elem_data.elemId);

		// Set the CSS for fading out
		deleteElem.classList.add('windowDisappear');

		// Delete the titlebar
		deleteElemTitle.parentNode.removeChild(deleteElemTitle);

		// When fade over, really delete the element
		setTimeout(function() {
			deleteElem.parentNode.removeChild(deleteElem);
		}, 400);

		// Clean up the UI DOM
		if (elem_data.elemId in controlObjects) {
			for (var item in controlItems) {
				if (item.indexOf(elem_data.elemId) > -1) {
					controlItems[item].divHandle.parentNode.removeChild(controlItems[item].divHandle);
					removeWidgetToAppConnector(item);
					delete controlItems[item];
				}

			}
			delete controlObjects[elem_data.elemId];
		}
	});

	wsio.on('hideControl', function(ctrl_data) {
		if (ctrl_data.id in controlItems && controlItems[ctrl_data.id].show === true) {
			controlItems[ctrl_data.id].divHandle.style.display = "none";
			controlItems[ctrl_data.id].show = false;
			clearConnectorColor(ctrl_data.id, ctrl_data.appId);
		}
	});

	wsio.on('showControl', function(ctrl_data) {
		if (ctrl_data.id in controlItems && controlItems[ctrl_data.id].show === false) {
			controlItems[ctrl_data.id].divHandle.style.display = "block";
			controlItems[ctrl_data.id].show = true;
		}
	});

	wsio.on('updateItemOrder', function(order) {
		resetIdle();
		var key;
		for (key in order) {
			var selectedElemTitle = document.getElementById(key + "_title");
			var selectedElem = document.getElementById(key);
			var selectedElemOverlay = document.getElementById(key + "_overlay");

			if (selectedElemTitle) {
				selectedElemTitle.style.zIndex = order[key].toString();
			}
			if (selectedElem) {
				selectedElem.style.zIndex = order[key].toString();
			}
			if (selectedElemOverlay) {
				selectedElemOverlay.style.zIndex = order[key].toString();
			}
		}
	});

	wsio.on('hoverOverItemCorner', function(elem_data) {
		var selectedElem = document.getElementById(elem_data.elemId);
		if (selectedElem) {
			var dragCorner   = selectedElem.getElementsByClassName("dragCorner");
			if (elem_data.flag) {
				dragCorner[0].style.backgroundColor = "rgba(255,255,255,0.7)";
				dragCorner[0].style.border = "2px solid #333333";
			} else {
				dragCorner[0].style.backgroundColor = "rgba(255,255,255,0.0)";
				dragCorner[0].style.border = "none";
			}
		}
	});

	wsio.on('setItemPosition', function(position_data) {
		resetIdle();

		if (position_data.elemId.split("_")[0] === "portal") {
			dataSharingPortals[position_data.elemId].setPosition(position_data.elemLeft, position_data.elemTop);
			return;
		}

		if (position_data.elemAnimate) {
			moveItemWithAnimation(position_data);
		} else {
			var translate = "translate(" + position_data.elemLeft + "px," + position_data.elemTop + "px)";
			var selectedElemTitle = document.getElementById(position_data.elemId + "_title");
			selectedElemTitle.style.webkitTransform = translate;
			selectedElemTitle.style.mozTransform    = translate;
			selectedElemTitle.style.transform       = translate;

			var selectedElem = document.getElementById(position_data.elemId);
			selectedElem.style.webkitTransform = translate;
			selectedElem.style.mozTransform    = translate;
			selectedElem.style.transform       = translate;
		}


		var app = applications[position_data.elemId];
		if (app !== undefined) {
			var parentTransform = getTransform(selectedElem.parentNode);
			var border = parseInt(selectedElem.parentNode.style.borderWidth || 0, 10);
			app.sage2_x = (position_data.elemLeft + border + 1) * parentTransform.scale.x + parentTransform.translate.x;
			app.sage2_x = Math.round(app.sage2_x);
			app.sage2_y = (position_data.elemTop + ui.titleBarHeight + border) * parentTransform.scale.y
				+ parentTransform.translate.y;
			app.sage2_y = Math.round(app.sage2_y);
			app.sage2_width  = parseInt(position_data.elemWidth, 10) * parentTransform.scale.x;
			app.sage2_height = parseInt(position_data.elemHeight, 10) * parentTransform.scale.y;

			var date  = new Date(position_data.date);
			if (position_data.force || app.moveEvents === "continuous") {
				app.move(date);
			}
		}
		if (position_data.elemId in controlObjects) {
			var hOffset = (ui.titleBarHeight + position_data.elemHeight) / 2;
			for (var item in controlItems) {
				if (controlItems.hasOwnProperty(item) && item.indexOf(position_data.elemId) > -1 && controlItems[item].show) {
					var control = controlItems[item].divHandle;
					var cLeft = parseInt(control.style.left);
					var cTop = parseInt(control.style.top);
					var cHeight = parseInt(control.style.height);
					moveWidgetToAppConnector(item, cLeft + cHeight / 2.0, cTop + cHeight / 2.0,
						position_data.elemLeft - ui.offsetX + position_data.elemWidth / 2.0,
						position_data.elemTop - ui.offsetY + hOffset, cHeight / 2.4);
				}
			}
		}

	});

	wsio.on('setControlPosition', function(position_data) {
		var eLeft = position_data.elemLeft - ui.offsetX;
		var eTop = position_data.elemTop - ui.offsetY;
		var selectedControl = document.getElementById(position_data.elemId);
		var appData = position_data.appData;
		if (selectedControl !== undefined && selectedControl !== null) {
			selectedControl.style.left = eLeft.toString() + "px";
			selectedControl.style.top = eTop.toString() + "px";
			var hOffset = (ui.titleBarHeight + appData.height) / 2;
			moveWidgetToAppConnector(position_data.elemId,
				eLeft + position_data.elemHeight / 2.0,
				eTop + position_data.elemHeight / 2.0,
				appData.left - ui.offsetX + appData.width / 2.0,
				appData.top - ui.offsetY + hOffset,
				position_data.elemHeight / 2.4);
		} else {
			console.log("cannot find control: " + position_data.elemId);
		}
	});

	wsio.on('showWidgetToAppConnector', function(data) {
		showWidgetToAppConnectors(data);
		if (data.user_color !== null) {
			if (!(data.id in widgetConnectorRequestList)) {
				widgetConnectorRequestList[data.id] = [];
			}
			widgetConnectorRequestList[data.id].push(data);
		}
	});


	wsio.on('hideWidgetToAppConnector', function(control_data) {
		if (control_data.id in widgetConnectorRequestList) {
			var lst = widgetConnectorRequestList[control_data.id];
			if (lst.length > 1) {
				var len = lst.length;
				for (var i = len - 1; i >= 0; i--) {
					if (control_data.user_id === lst[i].user_id) {
						lst.splice(i, 1);
						showWidgetToAppConnectors(lst[len - 2]);
						break;
					}
				}
			} else if (lst.length === 1) {
				delete widgetConnectorRequestList[control_data.id];
				hideWidgetToAppConnectors(control_data.id);
			}
		}

	});

	wsio.on('setItemPositionAndSize', function(position_data) {
		resetIdle();

		if (position_data.elemId.split("_")[0] === "portal") {
			dataSharingPortals[position_data.elemId].setPositionAndSize(position_data.elemLeft,
				position_data.elemTop, position_data.elemWidth, position_data.elemHeight);
			return;
		}
		var selectedElem = document.getElementById(position_data.elemId);
		var child        = selectedElem.getElementsByClassName("sageItem");
		// If application not ready, return
		if (child.length < 1) {
			return;
		}

		var translate = "translate(" + position_data.elemLeft + "px," + position_data.elemTop + "px)";
		var selectedElemTitle = document.getElementById(position_data.elemId + "_title");
		selectedElemTitle.style.width = Math.round(position_data.elemWidth).toString() + "px";

		if (position_data.elemId.split("_")[0] === "portal") {
			dataSharingPortals[position_data.elemId].setPosition(position_data.elemLeft, position_data.elemTop);
			return;
		}

		if (position_data.elemAnimate) {
			moveItemWithAnimation(position_data);
		} else {
			selectedElemTitle.style.webkitTransform = translate;
			selectedElemTitle.style.mozTransform    = translate;
			selectedElemTitle.style.transform       = translate;

			selectedElem.style.webkitTransform = translate;
			selectedElem.style.mozTransform    = translate;
			selectedElem.style.transform       = translate;

		}

		selectedElemTitle.style.width = Math.round(position_data.elemWidth).toString() + "px";

		var selectedElemState = document.getElementById(position_data.elemId + "_state");
		selectedElemState.style.width = Math.round(position_data.elemWidth).toString() + "px";
		selectedElemState.style.height = Math.round(position_data.elemHeight).toString() + "px";

		var dragCorner = selectedElem.getElementsByClassName("dragCorner");
		var cornerSize = Math.min(position_data.elemWidth, position_data.elemHeight) / 5;
		dragCorner[0].style.width  = cornerSize.toString() + "px";
		dragCorner[0].style.height = cornerSize.toString() + "px";
		dragCorner[0].style.top    = (Math.round(position_data.elemHeight) - cornerSize).toString() + "px";
		dragCorner[0].style.left   = (Math.round(position_data.elemWidth) - cornerSize).toString()  + "px";

		// if the element is a div or iframe, resize should use the style object
		if (child[0].tagName.toLowerCase() === "div" ||
			child[0].tagName.toLowerCase() === "iframe" ||
			child[0].tagName.toLowerCase() === "webview") {
			child[0].style.width  = Math.round(position_data.elemWidth)  + "px";
			child[0].style.height = Math.round(position_data.elemHeight) + "px";
		} else {
			// if it's a canvas or else, just use width and height
			child[0].width  = Math.round(position_data.elemWidth);
			child[0].height = Math.round(position_data.elemHeight);
		}

		var app = applications[position_data.elemId];
		if (app !== undefined) {
			var parentTransform = getTransform(selectedElem.parentNode);
			var border = parseInt(selectedElem.parentNode.style.borderWidth || 0, 10);
			app.sage2_x = (position_data.elemLeft + border + 1) * parentTransform.scale.x + parentTransform.translate.x;
			app.sage2_x = Math.round(app.sage2_x);
			app.sage2_y = (position_data.elemTop + ui.titleBarHeight + border) * parentTransform.scale.y
				+ parentTransform.translate.y;
			app.sage2_y = Math.round(app.sage2_y);
			app.sage2_width  = parseInt(position_data.elemWidth, 10) * parentTransform.scale.x;
			app.sage2_height = parseInt(position_data.elemHeight, 10) * parentTransform.scale.y;

			var date = new Date(position_data.date);
			if (position_data.force || app.resizeEvents === "continuous") {
				if (app.resize) {
					app.resize(date);
				}
			}
			if (position_data.force || app.moveEvents === "continuous") {
				if (app.move) {
					app.move(date);
				}
			}
		}
		if (position_data.elemId in controlObjects) {
			var hOffset = (ui.titleBarHeight + position_data.elemHeight) / 2;
			for (var item in controlItems) {
				if (controlItems.hasOwnProperty(item) && item.indexOf(position_data.elemId) > -1 && controlItems[item].show) {
					var control = controlItems[item].divHandle;
					var cLeft = parseInt(control.style.left);
					var cTop = parseInt(control.style.top);
					var cHeight = parseInt(control.style.height);
					moveWidgetToAppConnector(item, cLeft + cHeight / 2.0,
						cTop + cHeight / 2.0,
						position_data.elemLeft - ui.offsetX + position_data.elemWidth / 2.0,
						position_data.elemTop - ui.offsetY + hOffset,
						cHeight / 2.4);
				}
			}
		}
	});

	wsio.on('startMove', function(data) {
		resetIdle();

		var app = applications[data.id];
		if (app !== undefined && app.moveEvents === "onfinish") {
			var date = new Date(data.date);
			if (app.startMove) {
				app.startMove(date);
			}
		}
	});

	wsio.on('finishedMove', function(data) {
		resetIdle();

		var app = applications[data.id];
		if (app !== undefined && app.moveEvents === "onfinish") {
			var date = new Date(data.date);
			if (app.move) {
				app.move(date);
			}
		}
		SAGE2RemoteSitePointer.checkIfAppNeedsUpdate(app);
	});

	wsio.on('startResize', function(data) {
		resetIdle();

		var app = applications[data.id];
		if (app !== undefined && app.resizeEvents === "onfinish") {
			var date = new Date(data.date);
			if (app.startResize) {
				app.startResize(date);
			}
		}
	});

	wsio.on('finishedResize', function(data) {
		resetIdle();
		var app = applications[data.id];
		if (app !== undefined && app.resizeEvents === "onfinish") {
			var date = new Date(data.date);
			if (app.resize) {
				app.resize(date);
			}
		}
		SAGE2RemoteSitePointer.checkIfAppNeedsUpdate(app);
	});

	wsio.on('animateCanvas', function(data) {
		var app = applications[data.id];
		if (app !== undefined && app !== null) {
			var date = new Date(data.date);
			app.refresh(date);
			wsio.emit('finishedRenderingAppFrame', {id: data.id, fps: app.maxFPS});
		}
	});

	wsio.on('eventInItem', function(event_data) {
		var app = applications[event_data.id];

		// console.log(event_data, app, applications);

		if (app) {
			var date = new Date(event_data.date);
			app.SAGE2Event(event_data.type, event_data.position, event_data.user, event_data.data, date);
		}
	});

	wsio.on('requestNewControl', function(data) {
		var dt = new Date(data.date);
		if (data.elemId !== undefined && data.elemId !== null) {
			if (controlObjects[data.elemId] !== undefined) {

				var spec = controlObjects[data.elemId].controls;
				if (spec.controlsReady() === true) {
					var size = spec.computeSize();
					wsio.emit('addNewControl', {
						id: data.elemId + data.user_id + "_controls",
						appId: data.elemId,
						left: data.x - size.height / 2,
						top: data.y - size.height / 2,
						width: size.width,
						height: size.height,
						barHeight: size.barHeight,
						hasSideBar: size.hasSideBar,
						show: true,
						date: dt
					});
				}

			}
		}
	});

	wsio.on('createControl', function(data) {
		if (controlItems[data.id] === null || controlItems[data.id] === undefined) {
			var ctrDiv =  document.createElement("div");
			ctrDiv.id = data.id;
			ctrDiv.className = "windowControls";
			ctrDiv.style.width = data.width.toString() + "px";
			ctrDiv.style.fill = "rgba(0,0,0,0.0)";
			ctrDiv.style.height = data.height.toString() + "px";
			ctrDiv.style.left = (data.left - ui.offsetX).toString() + "px";
			ctrDiv.style.top = (data.top - ui.offsetY).toString() + "px";
			ctrDiv.style.zIndex = "9990".toString();
			ctrDiv.style.display = data.show ? "block" : "none";
			if (ui.noDropShadow === true) {
				ctrDiv.style.boxShadow = "none";
			}

			var spec = controlObjects[data.appId].controls;
			if (spec.controlsReady() === true) {
				var handle = new SAGE2WidgetControlInstance(data.id, spec);
				ctrDiv.appendChild(handle);
				ui.main.appendChild(ctrDiv);
				controlItems[data.id] = {show: data.show, divHandle: ctrDiv};
				createWidgetToAppConnector(data.id);
			}

		}
	});
	wsio.on('removeControlsForUser', function(data) {
		for (var idx in controlItems) {
			if (idx.indexOf(data.user_id) > -1) {
				controlItems[idx].divHandle.parentNode.removeChild(controlItems[idx].divHandle);
				removeWidgetToAppConnector(idx);
				delete controlItems[idx];
			}
		}
	});

	wsio.on('executeControlFunction', function(data) {
		var ctrl = getWidgetControlInstanceById(data.ctrl);
		if (ctrl) {
			var ctrlId = ctrl.attr('id');
			var action = "buttonPress";
			var ctrlParent = ctrl.parent();
			var radioButtonSelected = null;
			if (/button/.test(ctrlId) === true && /radio/.test(ctrlId) === false) {
				ctrl = ctrlParent.select("svg");
				var animationInfo = ctrlParent.data("animationInfo");
				var state = animationInfo.state;
				if (ctrl !== null && ctrl !== undefined) {
					if (state === null || state === undefined) {
						ctrl = ctrlParent.select("circle") || ctrlParent.select("polygon");
						if (ctrl !== null && ctrl !== undefined) {
							var fillVal = ctrl.attr("fill");
							ctrl.animate({fill: "rgba(230,230,230,1.0)"}, 400, mina.bounce, function() {
								ctrl.animate({fill: fillVal}, 400, mina.bounce);
							});
						}
					}
				} else {
					ctrl = ctrlParent.select("path") || ctrlParent.select("text");
					if (animationInfo.textual === false && animationInfo.animation === true) {
						var delay = animationInfo.delay;
						var fromPath = animationInfo.from;
						var toPath = animationInfo.to;
						var fromFill = animationInfo.fill;
						var toFill = animationInfo.toFill;
						if (toFill === null || toFill === undefined) {
							toFill = fromFill;
						}
						if (state === null) {
							ctrl.animate({path: toPath, fill: toFill}, delay, mina.bounce, function() {
								ctrl.animate({path: fromPath, fill: fromFill}, delay, mina.bounce);
							});

						} else {
							animationInfo.state = 1 - animationInfo.state;
							ctrl.data("animationInfo", animationInfo);
							// ctrl.animate({"path":path, "fill":fill}, delay, mina.bounce);
						}
					}
				}
				ctrlId = ctrlParent.attr("id").replace("button", "");
			} else if (/radio/.test(ctrlId) === true) {
				var radioButtonId =  ctrlParent.attr("id");
				var radioState = ctrlParent.data("radioState");
				radioButtonSelected = ctrlId.replace(radioButtonId, "");
				radioState.value = radioButtonSelected;
				ctrlParent.data("radioState", radioState);
				action = "radioButtonPress";
				ctrlId = radioButtonId.replace("button_radio", "");
			} else {
				ctrlId = ctrlParent.attr("id").replace("slider", "");
				action = "sliderRelease";
			}

			var appId = data.ctrl.appId;
			var app   = applications[appId];
			switch (ctrlId) {
				case "CloseApp":
					if (isMaster) {
						wsio.emit('closeAppFromControl', {appId: appId});
					}
					break;
				case "CloseWidget":
					if (isMaster) {
						wsio.emit('hideWidgetFromControl', {instanceID: data.ctrl.instanceID});
					}
					break;
				case "ShareApp":
					break;
				default:
					var widgetEventData = {identifier: ctrlId, action: action};
					if (radioButtonSelected !== null) {
						widgetEventData.value = radioButtonSelected;
					}
					app.SAGE2Event("widgetEvent", null, data.user, widgetEventData, new Date(data.date));
					break;
			}

			// Check whether a request for clone was made.
			if (app.cloneable === true && app.requestForClone === true) {
				app.requestForClone = false;
				if (isMaster) {
					wsio.emit('createAppClone', {id: appId, cloneData: app.state});
				}
			}

		}

	});

	wsio.on('sliderKnobLockAction', function(data) {
		var ctrl   = getWidgetControlInstanceById(data.ctrl);
		var slider = ctrl.parent();
		var appId = data.ctrl.appId;
		var app = applications[appId];
		var ctrlId = slider.attr("id").replace("slider", "");
		app.SAGE2Event("widgetEvent", null, data.user, {identifier: ctrlId, action: "sliderLock"}, new Date(data.date));
		var ctrHandle    = document.getElementById(slider.data("instanceID"));
		var widgetOffset = ctrHandle ? parseInt(ctrHandle.style.left) : 0;
		var pos = data.x - ui.offsetX - widgetOffset;
		var sliderKnob = slider.select("rect");
		var knobWidthHalf = parseInt(sliderKnob.attr("width")) / 2;
		var knobCenterX   = parseInt(sliderKnob.attr("x")) + knobWidthHalf;
		if (Math.abs(pos - knobCenterX) > knobWidthHalf) {
			var updatedSliderInfo = mapMoveToSlider(sliderKnob, pos);
			var appObj = getPropertyHandle(applications[slider.data("appId")], slider.data("appProperty"));
			appObj.handle[appObj.property] = updatedSliderInfo.sliderValue;
			app.SAGE2Event("widgetEvent", null, data.user, {identifier: ctrlId, action: "sliderUpdate"}, new Date(data.date));
		}
	});

	wsio.on('moveSliderKnob', function(data) {
		// TODO: add `date` to `data` object
		//       DON'T USE `new Date()` CLIENT SIDE (apps will get out of sync)
		var ctrl = getWidgetControlInstanceById(data.ctrl);
		var slider = ctrl.parent();
		var ctrHandle = document.getElementById(slider.data("instanceID"));
		var widgetOffset = ctrHandle ? parseInt(ctrHandle.style.left) : 0;
		var pos = data.x - ui.offsetX - widgetOffset;
		var sliderKnob = slider.select("rect");
		var updatedSliderInfo = mapMoveToSlider(sliderKnob, pos);
		var appObj = getPropertyHandle(applications[slider.data("appId")], slider.data("appProperty"));
		appObj.handle[appObj.property] = updatedSliderInfo.sliderValue;
		var appId  = data.ctrl.appId;
		var app    = applications[appId];
		var ctrlId = slider.attr("id").replace("slider", "");
		app.SAGE2Event("widgetEvent", null, data.user, {identifier: ctrlId, action: "sliderUpdate"}, new Date(data.date));
	});

	wsio.on('keyInTextInputWidget', function(data) {
		// TODO: add `date` to `data` object
		//       DON'T USE `new Date()` CLIENT SIDE (apps will get out of sync)

		var ctrl = getWidgetControlInstanceById(data);
		if (ctrl) {
			var textInput = ctrl.parent();
			if (data.code !== 13) {
				insertTextIntoTextInputWidget(textInput, data.code, data.printable);
			} else {
				var ctrlId = textInput.attr("id").replace("textInput", "");
				var blinkControlHandle = textInput.data("blinkControlHandle");
				clearInterval(blinkControlHandle);
				var app = applications[data.appId];
				app.SAGE2Event("widgetEvent", null, data.user,
					{identifier: ctrlId, action: "textEnter", text: getTextFromTextInputWidget(textInput)}, new Date(data.date));
			}
		}
	});

	wsio.on('activateTextInputControl', function(data) {
		var ctrl = null;
		if (data.prevTextInput) {
			ctrl = getWidgetControlInstanceById(data.prevTextInput);
		}
		var textInput, blinkControlHandle;
		if (ctrl) {
			textInput = ctrl.parent();
			blinkControlHandle = textInput.data("blinkControlHandle");
			clearInterval(blinkControlHandle);
		}
		ctrl = getWidgetControlInstanceById(data.curTextInput);
		if (ctrl) {
			textInput = ctrl.parent();
			blinkControlHandle = setInterval(textInput.data("blinkCallback"), 1000);
			textInput.data("blinkControlHandle", blinkControlHandle);
		}
	});

	// Called when the user clicks outside the widget control while a lock exists on text input
	wsio.on('deactivateTextInputControl', function(data) {
		var ctrl = getWidgetControlInstanceById(data);
		if (ctrl) {
			var textInput = ctrl.parent();
			var blinkControlHandle = textInput.data("blinkControlHandle");
			clearInterval(blinkControlHandle);
		}
	});

	wsio.on('requestedDataSharingSession', function(data) {
		ui.showDataSharingRequestDialog(data);
	});

	wsio.on('closeRequestDataSharingDialog', function(data) {
		ui.hideDataSharingRequestDialog();
	});

	wsio.on('dataSharingConnectionWait', function(data) {
		ui.showDataSharingWaitingDialog(data);
	});

	wsio.on('closeDataSharingWaitDialog', function(data) {
		ui.hideDataSharingWaitingDialog();
	});

	wsio.on('initializeDataSharingSession', function(data) {
		dataSharingPortals[data.id] = new DataSharing(data);
	});

	wsio.on('setTitle', function(data) {
		if (data.id !== null && data.id !== undefined) {
			var titleDiv = document.getElementById(data.id + "_title");
			var pElement = titleDiv.getElementsByTagName("p");
			pElement[0].textContent = data.title;
		}
	});

	wsio.on('setAppSharingFlag', function(data) {
		var windowTitle = document.getElementById(data.id + "_title");
		var windowIconSync = document.getElementById(data.id + "_iconSync");

		if (data.sharing === true) {
			windowTitle.style.backgroundColor = "#39C4A6";
			windowIconSync.style.display = "block";
		} else {
			windowTitle.style.backgroundColor = "#666666";
			windowIconSync.display = "none";
		}
	});

	wsio.on('toggleSyncOptions', function(data) {
		var fullSync = true;
		var key;
		var windowTitle = document.getElementById(data.id + "_title");
		var windowIconSync = document.getElementById(data.id + "_iconSync");
		var windowIconUnSync = document.getElementById(data.id + "_iconUnSync");
		var windowState = document.getElementById(data.id + "_state");
		if (fullSync === true) {
			if (windowIconSync.style.display === "block") {
				windowTitle.style.backgroundColor = "#666666";
				windowIconSync.style.display = "none";
				windowIconUnSync.style.display = "block";

				for (key in applications[data.id].SAGE2StateOptions) {
					applications[data.id].SAGE2StateSyncChildren(applications[data.id].SAGE2StateOptions[key]._name,
						applications[data.id].SAGE2StateOptions, false);
				}
			} else {
				windowTitle.style.backgroundColor = "#39C4A6";
				windowIconSync.style.display = "block";
				windowIconUnSync.style.display = "none";

				for (key in applications[data.id].SAGE2StateOptions) {
					applications[data.id].SAGE2StateSyncChildren(applications[data.id].SAGE2StateOptions[key]._name,
						applications[data.id].SAGE2StateOptions, true);
				}
			}

			if (isMaster) {
				var stateOp = ignoreFields(applications[data.id].SAGE2StateOptions, ["_name", "_value"]);
				wsio.emit('updateStateOptions', {id: data.id, options: stateOp});
			}
		} else {
			if (applications[data.id].SAGE2StateSyncOptions.visible === false) {
				applications[data.id].SAGE2StateSyncOptions.visible = true;
				windowTitle.style.backgroundColor = "#666666";
				windowState.style.display = "block";
			} else {
				applications[data.id].SAGE2StateSyncOptions.visible = false;
				windowTitle.style.backgroundColor = "#39C4A6";
				windowState.style.display = "none";
			}
		}
	});

	wsio.on('sendServerWallScreenshot', function(data) {
		// first tell user that screenshot is happening, because screen will freeze
		makingScreenshotDialog = ui.buildMessageBox('makingScreenshotDialog',
			'Please wait, wall is taking a screenshot');
		// Add to the DOM
		ui.main.appendChild(makingScreenshotDialog);
		// Make the dialog visible
		makingScreenshotDialog.style.display = "block";
		// now do check and perform capture if can
		if (!__SAGE2__.browser.isElectron) {
			wsio.emit("wallScreenshotFromDisplay", {capable: false});
		} else {
			// set a rectangle of the client size
			var captureRect = { x: 0, y: 0, width: ui.main.clientWidth, height: ui.main.clientHeight };
			require('electron').remote.getCurrentWindow().capturePage(captureRect, function(img) {
				var imageData, resized;
				// get the size of the screenshot image
				var shot = img.getSize();
				if (shot.width !== captureRect.width) {
					// in retina mode, need to downscale the image
					resized = img.resize({width: captureRect.width, quality: 'better'});
					// use JPEG with quality 90%
					imageData = resized.toJPEG(90);
				} else {
					imageData = img.toJPEG(90);
				}
				// Send the image back to the server as JPEG
				wsio.emit("wallScreenshotFromDisplay", {
					capable: true,
					imageData: imageData
				});
				// Close the dialog
				deleteElement('makingScreenshotDialog');
			});
		}
	});

	wsio.on('showStickyPin', function(data) {
		if (data.sticky !== true) {
			return;
		}
		var titleBarHeight = ui.titleBarHeight;
		var iconWidth = Math.round(titleBarHeight) * (300 / 235);
		var iconSpace = 0.1 * iconWidth;
		var titleText = document.getElementById(data.id + "_text");
		var windowIconPinned = document.getElementById(data.id + "_iconPinned");
		var windowIconPinout = document.getElementById(data.id + "_iconPinout");
		titleText.style.marginLeft = Math.round(iconWidth + 2 * iconSpace) + "px";
		if (data.pinned === true) {
			windowIconPinned.style.display = "block";
			windowIconPinout.style.display = "none";
		} else {
			windowIconPinned.style.display = "none";
			windowIconPinout.style.display = "block";
		}
	});

	wsio.on('hideStickyPin', function(data) {
		if (data.sticky !== true) {
			return;
		}
		var titleBarHeight = ui.titleBarHeight;
		var titleText = document.getElementById(data.id + "_text");
		var windowIconPinned = document.getElementById(data.id + "_iconPinned");
		var windowIconPinout = document.getElementById(data.id + "_iconPinout");
		titleText.style.marginLeft = Math.round(titleBarHeight / 4.0) + "px";
		windowIconPinned.style.display = "none";
		windowIconPinout.style.display = "none";
	});

	wsio.on('getPerformanceData', function(data) {
		if (__SAGE2__.browser.isElectron) {
			require('electron').ipcRenderer.send('getPerformanceData');
		}
	});

	wsio.on('performanceData', function(data) {
		var perfAppList = data.appList;
		var app;
		if (perfAppList === undefined || perfAppList === null) {
			return;
		}
		for (var i = 0; i < perfAppList.length; i++) {
			app = applications[perfAppList[i]];
			app.SAGE2Event('performanceData', null, null, data, data.date);
		}
	});
}

function createAppWindow(data, parentId, titleBarHeight, titleTextSize, offsetX, offsetY) {
	resetIdle();

	var parent = document.getElementById(parentId);

	var date = new Date(data.date);
	var translate = "translate(" + data.left + "px," + data.top + "px)";

	var windowTitle = document.createElement("div");
	windowTitle.id  = data.id + "_title";
	windowTitle.className    = "windowTitle";
	windowTitle.style.width  = data.width.toString() + "px";
	windowTitle.style.height = titleBarHeight.toString() + "px";
	windowTitle.style.left   = (-offsetX).toString() + "px";
	windowTitle.style.top    = (-offsetY).toString() + "px";
	windowTitle.style.webkitTransform = translate;
	windowTitle.style.mozTransform    = translate;
	windowTitle.style.transform       = translate;
	windowTitle.style.zIndex = itemCount.toString();
	if (ui.noDropShadow === true) {
		windowTitle.style.boxShadow = "none";
	}
	if (ui.uiHidden === true) {
		windowTitle.style.display   = "none";
	}

	var iconWidth = Math.round(titleBarHeight) * (300 / 235);
	var iconSpace = 0.1 * iconWidth;
	var windowIconSync = document.createElement("img");
	windowIconSync.id  = data.id + "_iconSync";
	windowIconSync.src = "images/window-sync.svg";
	windowIconSync.height = Math.round(titleBarHeight);
	windowIconSync.style.position = "absolute";
	windowIconSync.style.right    = Math.round(2 * (iconWidth + iconSpace)) + "px";
	windowIconSync.style.display  = "none";
	windowTitle.appendChild(windowIconSync);

	var windowIconUnSync = document.createElement("img");
	windowIconUnSync.id  = data.id + "_iconUnSync";
	windowIconUnSync.src = "images/window-unsync.svg";
	windowIconUnSync.height = Math.round(titleBarHeight);
	windowIconUnSync.style.position = "absolute";
	windowIconUnSync.style.right    = Math.round(2 * (iconWidth + iconSpace)) + "px";
	windowIconUnSync.style.display  = "none";
	windowTitle.appendChild(windowIconUnSync);

	var windowIconFullscreen = document.createElement("img");
	windowIconFullscreen.id  = data.id + "_iconFullscreen";
	windowIconFullscreen.src = "images/window-fullscreen.svg";
	windowIconFullscreen.height = Math.round(titleBarHeight);
	windowIconFullscreen.style.position = "absolute";
	windowIconFullscreen.style.right    = Math.round(1 * (iconWidth + iconSpace)) + "px";
	windowTitle.appendChild(windowIconFullscreen);

	var windowIconClose = document.createElement("img");
	windowIconClose.id  = data.id + "_iconClose";
	windowIconClose.src = "images/window-close3.svg";
	windowIconClose.height = Math.round(titleBarHeight);
	windowIconClose.style.position = "absolute";
	windowIconClose.style.right    = "0px";
	windowTitle.appendChild(windowIconClose);

	if (data.sticky === true) {
		var windowIconPinned = document.createElement("img");
		windowIconPinned.id  = data.id + "_iconPinned";
		windowIconPinned.src = "images/window-pinned.svg";
		windowIconPinned.height = Math.round(titleBarHeight);
		windowIconPinned.style.position = "absolute";
		windowIconPinned.style.left    = Math.round(iconSpace) + "px";
		windowIconPinned.style.display  = "none";
		windowTitle.appendChild(windowIconPinned);

		var windowIconPinout = document.createElement("img");
		windowIconPinout.id  = data.id + "_iconPinout";
		windowIconPinout.src = "images/window-pinout.svg";
		windowIconPinout.height = Math.round(titleBarHeight);
		windowIconPinout.style.position = "absolute";
		windowIconPinout.style.left    = Math.round(iconSpace) + "px";
		windowIconPinout.style.display  = "none";
		windowTitle.appendChild(windowIconPinout);
	}
	var titleText = document.createElement("p");
	titleText.id  = data.id + "_text";
	titleText.style.lineHeight = Math.round(titleBarHeight) + "px";
	titleText.style.fontSize   = Math.round(titleTextSize) + "px";
	titleText.style.color      = "#FFFFFF";
	titleText.style.marginLeft = Math.round(titleBarHeight / 4.0) + "px";
	titleText.textContent      = data.title;
	windowTitle.appendChild(titleText);

	var windowItem = document.createElement("div");
	windowItem.id = data.id;
	windowItem.className      = "windowItem";
	windowItem.style.left     = (-offsetX).toString() + "px";
	windowItem.style.top      = (titleBarHeight - offsetY).toString() + "px";
	windowItem.style.webkitTransform = translate;
	windowItem.style.mozTransform    = translate;
	windowItem.style.transform       = translate;
	windowItem.style.overflow = "hidden";
	windowItem.style.zIndex   = (itemCount + 1).toString();
	if (ui.noDropShadow === true) {
		windowItem.style.boxShadow = "none";
	}
	if (ui.uiHidden === true) {
		windowItem.classList.toggle("windowItemNoBorder");
	}

	var windowState = document.createElement("div");
	windowState.id = data.id + "_state";
	windowState.style.position = "absolute";
	windowState.style.width  = data.width.toString() + "px";
	windowState.style.height = data.height.toString() + "px";
	windowState.style.backgroundColor = "rgba(0,0,0,0.8)";
	windowState.style.lineHeight = Math.round(1.5 * titleTextSize) + "px";
	windowState.style.zIndex = "100";
	windowState.style.display = "none";

	var windowStateContatiner = document.createElement("div");
	windowStateContatiner.id = data.id + "_statecontainer";
	windowStateContatiner.style.position = "absolute";
	windowStateContatiner.style.top = "0px";
	windowStateContatiner.style.left = "0px";
	windowStateContatiner.style.webkitTransform = "translate(0px,0px)";
	windowStateContatiner.style.mozTransform = "translate(0px,0px)";
	windowStateContatiner.style.transform = "translate(0px,0px)";
	windowState.appendChild(windowStateContatiner);
	windowItem.appendChild(windowState);

	var cornerSize = Math.min(data.width, data.height) / 5;
	var dragCorner = document.createElement("div");
	dragCorner.className      = "dragCorner";
	dragCorner.style.position = "absolute";
	dragCorner.style.width    = cornerSize.toString() + "px";
	dragCorner.style.height   = cornerSize.toString() + "px";
	dragCorner.style.top      = (data.height - cornerSize).toString() + "px";
	dragCorner.style.left     = (data.width - cornerSize).toString() + "px";
	dragCorner.style.backgroundColor = "rgba(255,255,255,0.0)";
	dragCorner.style.border   = "none";
	dragCorner.style.zIndex   = "1";
	windowItem.appendChild(dragCorner);

	parent.appendChild(windowTitle);
	parent.appendChild(windowItem);

	// App launched in window
	if (data.application === "media_stream") {
		wsio.emit('receivedMediaStreamFrame', {id: data.id});
	}
	if (data.application === "media_block_stream") {
		wsio.emit('receivedMediaBlockStreamFrame', {id: data.id, newClient: true});
	}

	// convert url if hostname is alias for current origin
	var url = cleanURL(data.url);

	function loadApplication() {
		var init = {
			id: data.id,
			x: data.left,
			y: data.top + titleBarHeight,
			width: data.width,
			height: data.height,
			resrc: url,
			state: data.data,
			date: date,
			title: data.title,
			application: data.application
		};
		// extra data that may be passed from launchAppWithValues
		if (data.customLaunchParams) {
			init.customLaunchParams = data.customLaunchParams;
		}

		// load new app
		if (window[data.application] === undefined) {
			var js = document.createElement("script");
			js.addEventListener('error', function(event) {
				console.log("Error loading script: " + data.application + ".js");
			}, false);
			js.addEventListener('load', function(event) {
				var newapp = new window[data.application]();
				newapp.init(init);
				newapp.refresh(date);

				// Sending the context menu info to the server
				if (isMaster) {
					newapp.getFullContextMenuAndUpdate();
				}

				applications[data.id]   = newapp;
				controlObjects[data.id] = newapp;

				if (data.animation === true) {
					wsio.emit('finishedRenderingAppFrame', {id: data.id});
				}
			}, false);
			js.type  = "text/javascript";
			js.async = false;
			js.src = url + "/" + data.application + ".js";
			console.log("Loading>", data.id, url + "/" + data.application + ".js");
			document.head.appendChild(js);
		} else {
			// load existing app
			var app = new window[data.application]();
			app.init(init);
			app.refresh(date);

			// Sending the context menu info to the server
			if (isMaster) {
				app.getFullContextMenuAndUpdate();
			}

			applications[data.id] = app;
			controlObjects[data.id] = app;

			if (data.animation === true) {
				wsio.emit('finishedRenderingAppFrame', {id: data.id});
			}
			if (data.application === "movie_player") {
				setTimeout(function() {
					wsio.emit('requestVideoFrame', {id: data.id});
				}, 500);
			}
		}
	}

	// load all dependencies
	if (data.resrc === undefined || data.resrc === null || data.resrc.length === 0) {
		loadApplication();
	} else {
		var loadResource = function(idx) {
			var resourceUrl = data.resrc[idx];

			if (dependencies[resourceUrl] !== undefined) {
				if ((idx + 1) < data.resrc.length) {
					loadResource(idx + 1);
				} else {
					console.log("all resources loaded", data.id);
					loadApplication();
				}

				return;
			}

			// Not loaded yet
			dependencies[resourceUrl] = false;

			// Check the type
			var loaderType;
			if (resourceUrl.endsWith(".js")) {
				loaderType = "script";
			} else if (resourceUrl.endsWith(".css")) {
				loaderType = "link";
			} else {
				console.log('Dependencies> unknown file extension, assuming script', resourceUrl);
				loaderType = "script";
			}

			if (loaderType) {
				// Create the DOM element to laod the resource
				var loader = document.createElement(loaderType);

				// Place an error handler
				loader.addEventListener('error', function(event) {
					console.log("Dependencies> Error loading", resourceUrl);
				}, false);

				// When done, try next dependency in the list
				loader.addEventListener('load', function(event) {
					// Success, mark as loaded
					dependencies[data.resrc[idx]] = true;
					if ((idx + 1) < data.resrc.length) {
						// load the next one
						loadResource(idx + 1);
					} else {
						// We are done
						console.log("Dependencies> all resources loaded", data.id);
						loadApplication();
					}
				});

				// if not a full URL, add the local one
				if (resourceUrl.indexOf("http://")  !== 0 &&
					resourceUrl.indexOf("https://") !== 0 &&
					resourceUrl.indexOf("/") !== 0) {
					resourceUrl = url + "/" + resourceUrl;
				}

				// is it a JS file
				if (loaderType === "script") {
					loader.type  = "text/javascript";
					loader.async = false;
					loader.src   = resourceUrl;
				} else if (loaderType === "link") {
					// is it a CSS file
					loader.setAttribute("type", "text/css");
					loader.setAttribute("rel",  "stylesheet");
					loader.setAttribute("href", resourceUrl);
				} else {
					console.log('Dependencies> unknown file type', resourceUrl);
				}

				// Finally, add it to the document to trigger the laod
				document.head.appendChild(loader);
			}
		};
		// Start loading the first resource
		loadResource(0);
	}

	itemCount += 2;
}

function moveItemWithAnimation(updatedApp) {
	var elemTitle = document.getElementById(updatedApp.elemId + "_title");
	var elem = document.getElementById(updatedApp.elemId);

	var translate = "translate(" + updatedApp.elemLeft + "px," + updatedApp.elemTop + "px)";

	// allow for transform transitions
	elemTitle.style.transition = "opacity 0.2s ease-in, transform 0.2s linear";
	elem.style.transition = "opacity 0.2s ease-in, transform 0.2s linear";

	// update transforms
	elemTitle.style.transform = translate;
	elem.style.transform = translate;

	// reset transition after transform transition time
	setTimeout(function() {
		elemTitle.style.transition = "opacity 0.2s ease-in";
		elem.style.transition = "opacity 0.2s ease-in";
	}, 200);
}