define(["sugar-web/activity/activity","sugar-web/datastore","notepalette","zoompalette","sugar-web/graphics/presencepalette","humane","tutorial","sugar-web/env"], function (activity,datastore,notepalette,zoompalette,presencepalette,humane,tutorial,env) {
var defaultColor = '#FFF29F';
var isShared = false;
var isHost = false;
var network = null;
var connectedPeople = {};
var xoLogo = ']>';
// Manipulate the DOM only when it is ready.
requirejs(['domReady!'], function (doc) {
// Initialize the activity.
activity.setup();
// Handle toolbar mode switch
var currentMode = 0;
var nodetextButton = document.getElementById("nodetext-button");
var removeButton = document.getElementById("delete-button");
var switchMode = function(newMode) {
currentMode = newMode;
nodetextButton.classList.remove('active');
removeButton.classList.remove('active');
saveAndFinishEdit();
if (newMode == 0) nodetextButton.classList.add('active');
else if (newMode == 1) removeButton.classList.add('active');
if (lastSelected != null) {
unselectAllNode();
lastSelected = null;
}
}
nodetextButton.addEventListener('click', function () { switchMode(0); }, true);
removeButton.addEventListener('click', function () { switchMode(1); }, true);
var colorButton = document.getElementById("color-button");
colorPalette = new notepalette.NotePalette(colorButton);
colorPalette.setColor('rgb(255, 242, 159)');
colorPalette.addEventListener('colorChange', function(e) {
if (isSelectedNode(lastSelected)) {
pushState({
redo: {action:"update", id:lastSelected.id(), color: e.detail.color},
undo: {action:"update", id:lastSelected.id(), color: lastSelected.data('background-color')}
});
lastSelected.style('background-color', e.detail.color);
lastSelected.data('background-color', e.detail.color);
}
textValue.style.backgroundColor = e.detail.color;
defaultColor = e.detail.color;
});
var zoomButton = document.getElementById("zoom-button");
zoomPalette = new zoompalette.zoomPalette(zoomButton);
zoomPalette.addEventListener('pop', function(e) {
});
zoomPalette.addEventListener('zoom', function(e) {
var action = e.detail.zoom;
var currentZoom = cy.zoom();
var zoomStep = 0.25;
if (action == 0) {
if (currentZoom != cy.minZoom() && currentZoom-zoomStep > cy.minZoom()) cy.zoom(currentZoom-zoomStep);
} else if (action == 1) {
if (currentZoom != cy.maxZoom()) cy.zoom(currentZoom+zoomStep);
} else if (action == 2) {
cy.fit();
} else if (action == 3) {
cy.center();
}
});
var pngButton = document.getElementById("png-button");
pngButton.addEventListener('click', function(e) {
var inputData = cy.png();
var mimetype = inputData.split(";")[0].split(":")[1];
var type = mimetype.split("/")[0];
var metadata = {
mimetype: mimetype,
title: type.charAt(0).toUpperCase() + type.slice(1) + " Shared Notes",
activity: "org.olpcfrance.MediaViewerActivity",
timestamp: new Date().getTime(),
creation_time: new Date().getTime(),
file_size: 0
};
datastore.create(metadata, function() {
console.log("export done.")
}, inputData);
});
var networkButton = document.getElementById("network-button");
var presence = new presencepalette.PresencePalette(networkButton, undefined);
// Handle graph save/world
var stopButton = document.getElementById("stop-button");
stopButton.addEventListener('click', function (event) {
console.log("writing...");
saveGraph(function (error) {
if (error === null) {
console.log("write done.");
}
else {
console.log("write failed.");
}
});
});
// --- Node and edge handling functions
var defaultFontFamily = "Arial";
var defaultFontSize = 16;
var lastSelected = null;
var defaultText = "";
var textValue = document.getElementById("textvalue");
var draggedPosition = null;
textValue.addEventListener('click', function (event) {
saveAndFinishEdit();
});
// Create a new node with text and position
var createNode = function(id, text, position, color) {
cy.add({
group: 'nodes',
nodes: [
{
data: {
id: id,
'content': text,
'color': 'rgb(0, 0, 0)',
'background-color': color
},
position: {
x: position.x,
y: position.y
}
}
]
});
var newnode = cy.getElementById(id);
newnode.style({
'content': text,
'background-color': color
});
newnode.addClass('standard-node');
return newnode;
}
// Update node text and change size
var updateNodeText = function(node, text) {
if (node == null) return;
var previous = node.data('content');
if (text === undefined) text = node.style()['content'];
else node.data('content', text);
node.style({
'content': text
});
if (previous != text) {
pushState({
redo: {action:"update", id:node.id(), text: text},
undo: {action:"update", id:node.id(), text: previous}
});
}
}
// Test if node is selected
var isSelectedNode = function(node) {
if (node == null) return false;
return node.style()['border-style'] == 'dashed';
}
// Set node as selected
var selectNode = function(node) {
if (node == null) return;
node.style({
'border-color': 'black',
'border-style': 'dashed',
'border-width': '4px'
});
}
// Set node as unselected
var unselectNode = function(node) {
if (node == null) return;
node.style({
'border-color': 'darkgray',
'border-style': 'solid',
'border-width': '1px'
});
}
// Unselect all node
var unselectAllNode = function() {
var nodes = cy.collection("node");
for (var i = 0 ; i < nodes.length ; i++) {
unselectNode(nodes[i]);
}
}
// Delete node, linked edges are removed too
var deleteNode = function(node) {
if (node == null) return;
cy.remove(node);
}
// --- Utility functions
// Show edit field
var showEditField = function(node) {
var position = node.renderedPosition();
var zoom = cy.zoom();
textValue.value = node.data('content');
textValue.style.visibility = "visible";
textValue.style.backgroundColor = node.style().backgroundColor;
var delta = 100 * zoom - 200 * zoom;
textValue.style.left = (position.x + delta) + "px";
textValue.style.top = (55 + position.y + delta) + "px";
textValue.style.width = 190 * zoom + "px";
textValue.style.height = 190 * zoom + "px";
if (textValue.value == defaultText)
textValue.setSelectionRange(0, textValue.value.length);
else
textValue.setSelectionRange(textValue.value.length, textValue.value.length);
textValue.focus();
}
// Hide edit field
var hideEditField = function() {
textvalue.style.visibility = "hidden";
}
var saveAndFinishEdit = function() {
if (lastSelected != null && isSelectedNode(lastSelected)) {
updateNodeText(lastSelected, textValue.value);
hideEditField();
unselectNode(lastSelected);
}
}
// Get center of drawing zone
var getCenter = function() {
var canvas = document.getElementById("canvas");
var center = {x: canvas.clientWidth/2, y: canvas.clientHeight/2};
return center;
}
// Generate a new id
var newId = function() {
var s = [];
var hexDigits = "0123456789abcdef";
for (var i = 0; i < 36; i++) {
s[i] = hexDigits.substr(Math.floor(Math.random() * 0x10), 1);
}
s[14] = "4";
s[19] = hexDigits.substr((s[19] & 0x3) | 0x8, 1);
s[8] = s[13] = s[18] = s[23] = "-";
var uuid = s.join("");
return uuid;
}
// Handle an update command from history or from the network
var doAction = function(command) {
if (command.action === undefined) return;
else if (command.action == 'create') {
// Create a new node
createNode(command.id, command.text, command.position, command.color);
} else if (command.action == 'delete') {
// Get the node
var node = cy.getElementById(command.id);
if (node == null) return;
// Delete it
cy.remove(node);
} else if (command.action == 'update') {
// Get the node
var node = cy.getElementById(command.id);
if (node == null) return;
// Update it
if (command.text !== undefined) {
node.data('content', command.text);
node.style({'content': command.text});
}
if (command.color !== undefined) {
node.data('background-color', command.color);
node.style({'background-color': command.color});
}
if (command.position !== undefined) {
node.position({
x: command.position.x,
y: command.position.y
});
}
}
}
// Load graph from datastore
var initGraph = function(graph) {
if (graph == null)
return;
cy.remove("node");
lastSelected = null;
for(var i = 0 ; i < graph.length ; i++) {
doAction(graph[i]);
}
hideEditField();
reinitState();
}
var loadGraph = function() {
var datastoreObject = activity.getDatastoreObject();
datastoreObject.loadAsText(function (error, metadata, data) {
initGraph(data);
});
}
// Save graph to datastore, generate command to rebuild each node
var getGraph = function() {
var commands = [];
var nodes = cy.elements("node");
for(var i = 0; i < nodes.length ; i++) {
var node = nodes[i];
commands.push({
action:"create", id:node.id(), text: node.data("content"), position: {x: node.position().x, y: node.position().y}, color: node.data("background-color")
});
}
return commands;
}
var saveGraph = function(callback) {
var datastoreObject = activity.getDatastoreObject();
var commands = getGraph();
datastoreObject.setDataAsText(commands);
datastoreObject.save(callback);
}
// Do/Undo handling
var stateHistory = [];
var stateIndex = 0;
var maxHistory = 30;
var undoButton = document.getElementById("undo-button");
undoButton.addEventListener('click', function () { saveAndFinishEdit(); undoState(); }, true);
var redoButton = document.getElementById("redo-button");
redoButton.addEventListener('click', function () { saveAndFinishEdit(); redoState(); }, true);
var reinitState = function() {
stateHistory = [];
stateIndex = 0;
}
var pushState = function(state, fromNetwork) {
if (stateIndex < stateHistory.length - 1) {
var stateCopy = [];
for (var i = 0 ; i < stateIndex + 1; i++)
stateCopy.push(stateHistory[i]);
stateHistory = stateCopy;
}
var stateLength = stateHistory.length - 1;
var currentState = state;
if (stateLength < maxHistory) stateHistory.push(currentState);
else {
for (var i = 0 ; i < stateLength ; i++) {
stateHistory[i] = stateHistory[i+1];
}
stateHistory[stateHistory.length-1] = currentState;
}
stateIndex = stateHistory.length - 1;
if (isShared && !fromNetwork) {
sendMessage({
action: 'updateBoard',
data: state
});
}
updateStateButtons();
}
var undoState = function(fromNetwork) {
if (stateHistory.length < 1 || stateIndex < 0) return;
var undo = stateHistory[stateIndex--].undo;
doAction(undo);
if (isShared && !fromNetwork) {
sendMessage({
action: 'undoBoard'
});
}
updateStateButtons();
}
var redoState = function(fromNetwork) {
if (stateIndex+1 >= stateHistory.length) return;
var redo = stateHistory[++stateIndex].redo;
doAction(redo);
if (isShared && !fromNetwork) {
sendMessage({
action: 'redoBoard'
});
}
updateStateButtons();
}
var updateStateButtons = function() {
var stateLength = stateHistory.length;
undoButton.disabled = (stateHistory.length < 1 || stateIndex < 0);
redoButton.disabled = (stateIndex+1 >= stateLength);
}
// Users network list functions
var generateXOLogoWithColor = function(color) {
var coloredLogo = xoLogo;
coloredLogo = coloredLogo.replace("#010101", color.stroke)
coloredLogo = coloredLogo.replace("#FFFFFF", color.fill)
return "data:image/svg+xml;base64," + btoa(coloredLogo);
}
var displayConnectedPeopleHtml = function() {
var presenceUsersDiv = document.getElementById("presence-users");
var html = "
"
for (var key in connectedPeople) {
html += "- " + connectedPeople[key].name + "
"
}
html += "
"
presenceUsersDiv.innerHTML = html
}
var displayConnectedPeople = function(users) {
var presenceUsersDiv = document.getElementById("presence-users");
if (!users || !presenceUsersDiv) {
return;
}
network.listSharedActivityUsers(network.getSharedInfo().id, function(usersConnected) {
connectedPeople = {};
for (var i = 0; i < usersConnected.length; i++) {
var userConnected = usersConnected[i];
connectedPeople[userConnected.networkId] = userConnected;
}
displayConnectedPeopleHtml();
});
}
// Handle activity sharing
var shareActivity = function() {
network = activity.getPresenceObject(function(error, network) {
// Unable to join
if (error) {
console.log("error");
return;
}
isShared = true;
// Store settings
userSettings = network.getUserInfo();
console.log("connected");
// Not found, create a new shared activity
if (!window.top.sugar.environment.sharedId) {
isHost = true;
network.createSharedActivity('org.olpcfrance.sharednotes', function(groupId) {});
}
// Show a disconnected message when the WebSocket is closed.
network.onConnectionClosed(function(event) {
console.log(event);
console.log("Connection closed");
});
// Display connection changed
network.onSharedActivityUserChanged(function(msg) {
onNetworkUserChanged(msg);
});
// Handle messages received
network.onDataReceived(onNetworkDataReceived);
});
}
var sendMessage = function(content) {
try {
network.sendMessage(network.getSharedInfo().id, {
user: network.getUserInfo(),
content: content
});
} catch (e) {}
}
var onNetworkDataReceived = function(msg) {
// Ignore messages coming from ourselves
if (network.getUserInfo().networkId === msg.user.networkId) {
return;
}
switch (msg.content.action) {
case 'initialBoard':
// Receive initial board from the host
initGraph(msg.content.data);
break;
case 'updateBoard':
// Board change received
doAction(msg.content.data.redo);
pushState(msg.content.data, true);
break
case 'undoBoard':
// Undo board
undoState(true);
break;
case 'redoBoard':
// Redo board
redoState(true);
break;
}
}
var onNetworkUserChanged = function(msg) {
var userName = msg.user.name.replace('<', '<').replace('>', '>');
var html = "";
if (msg.move === 1) {
humane.log(html + l10n_s.get("PlayerJoin",{user: userName}));
if (isHost) {
sendMessage({
action: 'initialBoard',
data: getGraph()
});
}
} else if (msg.move === -1) {
humane.log(html + l10n_s.get("PlayerLeave",{user: userName}));
}
network.listSharedActivities(function(activities) {
for (var i = 0; i < activities.length; i++) {
if (activities[i].id === network.getSharedInfo().id) {
displayConnectedPeople(activities[i].users);
}
}
});
}
// Handle presence palette
presence.addEventListener('shared', function() {
presence.popDown();
shareActivity();
});
if (window.top && window.top.sugar && window.top.sugar.environment && window.top.sugar.environment.sharedId) {
shareActivity();
presence.setShared(true);
}
// Handle help
var helpButton = document.getElementById("help-button");
tutorial.setElement("activity", document.getElementById("activity-button"));
tutorial.setElement("title", document.getElementById("title"));
tutorial.setElement("network", networkButton);
tutorial.setElement("help", helpButton);
tutorial.setElement("shared", document.getElementById("shared-button"));
tutorial.setElement("png", pngButton);
tutorial.setElement("zoom", zoomButton);
tutorial.setElement("color", colorButton);
tutorial.setElement("add", nodetextButton);
tutorial.setElement("remove", removeButton);
tutorial.setElement("undo", document.getElementById("undo-button"));
tutorial.setElement("redo", document.getElementById("redo-button"));
tutorial.setElement("stop", stopButton);
tutorial.setElement("node", document.getElementById('canvas'));
helpButton.addEventListener('click', function (event) {
tutorial.start();
});
env.getEnvironment(function(err, environment) {
// Initial tutorial coming from home view
if (environment.help) {
setTimeout(function() {
tutorial.start(tutorial.tourInit);
}, 500);
}
});
// Handle localization
window.addEventListener('localized', function() {
env.getEnvironment(function(err, environment) {
var defaultLanguage = (typeof chrome != 'undefined' && chrome.app && chrome.app.runtime) ? chrome.i18n.getUILanguage() : navigator.language;
var language = environment.user ? environment.user.language : defaultLanguage;
if (l10n_s.language.code != language) {
l10n_s.language.code = language;
};
var oldDefaultText = defaultText;
defaultText = l10n_s.get("YourNewIdea");
nodetextButton.title = l10n_s.get("nodetextTitle");
removeButton.title = l10n_s.get("removeButtonTitle");
undoButton.title = l10n_s.get("undoButtonTitle");
redoButton.title = l10n_s.get("redoButtonTitle");
zoomButton.title = l10n_s.get("zoomButtonTitle");
pngButton.title = l10n_s.get("pngButtonTitle");
networkButton.title = l10n_s.get("networkButtonTitle");
helpButton.title = l10n_s.get("helpButtonTitle");
if (cy) {
var nodes = cy.elements("node");
for(var i = 0; i < nodes.length ; i++) {
var node = nodes[i];
if (node.data('content') == oldDefaultText) {
node.data('content', defaultText);
node.style({'content': defaultText});
}
}
if (textValue && textValue.value == oldDefaultText) {
textValue.value = defaultText;
if (lastSelected) {
showEditField(lastSelected);
}
}
}
});
}, false);
// --- Cytoscape handling
// Initialize board
cy = cytoscape({
container: document.getElementById('cy'),
ready: function() {
// Create first node and select id
cy = this;
var firstNode = createNode(newId(), defaultText, getCenter(), defaultColor);
pushState({
redo: {action:"create", id:firstNode.id(), text: firstNode.data("content"), position: {x: firstNode.position().x, y: firstNode.position().y}, color: defaultColor},
undo: {action:"delete", id:firstNode.id()}
});
firstNode.select();
selectNode(firstNode);
showEditField(firstNode);
lastSelected = firstNode;
// Load world
loadGraph();
},
style: [
{
selector: '.standard-node',
css: {
'width': '200px',
'height': '200px',
'text-valign': 'center',
'text-halign': 'center',
'border-color': 'darkgray',
'border-width': '1px',
'background-color': defaultColor,
'text-wrap': 'wrap',
'text-max-width': '200px',
'shadow-color': 'black',
'shadow-offset-x': '4px',
'shadow-offset-y': '4px',
'shadow-opacity': '0.5',
'shape': 'rectangle'
}
}
]
});
// Event: a node is selected
cy.on('tap', 'node', function() {
if (currentMode == 1) {
pushState({
redo: {action:"delete", id:this.id()},
undo: {action:"create", id:this.id(), text: this.data("content"), position: {x: this.position().x, y: this.position().y}, color: defaultColor}
});
deleteNode(this);
if (lastSelected == this) lastSelected = null;
return;
} else {
if (!isSelectedNode(this)) {
if (lastSelected != null) {
updateNodeText(lastSelected, textValue.value);
unselectNode(lastSelected);
}
selectNode(this);
showEditField(this);
}
lastSelected = this;
}
});
// Event: a node is unselected
cy.on('unselect', 'node', function() {
saveAndFinishEdit();
unselectNode(this);
});
// Event: tap on the board
cy.on('tap', function(e){
if (e.cyTarget === cy) {
if (currentMode == 0) {
var newNode = createNode(newId(), defaultText, e.cyPosition, defaultColor);
pushState({
redo: {action:"create", id:newNode.id(), text: newNode.data("content"), position: {x: newNode.position().x, y: newNode.position().y}, color: defaultColor},
undo: {action:"delete", id:newNode.id()}
});
newNode.select();
selectNode(newNode);
showEditField(newNode);
lastSelected = newNode;
}
}
});
// Event: elements moved
cy.on('drag', 'node', function(e) {
saveAndFinishEdit();
if (draggedPosition == null) {
draggedPosition = {x: this.position().x, y: this.position().y};
}
});
cy.on('free', 'node', function(e) {
if (draggedPosition != null && (this.position().x != draggedPosition.x || this.position().y != draggedPosition.y)) {
pushState({
redo: {action:"update", id:this.id(), position: {x: this.position().x, y: this.position().y}},
undo: {action:"update", id:this.id(), position: {x: draggedPosition.x, y: draggedPosition.y}}
});
}
draggedPosition = null;
});
// Event: zoom
cy.on('zoom', function() {
saveAndFinishEdit();
});
// Event: move
cy.on('pan', function() {
saveAndFinishEdit();
});
});
});