summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.gitmodules12
m---------Snapin8r0
-rw-r--r--blocks.js119
-rw-r--r--github.js312
-rw-r--r--gui.js537
-rwxr-xr-xhistory.txt26
-rw-r--r--lang-de.js13
-rw-r--r--morphic.js39
-rw-r--r--objects.js505
m---------octokit.js0
-rw-r--r--paint.js7
-rw-r--r--peer.js2711
-rw-r--r--promise-1.0.0.js684
-rwxr-xr-xsnap.html12
-rw-r--r--store.js57
-rw-r--r--threads.js190
-rw-r--r--tools.xml2
m---------vkBeautify0
-rw-r--r--widgets.js11
19 files changed, 5108 insertions, 129 deletions
diff --git a/.gitmodules b/.gitmodules
new file mode 100644
index 0000000..c1a2763
--- /dev/null
+++ b/.gitmodules
@@ -0,0 +1,12 @@
+[submodule "Snapin8r"]
+ path = Snapin8r
+ url = https://github.com/Hardmath123/Snapin8r
+[submodule "octokit.js"]
+ path = octokit.js
+ url = https://github.com/philschatz/octokit.js
+[submodule "vkBeautify"]
+ path = vkBeautify
+ url = https://github.com/vkiryukhin/vkBeautify
+[submodule "diff-merge"]
+ path = diff-merge
+ url = https://github.com/nighca/diff-merge
diff --git a/Snapin8r b/Snapin8r
new file mode 160000
+Subproject c9cebaa147c2c53520212a221efd4061a5dcb19
diff --git a/blocks.js b/blocks.js
index 5cdcc1c..e9daff2 100644
--- a/blocks.js
+++ b/blocks.js
@@ -9,7 +9,7 @@
written by Jens Mönig
jens@moenig.org
- Copyright (C) 2014 by Jens Mönig
+ Copyright (C) 2015 by Jens Mönig
This file is part of Snap!.
@@ -155,7 +155,7 @@ DialogBoxMorph, BlockInputFragmentMorph, PrototypeHatBlockMorph, Costume*/
// Global stuff ////////////////////////////////////////////////////////
-modules.blocks = '2015-February-28';
+modules.blocks = '2015-March-23';
var SyntaxElementMorph;
@@ -340,7 +340,7 @@ SyntaxElementMorph.prototype.setScale = function (num) {
};
SyntaxElementMorph.prototype.setScale(1);
-SyntaxElementMorph.prototype.isCachingInputs = true;
+SyntaxElementMorph.prototype.isCachingInputs = false;
// SyntaxElementMorph instance creation:
@@ -382,9 +382,36 @@ SyntaxElementMorph.prototype.inputs = function () {
return part instanceof SyntaxElementMorph;
});
}
+ // this.debugCachedInputs();
return this.cachedInputs;
};
+SyntaxElementMorph.prototype.debugCachedInputs = function () {
+ // private - only used for manually debugging inputs caching
+ var realInputs, i;
+ if (!isNil(this.cachedInputs)) {
+ realInputs = this.parts().filter(function (part) {
+ return part instanceof SyntaxElementMorph;
+ });
+ }
+ if (this.cachedInputs.length !== realInputs.length) {
+ throw new Error('cached inputs size do not match: ' +
+ this.constructor.name);
+ }
+ for (i = 0; i < realInputs.length; i += 1) {
+ if (this.cachedInputs[i] !== realInputs[i]) {
+ throw new Error('cached input does not match: ' +
+ this.constructor.name +
+ ' #' +
+ i +
+ ' ' +
+ this.cachedInputs[i].constructor.name +
+ ' != ' +
+ realInputs[i].constructor.name);
+ }
+ }
+};
+
SyntaxElementMorph.prototype.allInputs = function () {
// answer arguments and nested reporters of all children
var myself = this;
@@ -994,6 +1021,8 @@ SyntaxElementMorph.prototype.labelPart = function (spec) {
null,
false,
{
+ 'any key': ['any key'],
+ 'number key': ['number key'],
'up arrow': ['up arrow'],
'down arrow': ['down arrow'],
'right arrow': ['right arrow'],
@@ -1164,6 +1193,15 @@ SyntaxElementMorph.prototype.labelPart = function (spec) {
);
part.isStatic = true;
break;
+ case '%shd':
+ part = new InputSlotMorph(
+ null,
+ false,
+ 'shadowedVariablesMenu',
+ true
+ );
+ part.isStatic = true;
+ break;
case '%lst':
part = new InputSlotMorph(
null,
@@ -1862,7 +1900,8 @@ SyntaxElementMorph.prototype.endLayout = function () {
%att - chameleon colored rectangular drop-down for attributes
%fun - chameleon colored rectangular drop-down for math functions
%typ - chameleon colored rectangular drop-down for data types
- %var - chameleon colored rectangular drop-down for variable names
+ %var - chameleon colored rectangular drop-down for variable names
+ %shd - Chameleon colored rectuangular drop-down for shadowed var names
%lst - chameleon colored rectangular drop-down for list names
%b - chameleon colored hexagonal slot (for predicates)
%l - list icon
@@ -1916,6 +1955,7 @@ BlockMorph.uber = SyntaxElementMorph.prototype;
// BlockMorph preferences settings:
+BlockMorph.prototype.isCachingInputs = true;
BlockMorph.prototype.zebraContrast = 40; // alternating color brightness
// BlockMorph sound feedback:
@@ -2988,6 +3028,12 @@ BlockMorph.prototype.alternateBlockColor = function () {
this.fixChildrensBlockColor(true); // has issues if not forced
};
+BlockMorph.prototype.ghost = function () {
+ this.setColor(
+ SpriteMorph.prototype.blockColor[this.category].lighter(35)
+ );
+};
+
BlockMorph.prototype.fixLabelColor = function () {
if (this.zebraContrast > 0 && this.category) {
var clr = SpriteMorph.prototype.blockColor[this.category];
@@ -3036,6 +3082,9 @@ BlockMorph.prototype.fullCopy = function () {
ans.setSpec(this.instantiationSpec);
}
ans.allChildren().filter(function (block) {
+ if (block instanceof SyntaxElementMorph) {
+ block.cachedInputs = null;
+ }
return !isNil(block.comment);
}).forEach(function (block) {
var cmnt = block.comment.fullCopy();
@@ -3065,6 +3114,10 @@ BlockMorph.prototype.mouseClickLeft = function () {
}
};
+BlockMorph.prototype.reactToTemplateCopy = function () {
+ this.forceNormalColoring();
+};
+
// BlockMorph thumbnail
BlockMorph.prototype.thumbnail = function (scale, clipWidth, noShadow) {
@@ -4614,6 +4667,7 @@ RingMorph.uber = ReporterBlockMorph.prototype;
// RingMorph preferences settings:
+RingMorph.prototype.isCachingInputs = false;
// RingMorph.prototype.edge = 2;
// RingMorph.prototype.rounding = 9;
// RingMorph.prototype.alpha = 0.8;
@@ -6860,6 +6914,21 @@ InputSlotMorph.prototype.getVarNamesDict = function () {
return {};
};
+InputSlotMorph.prototype.shadowedVariablesMenu = function () {
+ var block = this.parentThatIsA(BlockMorph),
+ rcvr,
+ dict = {};
+
+ if (!block) {return dict; }
+ rcvr = block.receiver();
+ if (rcvr) {
+ rcvr.inheritedVariableNames(true).forEach(function (name) {
+ dict[name] = name;
+ });
+ }
+ return dict;
+};
+
InputSlotMorph.prototype.setChoices = function (dict, readonly) {
// externally specify choices and read-only status,
// used for custom blocks
@@ -7778,6 +7847,8 @@ SymbolMorph.prototype.names = [
'pointRight',
'gears',
'file',
+ 'mutedSounds',
+ 'unmutedSounds',
'fullScreen',
'normalScreen',
'smallStage',
@@ -7902,6 +7973,10 @@ SymbolMorph.prototype.symbolCanvasColored = function (aColor) {
return this.drawSymbolGears(canvas, aColor);
case 'file':
return this.drawSymbolFile(canvas, aColor);
+ case 'mutedSounds':
+ return this.drawSymbolMutedSounds(canvas, aColor);
+ case 'unmutedSounds':
+ return this.drawSymbolUnmutedSounds(canvas, aColor);
case 'fullScreen':
return this.drawSymbolFullScreen(canvas, aColor);
case 'normalScreen':
@@ -8104,6 +8179,34 @@ SymbolMorph.prototype.drawSymbolFile = function (canvas, color) {
return canvas;
};
+SymbolMorph.prototype.drawSymbolMutedSounds = function (canvas, color) {
+ // answer a canvas showing a muted sounds toggling symbol
+ var ctx = canvas.getContext('2d'),
+ w = canvas.width,
+ h = canvas.height,
+ w2 = w / 2,
+ h2 = h / 2;
+
+ ctx.fillStyle = color.darker(40).toString();
+ ctx.fillRect(0, 0, w, h);
+
+ return canvas;
+};
+
+SymbolMorph.prototype.drawSymbolUnmutedSounds = function (canvas, color) {
+ // answer a canvas showing a UNmuted sounds toggling symbol
+ var ctx = canvas.getContext('2d'),
+ w = canvas.width,
+ h = canvas.height,
+ w2 = w / 2,
+ h2 = h / 2;
+
+ ctx.fillStyle = color.darker(60).toString();
+ ctx.fillRect(0, 0, w, h);
+
+ return canvas;
+};
+
SymbolMorph.prototype.drawSymbolFullScreen = function (canvas, color) {
// answer a canvas showing two arrows pointing diagonally outwards
var ctx = canvas.getContext('2d'),
@@ -9197,10 +9300,6 @@ MultiArgMorph.prototype = new ArgMorph();
MultiArgMorph.prototype.constructor = MultiArgMorph;
MultiArgMorph.uber = ArgMorph.prototype;
-// MultiArgMorph preferences settings
-
-MultiArgMorph.prototype.isCachingInputs = false;
-
// MultiArgMorph instance creation:
function MultiArgMorph(
@@ -9631,10 +9730,6 @@ ArgLabelMorph.prototype = new ArgMorph();
ArgLabelMorph.prototype.constructor = ArgLabelMorph;
ArgLabelMorph.uber = ArgMorph.prototype;
-// ArgLabelMorph preferences settings
-
-ArgLabelMorph.prototype.isCachingInputs = false;
-
// MultiArgMorph instance creation:
function ArgLabelMorph(argMorph, labelTxt) {
diff --git a/github.js b/github.js
new file mode 100644
index 0000000..5294037
--- /dev/null
+++ b/github.js
@@ -0,0 +1,312 @@
+/*
+
+ github.js
+
+ a GitHubBackend backend API for SNAP!
+
+ written by Gubolin, based on cloud.js by Jens Mönig
+
+ Copyright (C) 2014 by Jens Mönig, Gubolin
+
+ This file is part of Snap!.
+
+ Snap! is free software: you can redistribute it and/or modify
+ it under the terms of the GNU Affero General Public License as
+ published by the Free Software Foundation, either version 3 of
+ the License, or (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU Affero General Public License for more details.
+
+ You should have received a copy of the GNU Affero General Public License
+ along with this program. If not, see <http://www.gnu.org/licenses/>.
+
+*/
+
+// Global settings /////////////////////////////////////////////////////
+
+/*global modules, localize*/
+
+modules.github = '2014-July-31';
+
+// Global stuff
+
+var GitHubBackend;
+
+var GitHub = new GitHubBackend();
+
+// GitHubBackend /////////////////////////////////////////////////////////////
+
+function GitHubBackend() {
+ this.gh = null;
+ this.username = null;
+ this.password = null; // TODO saved as plain text
+}
+
+GitHubBackend.prototype.clear = function () {
+ this.gh = null;
+ this.username = null;
+ this.password = null;
+};
+
+// GitHubBackend: Snap! API
+
+GitHubBackend.prototype.getProject = function (
+ userName,
+ projectName,
+ callBack,
+ errorCall,
+ commitSha
+) {
+ var myself = this;
+
+ if (myself.gh === null) {
+ myself.gh = new Octokit();
+ }
+
+ var repo = myself.gh.getRepo(userName, projectName);
+ var branch = repo.getBranch(); // master (default)
+ branch.getCommits({}).then(
+ function (commits) {
+ branch.read('snap.xml', false).then(
+ function (sourceContent) {
+ callBack.call(
+ null,
+ sourceContent.content,
+ commits[0].sha
+ );
+ },
+ function (error) {
+ errorCall.call(this, error, 'GitHub');
+ }
+ );
+ },
+ function (error) {
+ errorCall.call(this, error, 'GitHub');
+ }
+ );
+};
+
+GitHubBackend.prototype.login = function (
+ username,
+ password,
+ validateData,
+ callBack,
+ errorCall
+) {
+ var myself = this;
+ var me;
+
+ myself.gh = new Octokit({
+ username: username,
+ password: password
+ });
+
+ if (validateData === true) {
+ me = myself.gh.getUser();
+ if (me !== null) {
+ me.getInfo().then(
+ function() {
+ myself.username = username;
+ myself.password = password;
+
+ callBack.call(myself);
+ },
+ function (error) {
+ errorCall.call(this, error, 'GitHub');
+ }
+ );
+ } else {
+ errorCall.call(myself, localize('Something went wrong :('), 'GitHub');
+ }
+ } else {
+ myself.username = username;
+ myself.password = password;
+
+ callBack.call(myself);
+ }
+};
+
+GitHubBackend.prototype.saveProject = function (commitMessage, parentCommitSha, lastCommit, ide, callBack, errorCall) {
+ var myself = this,
+ data;
+ var pdata, media;
+ var repoName = ide.projectName.replace(/[^\w-]/g, ''); // TODO validation of project name
+
+ ide.stage.fireStopAllEvent();
+ ide.serializer.isCollectingMedia = true;
+ pdata = ide.serializer.serialize(ide.stage);
+ media = ide.hasChangedMedia ?
+ ide.serializer.mediaXML(ide.projectName) : null;
+ data = '<snapdata>\n' + pdata + '\n' + media + '\n</snapdata>';
+
+ // check if serialized data can be parsed back again
+ try {
+ ide.serializer.parse(pdata);
+ } catch (err) {
+ ide.showMessage('Serialization of program data failed:\n' + err);
+ throw new Error('Serialization of program data failed:\n' + err);
+ }
+ if (media !== null) {
+ try {
+ ide.serializer.parse(media);
+ } catch (err) {
+ ide.showMessage('Serialization of media failed:\n' + err);
+ throw new Error('Serialization of media failed:\n' + err);
+ }
+ }
+ ide.serializer.isCollectingMedia = false;
+ ide.serializer.flushMedia();
+
+ myself.getProjectList(
+ function (projects) {
+ var exists = false;
+
+ projects.forEach(function (project) {
+ if (project.ProjectName.indexOf(repoName) > -1) {
+ exists = true;
+ return;
+ }
+ });
+
+ pushChanges = function () {
+ if (myself.gh !== null) {
+ var repo = myself.gh.getRepo(myself.username, ide.projectName);
+ var branch = repo.getBranch(); // master (default)
+ var message = commitMessage;
+
+ writeChanges = function (code, pcSha) {
+ if (pcSha !== parentCommitSha && data !== code) {
+ var compareResult = window.diff.compare(lastCommit, data, '\n'); // get diff from last push
+ data = window.diff.merge(code, compareResult); // apply it to the latest code
+ console.log(data);
+ }
+
+ var contents = {
+ 'snap.xml': data,
+ 'README.md': ide.projectNotes
+ };
+
+ branch.writeMany(contents, message, pcSha).then(
+ function () {
+ callBack.call();
+ },
+ function (error) {
+ errorCall.call(this, error, 'GitHub');
+ }
+ );
+ };
+
+ if (parentCommitSha !== null) { // repo was just created
+ myself.getProject(myself.username, ide.projectName,
+ writeChanges,
+ function (error) {
+ errorCall.call(this, error, 'GitHub');
+ }
+ );
+ } else {
+ writeChanges(data, parentCommitSha);
+ }
+ }
+ };
+
+ if (exists === false){
+ myself.gh.getUser().createRepo(repoName, { // these should be discussed
+ 'description': 'Snap! Project - http://gubolin.github.io/snap/index.html#github:Username=' + myself.username + '&projectName=' + repoName,
+ 'has_wiki': 'false',
+ 'has_downloads': 'false',
+ 'auto_init': true,
+ 'license_template': 'mit' // discuss
+ }).then(
+ pushChanges,
+ function (error) {
+ errorCall.call(this, error, 'GitHub');
+ }
+ );
+ } else {
+ pushChanges();
+ }
+
+ },
+ function (error) {
+ errorCall.call(null, error, 'GitHub');
+ }
+ );
+};
+
+GitHubBackend.prototype.getProjectList = function (callBack, errorCall) {
+ var myself = this;
+
+ if (myself.gh !== null){
+ var user = myself.gh.getUser();
+
+ if (user === null) {
+ myself.message('You are not logged in');
+ return;
+ }
+
+ user.getRepos().then(
+ function (repos) {
+ var snapProjects = [];
+
+ var modCallBack = (function () {
+ var called = 0;
+ return function () {
+ if (++called == repos.length) {
+ callBack.call(myself, snapProjects);
+ }
+ };
+ })();
+
+ if (repos.length === 0) {
+ callBack.call(myself, snapProjects);
+ }
+
+ repos.forEach(function (repo) {
+ if (repo.description.indexOf('Snap! Project') > -1) { // TODO nicer detection
+ var project, ghrepo, branch;
+
+ ghrepo = myself.gh.getRepo(repo.owner.login, repo.name);
+ branch = ghrepo.getBranch(); // master (default)
+
+ branch.read('README.md', false).then(
+ function (notesContent) {
+ project = {
+ 'ProjectName': repo.name,
+ 'Notes': notesContent.content,
+ 'Updated': repo.updated_at.replace(/T/, ' ').replace(/Z/, '') // TODO this could be better
+ };
+
+ snapProjects.push(project);
+ modCallBack();
+ },
+ function (error) {
+ errorCall.call(this, error, 'GitHub');
+ }
+ );
+ } else {
+ modCallBack();
+ }
+ });
+ },
+ function (error) {
+ errorCall.call(this, error, 'GitHub');
+ }
+ );
+ } else {
+ myself.message('You are not logged in');
+ return;
+ }
+};
+
+GitHubBackend.prototype.logout = function (callBack) {
+ this.clear();
+};
+
+// GitHub: user messages (to be overridden)
+
+GitHubBackend.prototype.message = function (string) {
+ alert(string);
+};
diff --git a/gui.js b/gui.js
index 75220b3..dab70d1 100644
--- a/gui.js
+++ b/gui.js
@@ -63,13 +63,13 @@ Costume, CostumeEditorMorph, MorphicPreferences, touchScreenSettings,
standardSettings, Sound, BlockMorph, ToggleMorph, InputSlotDialogMorph,
ScriptsMorph, isNil, SymbolMorph, BlockExportDialogMorph,
BlockImportDialogMorph, SnapTranslator, localize, List, InputSlotMorph,
-SnapCloud, Uint8Array, HandleMorph, SVG_Costume, fontHeight, hex_sha512,
+SnapCloud, GitHub, Uint8Array, HandleMorph, SVG_Costume, fontHeight, hex_sha512,
sb, CommentMorph, CommandBlockMorph, BlockLabelPlaceHolderMorph, Audio,
SpeechBubbleMorph*/
// Global stuff ////////////////////////////////////////////////////////
-modules.gui = '2015-February-28';
+modules.gui = '2015-March-21';
// Declarations
@@ -229,6 +229,7 @@ IDE_Morph.prototype.init = function (isAutoFill) {
this.corral = null;
this.isAutoFill = isAutoFill || true;
+ this.isMuted = false;
this.isAppMode = false;
this.isSmallStage = false;
this.filePicker = null;
@@ -238,6 +239,8 @@ IDE_Morph.prototype.init = function (isAutoFill) {
this.stageRatio = 1; // for IDE animations, e.g. when zooming
this.loadNewProject = false; // flag when starting up translated
+ this.parentCommitSha = null; // for GitHub
+ this.lastCommit = null; // for GitHub
this.shield = null;
// initialize inherited properties:
@@ -245,6 +248,39 @@ IDE_Morph.prototype.init = function (isAutoFill) {
// override inherited properites:
this.color = this.backgroundColor;
+
+ setInterval(this.save, 1000 * 60 * 60 * 5); // every 5 minutes
+ window.peers = [];
+ this.createPeer();
+};
+
+IDE_Morph.prototype.createPeer = function() {
+ var myself = this;
+ this.peer = new Peer(this.peerId, {
+ host: 'snapmesh.herokuapp.com',
+ port: 443,
+ secure: true,
+ path: '/'
+ });
+
+ this.peer.on('open', function (id) {
+ myself.peerId = id;
+ });
+ this.peer.on('disconnected', function () {
+ myself.peer.destroy();
+ myself.createPeer();
+ });
+ this.peer.on('error', function (err) {
+ console.log(err); // DEBUG
+ });
+
+ this.peer.on('connection', function (connection) {
+ connection.on('open', function () {
+ connection.on('data', function (data) {
+ myself.stage.newPeerMessage(data, connection.peer);
+ });
+ });
+ });
};
IDE_Morph.prototype.openIn = function (world) {
@@ -253,6 +289,7 @@ IDE_Morph.prototype.openIn = function (world) {
// get persistent user data, if any
if (localStorage) {
usr = localStorage['-snap-user'];
+ ghusr = localStorage['-snap-ghuser'];
if (usr) {
usr = SnapCloud.parseResponse(usr)[0];
if (usr) {
@@ -263,6 +300,13 @@ IDE_Morph.prototype.openIn = function (world) {
}
}
}
+ if (ghusr) {
+ ghusr = SnapCloud.parseResponse(ghusr)[0];
+ if (ghusr) {
+ GitHub.login(ghusr.username, ghusr.password, false,
+ function() {}, myself.githubError());
+ }
+ }
}
this.buildPanes();
@@ -280,6 +324,16 @@ IDE_Morph.prototype.openIn = function (world) {
}, 2000);
};
+ GitHub.message = function (string) {
+ var m = new MenuMorph(null, string),
+ intervalHandle;
+ m.popUpCenteredInWorld(world);
+ intervalHandle = setInterval(function () {
+ m.destroy();
+ clearInterval(intervalHandle);
+ }, 2000);
+ };
+
// prevent non-DialogBoxMorphs from being dropped
// onto the World in user-mode
world.reactToDropOf = function (morph) {
@@ -304,7 +358,8 @@ IDE_Morph.prototype.openIn = function (world) {
}
throw new Error('unable to retrieve ' + url);
} catch (err) {
- return;
+ myself.showMessage('unable to retrieve project');
+ return '';
}
}
@@ -391,6 +446,41 @@ IDE_Morph.prototype.openIn = function (world) {
},
this.cloudError()
);
+ } else if (location.hash.substr(0, 8) === '#github:') {
+ this.shield = new Morph();
+ this.shield.color = this.color;
+ this.shield.setExtent(this.parent.extent());
+ this.parent.add(this.shield);
+ myself.showMessage('Fetching project\nfrom GitHub...');
+
+ dict = SnapCloud.parseDict(location.hash.substr(8));
+
+ GitHub.getProject(
+ dict.Username,
+ dict.projectName,
+ function (code, pcSha) {
+ var msg;
+ myself.nextSteps([
+ function () {
+ msg = myself.showMessage('Opening GitHub project...');
+ },
+ function () {
+ myself.parentCommitSha = pcSha;
+ myself.lastCommit = code;
+ myself.rawOpenCloudDataString(code);
+ myself.hasChangedMedia = true;
+ },
+ function () {
+ myself.shield.destroy();
+ myself.shield = null;
+ msg.destroy();
+ myself.toggleAppMode(true);
+ myself.runScripts();
+ }
+ ]);
+ },
+ this.githubError()
+ );
} else if (location.hash.substr(0, 7) === '#cloud:') {
this.shield = new Morph();
this.shield.alpha = 0;
@@ -515,6 +605,7 @@ IDE_Morph.prototype.createControlBar = function () {
stopButton,
pauseButton,
startButton,
+ muteSoundsButton,
projectButton,
settingsButton,
stageSizeButton,
@@ -604,6 +695,38 @@ IDE_Morph.prototype.createControlBar = function () {
this.controlBar.add(appModeButton);
this.controlBar.appModeButton = appModeButton; // for refreshing
+ //muteSoundsButton
+ button = new ToggleButtonMorph(
+ null, //colors,
+ myself, // the IDE is the target
+ 'toggleMuteSounds',
+ [
+ new SymbolMorph('mutedSounds', 14),
+ new SymbolMorph('unmutedSounds', 14)
+ ],
+ function () { // query
+ return myself.isMuted;
+ }
+ );
+
+ button.corner = 12;
+ button.color = colors[0];
+ button.highlightColor = colors[1];
+ button.pressColor = colors[2];
+ button.labelMinExtent = new Point(36, 18);
+ button.padding = 0;
+ button.labelShadowOffset = new Point(-1, -1);
+ button.labelShadowColor = colors[1];
+ button.labelColor = this.buttonLabelColor;
+ button.contrast = this.buttonContrast;
+ button.drawNew();
+ // button.hint = 'sounds\nmuted & unmuted';
+ button.fixLayout();
+ button.refresh();
+ muteSoundsButton = button;
+ this.controlBar.add(muteSoundsButton);
+ this.controlBar.muteSoundsButton = button; // for refreshing
+
// stopButton
button = new PushButtonMorph(
this,
@@ -768,7 +891,7 @@ IDE_Morph.prototype.createControlBar = function () {
myself.right() - StageMorph.prototype.dimensions.x *
(myself.isSmallStage ? myself.stageRatio : 1)
);
- [stageSizeButton, appModeButton].forEach(
+ [stageSizeButton, appModeButton, muteSoundsButton].forEach(
function (button) {
x += padding;
button.setCenter(myself.controlBar.center());
@@ -1628,7 +1751,12 @@ IDE_Morph.prototype.droppedBinary = function (anArrayBuffer, name) {
myself = this,
suffix = name.substring(name.length - 3);
- if (suffix.toLowerCase() !== 'ypr') {return; }
+ if (suffix.toLowerCase() !== 'ypr') {
+ var zip = new JSZip(anArrayBuffer);
+ myself.droppedText(Snapin8r(zip));
+
+ return;
+ }
function loadYPR(buffer, lbl) {
var reader = new sb.Reader(),
@@ -2039,6 +2167,18 @@ IDE_Morph.prototype.cloudMenu = function () {
'changeCloudPassword'
);
}
+
+ if (!GitHub.username) {
+ menu.addItem(
+ 'Login via GitHub...',
+ 'initializeGitHub'
+ );
+ } else {
+ menu.addItem(
+ localize('Logout') + ' ' + GitHub.username + ' ' + localize('from GitHub'),
+ 'logoutGitHub'
+ );
+ }
if (shiftClicked) {
menu.addLine();
menu.addItem(
@@ -2269,10 +2409,10 @@ IDE_Morph.prototype.settingsMenu = function () {
addPreference(
'Cache Inputs',
function () {
- SyntaxElementMorph.prototype.isCachingInputs =
- !SyntaxElementMorph.prototype.isCachingInputs;
+ BlockMorph.prototype.isCachingInputs =
+ !BlockMorph.prototype.isCachingInputs;
},
- SyntaxElementMorph.prototype.isCachingInputs,
+ BlockMorph.prototype.isCachingInputs,
'uncheck to stop caching\ninputs (for debugging the evaluator)',
'check to cache inputs\nboosts recursion',
true
@@ -2384,6 +2524,9 @@ IDE_Morph.prototype.projectMenu = function () {
menu.addItem('New', 'createNewProject');
menu.addItem('Open...', 'openProjectsBrowser');
menu.addItem('Save', "save");
+ if (GitHub.username) {
+ menu.addItem('Save with commit message', 'commitProjectToGitHub');
+ }
menu.addItem(
'Save to disk',
'saveProjectToDisk',
@@ -2598,7 +2741,7 @@ IDE_Morph.prototype.aboutSnap = function () {
module, btn1, btn2, btn3, btn4, licenseBtn, translatorsBtn,
world = this.world();
- aboutTxt = 'Snap! 4.0\nBuild Your Own Blocks\n\n--- beta ---\n\n'
+ aboutTxt = 'Snap! 4.1 (OOP)\nBuild Your Own Blocks\n\n--- dev ---\n\n'
+ 'Copyright \u24B8 2015 Jens M\u00F6nig and '
+ 'Brian Harvey\n'
+ 'jens@moenig.org, bh@cs.berkeley.edu\n\n'
@@ -2832,8 +2975,10 @@ IDE_Morph.prototype.save = function () {
if (this.projectName) {
if (this.source === 'local') { // as well as 'examples'
this.saveProject(this.projectName);
- } else { // 'cloud'
+ } else if (this.source === 'cloud') { // 'cloud'
this.saveProjectToCloud(this.projectName);
+ } else { // 'github'
+ this.saveProjectToGitHub(this.projectName);
}
} else {
this.saveProjectsBrowser();
@@ -3509,6 +3654,27 @@ IDE_Morph.prototype.toggleStageSize = function (isSmall) {
}
};
+IDE_Morph.prototype.toggleMuteSounds = function (isMuted) {
+ this.isMuted = isNil(isMuted) ? !this.isMuted : isMuted;
+ this.controlBar.muteSoundsButton.refresh();
+
+ /* stage.activeSounds holds all active sounds
+ * a sprite's .activeSounds holds just its own
+ * so you have to use the stage to mute
+ * and the sprite to unmute, because the stage's volume
+ * overrides the sprite's one.
+ */
+
+ if (this.isMuted === false) {
+ this.stage.unmuteAllSounds();
+ this.sprites.asArray().forEach(function (sprt) {
+ sprt.unmuteAllSounds();
+ });
+ } else {
+ this.stage.muteAllSounds();
+ }
+};
+
IDE_Morph.prototype.createNewProject = function () {
var myself = this;
this.confirm(
@@ -3569,6 +3735,12 @@ IDE_Morph.prototype.setLanguage = function (lang, callback) {
IDE_Morph.prototype.reflectLanguage = function (lang, callback) {
var projectData;
SnapTranslator.language = lang;
+ this.world().children.forEach(function (morph) {
+ if (morph instanceof BlockEditorMorph) {
+ morph.updateDefinition(); // save custom blocks
+ // otherwise, initBlocks() will reset the definition
+ }
+ });
if (!this.loadNewProject) {
if (Process.prototype.isCatchingErrors) {
try {
@@ -3801,6 +3973,48 @@ IDE_Morph.prototype.initializeCloud = function () {
);
};
+IDE_Morph.prototype.initializeGitHub = function () {
+ var myself = this,
+ world = this.world();
+ new DialogBoxMorph(
+ null,
+ function (user) {
+ var pw = user.password,
+ str;
+ GitHub.login(
+ user.username,
+ pw,
+ true,
+ function () {
+ if (user.choice) {
+ str = SnapCloud.encodeDict(
+ {
+ username: user.username,
+ password: pw
+ }
+ );
+ localStorage['-snap-ghuser'] = str;
+ }
+ myself.source = 'github';
+ myself.showMessage('now connected.', 2);
+ },
+ myself.githubError()
+ );
+ }
+ ).withKey('cloudlogin').promptCredentials(
+ 'Sign in with your GitHub account',
+ 'login',
+ null,
+ null,
+ null,
+ null,
+ 'stay signed in on this computer\nuntil logging out',
+ world,
+ myself.cloudIcon(),
+ myself.cloudMsg
+ );
+};
+
IDE_Morph.prototype.createCloudAccount = function () {
var myself = this,
world = this.world();
@@ -3928,6 +4142,19 @@ IDE_Morph.prototype.logout = function () {
);
};
+IDE_Morph.prototype.logoutGitHub = function () {
+ var myself = this;
+ delete localStorage['-snap-ghuser'];
+ GitHub.logout(
+ function () {
+ myself.showMessage('disconnected.', 2);
+ },
+ function () {
+ myself.showMessage('disconnected.', 2);
+ }
+ );
+};
+
IDE_Morph.prototype.saveProjectToCloud = function (name) {
var myself = this;
if (name) {
@@ -3941,6 +4168,90 @@ IDE_Morph.prototype.saveProjectToCloud = function (name) {
}
};
+IDE_Morph.prototype.saveProjectToGitHub = function (name, commitMessage) {
+ var myself = this;
+ if (name) {
+ this.showMessage('Comitting project\nto GitHub...');
+ this.setProjectName(name);
+ GitHub.saveProject(
+ commitMessage,
+ this.parentCommitSha,
+ this.lastCommit,
+ this,
+ function () {
+ GitHub.getProject(
+ GitHub.username,
+ name,
+ function (code, pcSha) {
+ myself.source = 'github';
+ myself.parentCommitSha = pcSha;
+ myself.lastCommit = code;
+ myself.droppedText(code);
+ },
+ myself.githubError()
+ );
+ },
+ this.githubError()
+ );
+ }
+};
+
+IDE_Morph.prototype.commitProjectToGitHub = function () {
+ var dialog = new DialogBoxMorph().withKey('commitMessage'),
+ frame = new ScrollFrameMorph(),
+ text = new TextMorph(''),
+ ok = dialog.ok,
+ myself = this,
+ size = 120,
+ world = this.world();
+
+ frame.padding = 6;
+ frame.setWidth(size);
+ frame.acceptsDrops = false;
+ frame.contents.acceptsDrops = false;
+
+ text.setWidth(size - frame.padding * 2);
+ text.setPosition(frame.topLeft().add(frame.padding));
+ text.enableSelecting();
+ text.isEditable = true;
+
+ frame.setHeight(size);
+ frame.fixLayout = nop;
+ frame.edge = InputFieldMorph.prototype.edge;
+ frame.fontSize = InputFieldMorph.prototype.fontSize;
+ frame.typeInPadding = InputFieldMorph.prototype.typeInPadding;
+ frame.contrast = InputFieldMorph.prototype.contrast;
+ frame.drawNew = InputFieldMorph.prototype.drawNew;
+ frame.drawRectBorder = InputFieldMorph.prototype.drawRectBorder;
+
+ frame.addContents(text);
+ text.drawNew();
+
+ dialog.ok = function () {
+ myself.saveProjectToGitHub(
+ myself.projectName,
+ text.text
+ );
+
+ ok.call(this);
+ };
+
+ dialog.justDropped = function () {
+ text.edit();
+ };
+
+ dialog.labelString = 'Commit message';
+ dialog.createLabel();
+ dialog.addBody(frame);
+ frame.drawNew();
+ dialog.addButton('ok', 'OK');
+ dialog.addButton('cancel', 'Cancel');
+ dialog.fixLayout();
+ dialog.drawNew();
+ dialog.popUp(world);
+ dialog.setCenter(world.center());
+ text.edit();
+};
IDE_Morph.prototype.exportProjectMedia = function (name) {
var menu, media;
this.serializer.isCollectingMedia = true;
@@ -4142,6 +4453,42 @@ IDE_Morph.prototype.cloudError = function () {
};
};
+IDE_Morph.prototype.githubError = function () {
+ var myself = this;
+
+ function getURL(url) {
+ try {
+ var request = new XMLHttpRequest();
+ request.open('GET', url, false);
+ request.send();
+ if (request.status === 200) {
+ return request.responseText;
+ }
+ return null;
+ } catch (err) {
+ return null;
+ }
+ }
+
+ return function (responseText, url) {
+ var response = responseText;
+ if (myself.shield) {
+ myself.shield.destroy();
+ myself.shield = null;
+ }
+ if (response.length > 50) {
+ response = response.substring(0, 50) + '...';
+ }
+ new DialogBoxMorph().inform(
+ 'GitHub',
+ (url ? url + '\n' : '')
+ + response,
+ myself.world(),
+ myself.cloudIcon(null, new Color(180, 0, 0))
+ );
+ };
+};
+
IDE_Morph.prototype.cloudIcon = function (height, color) {
var clr = color || DialogBoxMorph.prototype.titleBarColor,
isFlat = MorphicPreferences.isFlat,
@@ -4272,7 +4619,7 @@ ProjectDialogMorph.prototype.init = function (ide, task) {
// additional properties:
this.ide = ide;
this.task = task || 'open'; // String describing what do do (open, save)
- this.source = ide.source || 'local'; // or 'cloud' or 'examples'
+ this.source = ide.source || 'local'; // or 'cloud' or 'github' or 'examples'
this.projectList = []; // [{name: , thumb: , notes:}]
this.handle = null;
@@ -4332,6 +4679,7 @@ ProjectDialogMorph.prototype.buildContents = function () {
}
this.addSourceButton('cloud', localize('Cloud'), 'cloud');
+ this.addSourceButton('github', localize('GitHub'), 'github');
this.addSourceButton('local', localize('Browser'), 'storage');
if (this.task === 'open') {
this.addSourceButton('examples', localize('Examples'), 'poster');
@@ -4580,6 +4928,20 @@ ProjectDialogMorph.prototype.setSource = function (source) {
}
);
return;
+ case 'github':
+ msg = myself.ide.showMessage('Updating\nproject list...');
+ this.projectList = [];
+ GitHub.getProjectList(
+ function (projectList) {
+ myself.installGitHubProjectList(projectList);
+ msg.destroy();
+ },
+ function (err, lbl) {
+ msg.destroy();
+ myself.ide.githubError().call(null, err, lbl);
+ }
+ );
+ return;
case 'examples':
this.projectList = this.getExamplesProjectList();
break;
@@ -4632,7 +4994,7 @@ ProjectDialogMorph.prototype.setSource = function (source) {
}
myself.edit();
};
- } else { // 'examples', 'cloud' is initialized elsewhere
+ } else { // 'examples', 'cloud' and 'github' is initialized elsewhere
this.listField.action = function (item) {
var src, xml;
if (item === undefined) {return; }
@@ -4798,6 +5160,69 @@ ProjectDialogMorph.prototype.installCloudProjectList = function (pl) {
}
};
+ProjectDialogMorph.prototype.installGitHubProjectList = function (pl) {
+ var myself = this;
+ this.projectList = pl || [];
+ this.projectList.sort(function (x, y) {
+ return x.ProjectName < y.ProjectName ? -1 : 1;
+ });
+
+ this.listField.destroy();
+ this.listField = new ListMorph(
+ this.projectList,
+ this.projectList.length > 0 ?
+ function (element) {
+ return element.ProjectName;
+ } : null,
+ [],
+ function () {myself.ok(); }
+ );
+ this.fixListFieldItemColors();
+ this.listField.fixLayout = nop;
+ this.listField.edge = InputFieldMorph.prototype.edge;
+ this.listField.fontSize = InputFieldMorph.prototype.fontSize;
+ this.listField.typeInPadding = InputFieldMorph.prototype.typeInPadding;
+ this.listField.contrast = InputFieldMorph.prototype.contrast;
+ this.listField.drawNew = InputFieldMorph.prototype.drawNew;
+ this.listField.drawRectBorder = InputFieldMorph.prototype.drawRectBorder;
+
+ this.listField.action = function (item) {
+ if (item === undefined) {return; }
+ if (myself.nameField) {
+ myself.nameField.setContents(item.ProjectName || '');
+ }
+ if (myself.task === 'open') {
+ myself.notesText.text = item.Notes || '';
+ myself.notesText.drawNew();
+ myself.notesField.contents.adjustBounds();
+ myself.preview.texture = item.Thumbnail || null;
+ myself.preview.cachedTexture = null;
+ myself.preview.drawNew();
+ (new SpeechBubbleMorph(new TextMorph(
+ localize('last changed') + '\n' + item.Updated,
+ null,
+ null,
+ null,
+ null,
+ 'center'
+ ))).popUp(
+ myself.world(),
+ myself.preview.rightCenter().add(new Point(2, 0))
+ );
+ }
+ myself.buttons.fixLayout();
+ myself.fixLayout();
+ myself.edit();
+ };
+ this.body.add(this.listField);
+ this.deleteButton.show();
+ this.buttons.fixLayout();
+ this.fixLayout();
+ if (this.task === 'open') {
+ this.clearDetails();
+ }
+};
+
ProjectDialogMorph.prototype.clearDetails = function () {
this.notesText.text = '';
this.notesText.drawNew();
@@ -4814,6 +5239,8 @@ ProjectDialogMorph.prototype.openProject = function () {
this.ide.source = this.source;
if (this.source === 'cloud') {
this.openCloudProject(proj);
+ } else if (this.source === 'github') {
+ this.openGitHubProject(proj);
} else if (this.source === 'examples') {
src = this.ide.getURL(
'http://snap.berkeley.edu/snapsource/Examples/' +
@@ -4839,6 +5266,23 @@ ProjectDialogMorph.prototype.openCloudProject = function (project) {
]);
};
+ProjectDialogMorph.prototype.openGitHubProject = function (project, user) {
+ var myself = this;
+
+ if (user == null) { // jshint ignore:line
+ user = GitHub.username;
+ }
+
+ myself.ide.nextSteps([
+ function () {
+ myself.ide.showMessage('Fetching project\nfrom GitHub...');
+ },
+ function () {
+ myself.rawOpenGitHubProject(project, user);
+ }
+ ]);
+};
+
ProjectDialogMorph.prototype.rawOpenCloudProject = function (proj) {
var myself = this;
SnapCloud.reconnect(
@@ -4865,6 +5309,21 @@ ProjectDialogMorph.prototype.rawOpenCloudProject = function (proj) {
this.destroy();
};
+ProjectDialogMorph.prototype.rawOpenGitHubProject = function (proj, user) {
+ var myself = this;
+ GitHub.getProject(
+ user,
+ proj.ProjectName,
+ function (code, pcSha) {
+ myself.ide.source = 'github';
+ myself.ide.parentCommitSha = pcSha;
+ myself.ide.lastCommit = code;
+ myself.ide.droppedText(code);
+ },
+ myself.ide.githubError()
+ );
+ this.destroy();
+};
ProjectDialogMorph.prototype.saveProject = function () {
var name = this.nameField.contents().text.text,
notes = this.notesText.text,
@@ -4891,6 +5350,25 @@ ProjectDialogMorph.prototype.saveProject = function () {
this.ide.setProjectName(name);
myself.saveCloudProject();
}
+ } else if (this.source === 'github') {
+ if (detect(
+ this.projectList,
+ function (item) {return item.ProjectName === name; }
+ )) {
+ this.ide.confirm(
+ localize(
+ 'Are you sure you want to replace'
+ ) + '\n"' + name + '"?',
+ 'Replace Project',
+ function () {
+ myself.ide.setProjectName(name);
+ myself.saveGitHubProject();
+ }
+ );
+ } else {
+ this.ide.setProjectName(name);
+ myself.saveGitHubProject();
+ }
} else { // 'local'
if (detect(
this.projectList,
@@ -4932,13 +5410,32 @@ ProjectDialogMorph.prototype.saveCloudProject = function () {
this.destroy();
};
+ProjectDialogMorph.prototype.saveGitHubProject = function () {
+ var myself = this;
+ this.ide.showMessage('Committing project\nto GitHub...');
+ GitHub.saveProject(
+ null,
+ this.ide.parentCommitSha,
+ this.ide.lastCommit,
+ this.ide,
+ function () {
+ myself.ide.source = 'github';
+ myself.ide.showMessage('saved.', 2);
+ },
+ this.ide.githubError()
+ );
+ this.destroy();
+};
+
ProjectDialogMorph.prototype.deleteProject = function () {
var myself = this,
proj,
idx,
name;
- if (this.source === 'cloud') {
+ if (this.source === 'github') {
+ // TODO: not implemented
+ } else if (this.source === 'cloud') {
proj = this.listField.selected;
if (proj) {
this.ide.confirm(
@@ -5182,7 +5679,7 @@ function SpriteIconMorph(aSprite, aTemplate) {
}
SpriteIconMorph.prototype.init = function (aSprite, aTemplate) {
- var colors, action, query, myself = this;
+ var colors, action, query, hover, myself = this;
if (!aTemplate) {
colors = [
@@ -5212,6 +5709,11 @@ SpriteIconMorph.prototype.init = function (aSprite, aTemplate) {
return false;
};
+ hover = function () {
+ if (!aSprite.exemplar) {return null; }
+ return (localize('parent' + ':\n' + aSprite.exemplar.name));
+ };
+
// additional properties:
this.object = aSprite || new SpriteMorph(); // mandatory, actually
this.version = this.object.version;
@@ -5227,7 +5729,7 @@ SpriteIconMorph.prototype.init = function (aSprite, aTemplate) {
this.object.name, // label string
query, // predicate/selector
null, // environment
- null, // hint
+ hover, // hint
aTemplate // optional, for cached background images
);
@@ -5398,6 +5900,7 @@ SpriteIconMorph.prototype.userMenu = function () {
menu.addItem("duplicate", 'duplicateSprite');
menu.addItem("delete", 'removeSprite');
menu.addLine();
+ menu.addItem("parent...", 'chooseExemplar');
if (this.object.anchor) {
menu.addItem(
localize('detach from') + ' ' + this.object.anchor.name,
@@ -5432,6 +5935,10 @@ SpriteIconMorph.prototype.exportSprite = function () {
this.object.exportSprite();
};
+SpriteIconMorph.prototype.chooseExemplar = function () {
+ this.object.chooseExemplar();
+};
+
SpriteIconMorph.prototype.showSpriteOnStage = function () {
this.object.showOnStage();
};
diff --git a/history.txt b/history.txt
index 0f92071..fda02a3 100755
--- a/history.txt
+++ b/history.txt
@@ -2462,3 +2462,29 @@ ______
150302
------
* BYOB: fixed #730
+
+150306
+------
+* Blocks: fixed #736
+
+150309
+------
+* Blocks: fixed #738
+* GUI, Blocks: Only enable input caching for blocks
+
+150315
+------
+* Store: fixed #743
+* GUI, html: switch from beta to release candidate
+
+150321
+------
+* OOP: Prototypal inheritance of sprite-local variables
+
+150323
+------
+* OOP: Objects, Blocks, Threads - tweaks
+
+150323
+------
+* OOP: Morphic, Objects, Store - tweak inherited variable watcher slider deserialization
diff --git a/lang-de.js b/lang-de.js
index 3040cc2..8079d15 100644
--- a/lang-de.js
+++ b/lang-de.js
@@ -625,6 +625,19 @@ SnapTranslator.dict.de = {
'replace item %idx of %l with %s':
'ersetze Element %idx in %l durch %s',
+ // peer to peer communication
+ 'when I receive %upvar from %upvar':
+ 'Wenn ich %upvar von %upvar empfange',
+ 'peer':
+ 'Peer',
+ 'send %s to %s':
+ 'sende %s an %s',
+ 'my peer id':
+ 'meine Peer-ID',
+ 'peers online':
+ 'verbundene Peers',
+
+
// other
'Make a block':
'Neuer Block',
diff --git a/morphic.js b/morphic.js
index 4a16597..e291ec3 100644
--- a/morphic.js
+++ b/morphic.js
@@ -8,7 +8,7 @@
written by Jens Mönig
jens@moenig.org
- Copyright (C) 2014 by Jens Mönig
+ Copyright (C) 2015 by Jens Mönig
This file is part of Snap!.
@@ -527,6 +527,12 @@
a duplicate of the template whose "isDraggable" flag is true and
whose "isTemplate" flag is false, in other words: a non-template.
+ When creating a copy from a template, the copy's
+
+ reactToTemplateCopy
+
+ is invoked, if it is present.
+
Dragging is indicated by adding a drop shadow to the morph in hand.
If a morph follows the hand without displaying a drop shadow it is
merely being moved about without changing its parent (owner morph),
@@ -1020,9 +1026,10 @@
programming hero.
I have originally written morphic.js in Florian Balmer's Notepad2
- editor for Windows and later switched to Apple's Dashcode. I've also
- come to depend on both Douglas Crockford's JSLint, Mozilla's Firebug
- and Google's Chrome to get it right.
+ editor for Windows, later switched to Apple's Dashcode and later
+ still to Apple's Xcode. I've also come to depend on both Douglas
+ Crockford's JSLint, Mozilla's Firebug and Google's Chrome to get
+ it right.
IX. contributors
@@ -1041,7 +1048,7 @@
/*global window, HTMLCanvasElement, getMinimumFontHeight, FileReader, Audio,
FileList, getBlurredShadowSupport*/
-var morphicVersion = '2014-December-05';
+var morphicVersion = '2015-March-24';
var modules = {}; // keep track of additional loaded modules
var useBlurredShadows = getBlurredShadowSupport(); // check for Chrome-bug
@@ -5926,7 +5933,7 @@ SliderMorph.prototype.userSetStart = function (num) {
this.start = Math.max(num, this.stop);
};
-SliderMorph.prototype.setStart = function (num) {
+SliderMorph.prototype.setStart = function (num, noUpdate) {
// for context menu demo purposes
var newStart;
if (typeof num === 'number') {
@@ -5944,12 +5951,12 @@ SliderMorph.prototype.setStart = function (num) {
}
}
this.value = Math.max(this.value, this.start);
- this.updateTarget();
+ if (!noUpdate) {this.updateTarget(); }
this.drawNew();
this.changed();
};
-SliderMorph.prototype.setStop = function (num) {
+SliderMorph.prototype.setStop = function (num, noUpdate) {
// for context menu demo purposes
var newStop;
if (typeof num === 'number') {
@@ -5961,12 +5968,12 @@ SliderMorph.prototype.setStop = function (num) {
}
}
this.value = Math.min(this.value, this.stop);
- this.updateTarget();
+ if (!noUpdate) {this.updateTarget(); }
this.drawNew();
this.changed();
};
-SliderMorph.prototype.setSize = function (num) {
+SliderMorph.prototype.setSize = function (num, noUpdate) {
// for context menu demo purposes
var newSize;
if (typeof num === 'number') {
@@ -5984,7 +5991,7 @@ SliderMorph.prototype.setSize = function (num) {
}
}
this.value = Math.min(this.value, this.stop - this.size);
- this.updateTarget();
+ if (!noUpdate) {this.updateTarget(); }
this.drawNew();
this.changed();
};
@@ -8060,7 +8067,7 @@ TriggerMorph.prototype.init = function (
this.environment = environment || null;
this.labelString = labelString || null;
this.label = null;
- this.hint = hint || null;
+ this.hint = hint || null; // null, String, or Function
this.fontSize = fontSize || MorphicPreferences.menuFontSize;
this.fontStyle = fontStyle || 'sans-serif';
this.highlightColor = new Color(192, 192, 192);
@@ -8209,10 +8216,11 @@ TriggerMorph.prototype.triggerDoubleClick = function () {
// TriggerMorph events:
TriggerMorph.prototype.mouseEnter = function () {
+ var contents = this.hint instanceof Function ? this.hint() : this.hint;
this.image = this.highlightImage;
this.changed();
- if (this.hint) {
- this.bubbleHelp(this.hint);
+ if (contents) {
+ this.bubbleHelp(contents);
}
};
@@ -9683,6 +9691,9 @@ HandMorph.prototype.processMouseMove = function (event) {
morph = this.morphToGrab.fullCopy();
morph.isTemplate = false;
morph.isDraggable = true;
+ if (morph.reactToTemplateCopy) {
+ morph.reactToTemplateCopy();
+ }
this.grab(morph);
this.grabOrigin = this.morphToGrab.situation();
}
diff --git a/objects.js b/objects.js
index a952515..96fda2e 100644
--- a/objects.js
+++ b/objects.js
@@ -125,7 +125,7 @@ PrototypeHatBlockMorph*/
// Global stuff ////////////////////////////////////////////////////////
-modules.objects = '2015-February-28';
+modules.objects = '2015-March-24';
var SpriteMorph;
var StageMorph;
@@ -464,6 +464,23 @@ SpriteMorph.prototype.initBlocks = function () {
category: 'sound',
spec: 'stop all sounds'
},
+ doSetVolume: {
+ type: 'command',
+ category: 'sound',
+ spec: 'set volume to %n %',
+ defaults: [100]
+ },
+ doChangeVolume: {
+ type: 'command',
+ category: 'sound',
+ spec: 'change volume by %n',
+ defaults: [-10]
+ },
+ reportVolume: {
+ type: 'reporter',
+ category: 'sound',
+ spec: 'volume'
+ },
doRest: {
type: 'command',
category: 'sound',
@@ -879,6 +896,11 @@ SpriteMorph.prototype.initBlocks = function () {
category: 'sensing',
spec: 'key %key pressed?'
},
+ getKeysPressed: {
+ type: 'reporter',
+ category: 'sensing',
+ spec: 'keys pressed'
+ },
reportDistanceTo: {
type: 'reporter',
category: 'sensing',
@@ -1160,6 +1182,13 @@ SpriteMorph.prototype.initBlocks = function () {
spec: 'script variables %scriptVars'
},
+ // inheritance - experimental
+ doDeleteAttr: {
+ type: 'command',
+ category: 'variables',
+ spec: 'delete %shd'
+ },
+
// Lists
reportNewList: {
type: 'reporter',
@@ -1233,6 +1262,29 @@ SpriteMorph.prototype.initBlocks = function () {
defaults: [localize('each item')]
},
+ // peer to peer communication
+ receivePeerMessage: {
+ type: 'hat',
+ category: 'other',
+ spec: 'when I receive %upvar from %upvar',
+ defaults: [localize('message'), localize('peer')]
+ },
+ sendPeerMessage: {
+ type: 'command',
+ category: 'other',
+ spec: 'send %s to %s'
+ },
+ reportPeerId: {
+ type: 'reporter',
+ category: 'other',
+ spec: 'my peer id'
+ },
+ reportPeerList: {
+ type: 'reporter',
+ category: 'other',
+ spec: 'peers online'
+ },
+
// Code mapping - experimental
doMapCodeOrHeader: { // experimental
type: 'command',
@@ -1379,6 +1431,8 @@ SpriteMorph.prototype.init = function (globals) {
this.version = Date.now(); // for observer optimization
this.isClone = false; // indicate a "temporary" Scratch-style clone
this.cloneOriginName = '';
+ this.volume = 100;
+ this.activeSounds = [];
// sprite nesting properties
this.parts = []; // not serialized, only anchor (name)
@@ -1405,11 +1459,13 @@ SpriteMorph.prototype.init = function (globals) {
'confetti': 0
};
+ // sprite inheritance
+ this.exemplar = null;
+
SpriteMorph.uber.init.call(this);
this.isDraggable = true;
this.isDown = false;
-
this.heading = 90;
this.changed();
this.drawNew();
@@ -1482,8 +1538,11 @@ SpriteMorph.prototype.appearIn = function (ide) {
// SpriteMorph versioning
SpriteMorph.prototype.setName = function (string) {
- this.name = string || this.name;
- this.version = Date.now();
+ if (string != 'mouse-pointer' && string != 'pen trails'
+ && string != 'edge') { // used by system
+ this.name = string || this.name;
+ this.version = Date.now();
+ }
};
// SpriteMorph rendering
@@ -1699,7 +1758,8 @@ SpriteMorph.prototype.variableBlock = function (varName) {
SpriteMorph.prototype.blockTemplates = function (category) {
var blocks = [], myself = this, varNames, button,
- cat = category || 'motion', txt;
+ cat = category || 'motion', txt,
+ inheritedVars = this.inheritedVariableNames();
function block(selector) {
if (StageMorph.prototype.hiddenPrimitives[selector]) {
@@ -1714,6 +1774,9 @@ SpriteMorph.prototype.blockTemplates = function (category) {
var newBlock = SpriteMorph.prototype.variableBlock(varName);
newBlock.isDraggable = false;
newBlock.isTemplate = true;
+ if (contains(inheritedVars, varName)) {
+ newBlock.ghost();
+ }
return newBlock;
}
@@ -1762,15 +1825,18 @@ SpriteMorph.prototype.blockTemplates = function (category) {
}
function addVar(pair) {
+ var ide;
if (pair) {
- if (myself.variables.silentFind(pair[0])) {
+ if (myself.isVariableNameInUse(pair[0], pair[1])) {
myself.inform('that name is already in use');
} else {
+ ide = myself.parentThatIsA(IDE_Morph);
myself.addVariable(pair[0], pair[1]);
- myself.toggleVariableWatcher(pair[0], pair[1]);
- myself.blocksCache[cat] = null;
- myself.paletteCache[cat] = null;
- myself.parentThatIsA(IDE_Morph).refreshPalette();
+ if (!myself.showingVariableWatcher(pair[0])) {
+ myself.toggleVariableWatcher(pair[0], pair[1]);
+ }
+ ide.flushBlocksCache('variables'); // b/c of inheritance
+ ide.refreshPalette();
}
}
}
@@ -1858,6 +1924,11 @@ SpriteMorph.prototype.blockTemplates = function (category) {
blocks.push(block('doPlaySoundUntilDone'));
blocks.push(block('doStopAllSounds'));
blocks.push('-');
+ blocks.push(block('doSetVolume'));
+ blocks.push(block('doChangeVolume'));
+ blocks.push(watcherToggle('reportVolume'));
+ blocks.push(block('reportVolume'));
+ blocks.push('-');
blocks.push(block('doRest'));
blocks.push('-');
blocks.push(block('doPlayNote'));
@@ -1976,6 +2047,7 @@ SpriteMorph.prototype.blockTemplates = function (category) {
blocks.push(block('reportMouseDown'));
blocks.push('-');
blocks.push(block('reportKeyPressed'));
+ blocks.push(block('getKeysPressed'));
blocks.push('-');
blocks.push(block('reportDistanceTo'));
blocks.push('-');
@@ -2099,7 +2171,7 @@ SpriteMorph.prototype.blockTemplates = function (category) {
button.showHelp = BlockMorph.prototype.showHelp;
blocks.push(button);
- if (this.variables.allNames().length > 0) {
+ if (this.deletableVariableNames().length > 0) {
button = new PushButtonMorph(
null,
function () {
@@ -2108,7 +2180,7 @@ SpriteMorph.prototype.blockTemplates = function (category) {
null,
myself
);
- myself.variables.allNames().forEach(function (name) {
+ myself.deletableVariableNames().forEach(function (name) {
menu.addItem(name, name);
});
menu.popUpAtHand(myself.world());
@@ -2138,6 +2210,13 @@ SpriteMorph.prototype.blockTemplates = function (category) {
blocks.push(block('doHideVar'));
blocks.push(block('doDeclareVariables'));
+ // inheritance:
+
+ blocks.push('-');
+ blocks.push(block('doDeleteAttr'));
+
+ ///////////////////////////////
+
blocks.push('=');
blocks.push(block('reportNewList'));
@@ -2174,6 +2253,13 @@ SpriteMorph.prototype.blockTemplates = function (category) {
blocks.push('=');
+ blocks.push(block('receivePeerMessage'));
+ blocks.push(block('sendPeerMessage'));
+ blocks.push(block('reportPeerId'));
+ blocks.push(block('reportPeerList'));
+
+ blocks.push('=');
+
if (StageMorph.prototype.enableCodeMapping) {
blocks.push(block('doMapCodeOrHeader'));
blocks.push(block('doMapStringCode'));
@@ -2562,7 +2648,7 @@ SpriteMorph.prototype.searchBlocks = function () {
SpriteMorph.prototype.addVariable = function (name, isGlobal) {
var ide = this.parentThatIsA(IDE_Morph);
if (isGlobal) {
- this.variables.parentFrame.addVar(name);
+ this.globalVariables().addVar(name);
if (ide) {
ide.flushBlocksCache('variables');
}
@@ -2574,7 +2660,10 @@ SpriteMorph.prototype.addVariable = function (name, isGlobal) {
SpriteMorph.prototype.deleteVariable = function (varName) {
var ide = this.parentThatIsA(IDE_Morph);
- this.deleteVariableWatcher(varName);
+ if (!contains(this.inheritedVariableNames(true), varName)) {
+ // check only shadowed variables
+ this.deleteVariableWatcher(varName);
+ }
this.variables.deleteVar(varName);
if (ide) {
ide.flushBlocksCache('variables'); // b/c the var could be global
@@ -2691,7 +2780,8 @@ SpriteMorph.prototype.reportCostumes = function () {
// SpriteMorph sound management
SpriteMorph.prototype.addSound = function (audio, name) {
- this.sounds.add(new Sound(audio, name));
+ var volume = this.volume;
+ this.sounds.add(new Sound(audio, name, volume));
};
SpriteMorph.prototype.playSound = function (name) {
@@ -2702,7 +2792,18 @@ SpriteMorph.prototype.playSound = function (name) {
),
active;
if (sound) {
+ sound.volume = this.volume;
active = sound.play();
+
+ if (stage.muted === true) {
+ active.volume = 0;
+ }
+
+ this.activeSounds.push(active);
+ this.activeSounds = this.activeSounds.filter(function (aud) {
+ return !aud.ended && !aud.terminated;
+ });
+
if (stage) {
stage.activeSounds.push(active);
stage.activeSounds = stage.activeSounds.filter(function (aud) {
@@ -2713,6 +2814,34 @@ SpriteMorph.prototype.playSound = function (name) {
}
};
+SpriteMorph.prototype.doSetVolume = function (val) {
+ var myself = this;
+ myself.volume = Math.min(Math.max(0, val), 100);
+
+ if (myself.parentThatIsA(StageMorph).muted === true) {
+ return;
+ }
+
+ myself.activeSounds.forEach(function (snd) {
+ snd.volume = myself.volume / 100; // 'audio' objects
+ });
+};
+
+SpriteMorph.prototype.doChangeVolume = function (val) {
+ this.doSetVolume(this.volume + val);
+};
+
+SpriteMorph.prototype.reportVolume = function () {
+ return this.volume;
+}
+
+SpriteMorph.prototype.unmuteAllSounds = function () {
+ var stage = this.parentThatIsA(StageMorph);
+ stage.muted = false;
+
+ this.doSetVolume(this.volume);
+};
+
SpriteMorph.prototype.reportSounds = function () {
return this.sounds;
};
@@ -3365,9 +3494,7 @@ Morph.prototype.setPosition = function (aPoint, justMe) {
// override the inherited default to make sure my parts follow
// unless it's justMe
var delta = aPoint.subtract(this.topLeft());
- if ((delta.x !== 0) || (delta.y !== 0)) {
- this.moveBy(delta, justMe);
- }
+ this.moveBy(delta, justMe);
};
SpriteMorph.prototype.forward = function (steps) {
@@ -3588,6 +3715,9 @@ SpriteMorph.prototype.allHatBlocksFor = function (message) {
if (morph.selector === 'receiveOnClone') {
return message === '__clone__init__';
}
+ if (morph.selector === 'receivePeerMessage') {
+ return message === '__peer__message__';
+ }
}
return false;
});
@@ -3597,7 +3727,19 @@ SpriteMorph.prototype.allHatBlocksForKey = function (key) {
return this.scripts.children.filter(function (morph) {
if (morph.selector) {
if (morph.selector === 'receiveKey') {
- return morph.inputs()[0].evaluate()[0] === key;
+ var selectedOption = morph.inputs()[0].evaluate()[0];
+
+ if (selectedOption === 'any key') {
+ return true;
+ }
+ if (selectedOption === 'number key' &&
+ (key >= '0' && key <= '9')) {
+ return true;
+ }
+ if (selectedOption === key) {
+ return true;
+ }
+ return false;
}
}
return false;
@@ -3666,6 +3808,16 @@ SpriteMorph.prototype.getTempo = function () {
return 0;
};
+// SpriteMorph last key
+
+SpriteMorph.prototype.getKeysPressed = function () {
+ var stage = this.parentThatIsA(StageMorph);
+ if (stage) {
+ return stage.getKeysPressed();
+ }
+ return '';
+};
+
// SpriteMorph last message
SpriteMorph.prototype.getLastMessage = function () {
@@ -3714,6 +3866,7 @@ SpriteMorph.prototype.reportThreadCount = function () {
SpriteMorph.prototype.findVariableWatcher = function (varName) {
var stage = this.parentThatIsA(StageMorph),
+ globals = this.globalVariables(),
myself = this;
if (stage === null) {
return null;
@@ -3723,7 +3876,7 @@ SpriteMorph.prototype.findVariableWatcher = function (varName) {
function (morph) {
return morph instanceof WatcherMorph
&& (morph.target === myself.variables
- || morph.target === myself.variables.parentFrame)
+ || morph.target === globals)
&& morph.getter === varName;
}
);
@@ -3731,6 +3884,7 @@ SpriteMorph.prototype.findVariableWatcher = function (varName) {
SpriteMorph.prototype.toggleVariableWatcher = function (varName, isGlobal) {
var stage = this.parentThatIsA(StageMorph),
+ globals = this.globalVariables(),
watcher,
others;
if (stage === null) {
@@ -3750,12 +3904,12 @@ SpriteMorph.prototype.toggleVariableWatcher = function (varName, isGlobal) {
// if no watcher exists, create a new one
if (isNil(isGlobal)) {
- isGlobal = contains(this.variables.parentFrame.names(), varName);
+ isGlobal = contains(globals.names(), varName);
}
watcher = new WatcherMorph(
varName,
this.blockColor.variables,
- isGlobal ? this.variables.parentFrame : this.variables,
+ isGlobal ? globals : this.variables,
varName
);
watcher.setPosition(stage.position().add(10));
@@ -4181,6 +4335,129 @@ SpriteMorph.prototype.restoreLayers = function () {
this.layers = null;
};
+// SpriteMorph inheritance - general
+
+SpriteMorph.prototype.chooseExemplar = function () {
+ var stage = this.parentThatIsA(StageMorph),
+ myself = this,
+ other = stage.children.filter(function (m) {
+ return m instanceof SpriteMorph &&
+ (!contains(m.allExemplars(), myself));
+ }),
+ menu;
+ menu = new MenuMorph(
+ function (aSprite) {myself.setExemplar(aSprite); },
+ localize('current parent') +
+ ':\n' +
+ (this.exemplar ? this.exemplar.name : localize('none'))
+ );
+ other.forEach(function (eachSprite) {
+ menu.addItem(eachSprite.name, eachSprite);
+ });
+ menu.addLine();
+ menu.addItem(localize('none'), null);
+ menu.popUpAtHand(this.world());
+};
+
+SpriteMorph.prototype.setExemplar = function (another) {
+ var ide = this.parentThatIsA(IDE_Morph);
+ this.exemplar = another;
+ if (isNil(another)) {
+ this.variables.parentFrame = (this.globalVariables());
+ } else {
+ this.variables.parentFrame = (another.variables);
+ }
+ if (ide) {
+ ide.flushBlocksCache('variables');
+ ide.refreshPalette();
+ }
+};
+
+SpriteMorph.prototype.allExemplars = function () {
+ // including myself
+ var all = [],
+ current = this;
+ while (!isNil(current)) {
+ all.push(current);
+ current = current.exemplar;
+ }
+ return all;
+};
+
+SpriteMorph.prototype.specimens = function () {
+ // without myself
+ var myself = this;
+ return this.siblings().filter(function (m) {
+ return m instanceof SpriteMorph && (m.exemplar === myself);
+ });
+};
+
+SpriteMorph.prototype.allSpecimens = function () {
+ // without myself
+ var myself = this;
+ return this.siblings().filter(function (m) {
+ return m instanceof SpriteMorph && contains(m.allExemplars(), myself);
+ });
+};
+
+// SpriteMorph inheritance - variables
+
+SpriteMorph.prototype.isVariableNameInUse = function (vName, isGlobal) {
+ if (isGlobal) {
+ return contains(this.variables.allNames(), vName);
+ }
+ if (contains(this.variables.names(), vName)) {return true; }
+ return contains(this.globalVariables().names(), vName);
+};
+
+SpriteMorph.prototype.globalVariables = function () {
+ var current = this.variables.parentFrame;
+ while (current.owner) {
+ current = current.parentFrame;
+ }
+ return current;
+};
+
+SpriteMorph.prototype.shadowVar = function (name, value) {
+ var ide = this.parentThatIsA(IDE_Morph);
+ this.variables.addVar(name, value);
+ if (ide) {
+ ide.flushBlocksCache('variables');
+ ide.refreshPalette();
+ }
+};
+
+SpriteMorph.prototype.inheritedVariableNames = function (shadowedOnly) {
+ var names = [],
+ own = this.variables.names(),
+ current = this.variables.parentFrame;
+
+ function test(each) {
+ return shadowedOnly ? contains(own, each) : !contains(own, each);
+ }
+
+ while (current.owner instanceof SpriteMorph) {
+ names.push.apply(
+ names,
+ current.names().filter(test)
+ );
+ current = current.parentFrame;
+ }
+ return names;
+};
+
+SpriteMorph.prototype.deletableVariableNames = function () {
+ var locals = this.variables.names(),
+ inherited = this.inheritedVariableNames();
+ return locals.concat(
+ this.globalVariables().names().filter(
+ function (each) {
+ return !contains(locals, each) && !contains(inherited, each);
+ }
+ )
+ );
+};
+
// SpriteMorph highlighting
SpriteMorph.prototype.addHighlight = function (oldHighlight) {
@@ -4421,6 +4698,8 @@ StageMorph.prototype.init = function (globals) {
this.version = Date.now(); // for observers
this.isFastTracked = false;
this.cloneCount = 0;
+ this.volume = 100;
+ this.muted = false;
this.timerStart = Date.now();
this.tempo = 60; // bpm
@@ -4464,6 +4743,51 @@ StageMorph.prototype.init = function (globals) {
this.fps = this.frameRate;
};
+StageMorph.prototype.newPeerMessage = function (data, peer) {
+ var ide = this.parentThatIsA(IDE_Morph);
+ if (!ide || !peer) return;
+ var myself = this;
+ var hats = [], model, message;
+
+ try {
+ model = ide.serializer.parse(data);
+ message = ide.serializer.loadValue(model);
+
+ // TODO: If a Context is sent, a new Sprite appears.
+ // This below is just a workaround for one-level rings,
+ // objects should be cleaned recursively.
+ message.receiver = null;
+ message.outerContext = null;
+ } catch (err) {
+ console.log(err); // DEBUG
+ // Ok, it does not seem to be XML. It must be a string then.
+ message = data;
+ }
+
+ // call hat blocks
+ this.children.concat(myself).forEach(function (morph) {
+ if (morph instanceof SpriteMorph
+ || morph instanceof StageMorph) {
+ hats = hats.concat(
+ morph.allHatBlocksFor('__peer__message__'));
+ }
+ });
+ hats.forEach(function (block) {
+ var process = myself.threads.startProcess(block,
+ myself.isThreadSafe);
+ process.context.outerContext.variables.addVar(localize('message'));
+ process.context.outerContext.variables.setVar(
+ localize('message'),
+ message
+ );
+ process.context.outerContext.variables.addVar(localize('peer'));
+ process.context.outerContext.variables.setVar(
+ localize('peer'),
+ peer
+ );
+ });
+};
+
// StageMorph scaling
StageMorph.prototype.setScale = function (number) {
@@ -4709,6 +5033,16 @@ StageMorph.prototype.getTempo = function () {
return +this.tempo;
};
+// StageMorph keys
+
+StageMorph.prototype.getKeysPressed = function () {
+ var keys = [];
+ for (var key in this.keysPressed) {
+ keys.push(key);
+ }
+ return new List(keys);
+};
+
// StageMorph messages
StageMorph.prototype.getLastMessage = function () {
@@ -4762,7 +5096,7 @@ StageMorph.prototype.step = function () {
world.keyboardReceiver = this;
}
if (world.currentKey === null) {
- this.keyPressed = null;
+ this.keysPressed = {};
}
// manage threads
@@ -4950,7 +5284,7 @@ StageMorph.prototype.fireGreenFlagEvent = function () {
StageMorph.prototype.fireStopAllEvent = function () {
var ide = this.parentThatIsA(IDE_Morph);
- this.threads.resumeAll(this.stage);
+ //this.threads.resumeAll(this.stage); // leads to a strange Note bug
this.keysPressed = {};
this.threads.stopAll();
this.stopAllActiveSounds();
@@ -5042,7 +5376,7 @@ StageMorph.prototype.blockTemplates = function (category) {
function addVar(pair) {
if (pair) {
- if (myself.variables.silentFind(pair[0])) {
+ if (myself.isVariableNameInUse(pair[0])) {
myself.inform('that name is already in use');
} else {
myself.addVariable(pair[0], pair[1]);
@@ -5106,6 +5440,11 @@ StageMorph.prototype.blockTemplates = function (category) {
blocks.push(block('doPlaySoundUntilDone'));
blocks.push(block('doStopAllSounds'));
blocks.push('-');
+ blocks.push(block('doSetVolume'));
+ blocks.push(block('doChangeVolume'));
+ blocks.push(watcherToggle('reportVolume'));
+ blocks.push(block('reportVolume'));
+ blocks.push('-');
blocks.push(block('doRest'));
blocks.push('-');
blocks.push(block('doPlayNote'));
@@ -5204,6 +5543,7 @@ StageMorph.prototype.blockTemplates = function (category) {
blocks.push(block('reportMouseDown'));
blocks.push('-');
blocks.push(block('reportKeyPressed'));
+ blocks.push(block('getKeysPressed'));
blocks.push('-');
blocks.push(block('doResetTimer'));
blocks.push(watcherToggle('getTimer'));
@@ -5359,9 +5699,7 @@ StageMorph.prototype.blockTemplates = function (category) {
blocks.push(block('doShowVar'));
blocks.push(block('doHideVar'));
blocks.push(block('doDeclareVariables'));
-
blocks.push('=');
-
blocks.push(block('reportNewList'));
blocks.push('-');
blocks.push(block('reportCONS'));
@@ -5396,6 +5734,12 @@ StageMorph.prototype.blockTemplates = function (category) {
blocks.push('=');
+ blocks.push(block('receivePeerMessage'));
+ blocks.push(block('sendPeerMessage'));
+ blocks.push(block('reportPeerId'));
+ blocks.push(block('reportPeerList'));
+ blocks.push('=');
+
if (StageMorph.prototype.enableCodeMapping) {
blocks.push(block('doMapCodeOrHeader'));
blocks.push(block('doMapStringCode'));
@@ -5654,6 +5998,15 @@ StageMorph.prototype.addSound
StageMorph.prototype.playSound
= SpriteMorph.prototype.playSound;
+StageMorph.prototype.doSetVolume
+ = SpriteMorph.prototype.doSetVolume;
+
+StageMorph.prototype.doChangeVolume
+ = SpriteMorph.prototype.doChangeVolume;
+
+StageMorph.prototype.reportVolume
+ = SpriteMorph.prototype.reportVolume;
+
StageMorph.prototype.stopAllActiveSounds = function () {
this.activeSounds.forEach(function (audio) {
audio.pause();
@@ -5668,11 +6021,29 @@ StageMorph.prototype.pauseAllActiveSounds = function () {
};
StageMorph.prototype.resumeAllActiveSounds = function () {
+ var newSounds = []; // remove Sounds that have been played so they do not resume
+
+ this.activeSounds.forEach(function (audio) {
+ if (audio.ended === false) {
+ newSounds.push(audio);
+ audio.play();
+ }
+ });
+
+ this.activeSounds = newSounds;
+};
+
+StageMorph.prototype.muteAllSounds = function () {
+ this.muted = true;
+
this.activeSounds.forEach(function (audio) {
- audio.play();
+ audio.volume = 0;
});
};
+StageMorph.prototype.unmuteAllSounds
+ = SpriteMorph.prototype.unmuteAllSounds;
+
StageMorph.prototype.reportSounds
= SpriteMorph.prototype.reportSounds;
@@ -5751,6 +6122,18 @@ StageMorph.prototype.doubleDefinitionsFor
StageMorph.prototype.replaceDoubleDefinitionsFor
= SpriteMorph.prototype.replaceDoubleDefinitionsFor;
+// StageMorph inheritance support - variables
+
+StageMorph.prototype.isVariableNameInUse
+ = SpriteMorph.prototype.isVariableNameInUse;
+
+StageMorph.prototype.globalVariables
+ = SpriteMorph.prototype.globalVariables;
+
+StageMorph.prototype.inheritedVariableNames = function () {
+ return [];
+};
+
// SpriteBubbleMorph ////////////////////////////////////////////////////////
/*
@@ -6442,9 +6825,10 @@ CostumeEditorMorph.prototype.mouseMove
// Sound instance creation
-function Sound(audio, name) {
+function Sound(audio, name, volume) {
this.audio = audio; // mandatory
this.name = name || "Sound";
+ this.volume = volume || 100;
}
Sound.prototype.play = function () {
@@ -6452,6 +6836,7 @@ Sound.prototype.play = function () {
// externally (i.e. by the stage)
var aud = document.createElement('audio');
aud.src = this.audio.src;
+ aud.volume = Math.min(Math.max(0, this.volume), 100) / 100;
aud.play();
return aud;
};
@@ -6461,7 +6846,8 @@ Sound.prototype.copy = function () {
cpy;
snd.src = this.audio.src;
- cpy = new Sound(snd, this.name ? copy(this.name) : null);
+ snd.volume = this.volume;
+ cpy = new Sound(snd, this.name ? copy(this.name) : null, this.volume ? copy(this.volume) : null);
return cpy;
};
@@ -6475,8 +6861,9 @@ Sound.prototype.toDataURL = function () {
// Note instance creation
-function Note(pitch) {
+function Note(pitch, volume) {
this.pitch = pitch === 0 ? 0 : pitch || 69;
+ this.volume = volume;
this.setupContext();
this.oscillator = null;
}
@@ -6507,12 +6894,13 @@ Note.prototype.setupContext = function () {
}
Note.prototype.audioContext = new AudioContext();
Note.prototype.gainNode = Note.prototype.audioContext.createGain();
- Note.prototype.gainNode.gain.value = 0.25; // reduce volume by 1/4
};
// Note playing
Note.prototype.play = function () {
+ this.gainNode.gain.value = 0.25 * this.volume / 100; // reduce volume by 1/4
+
this.oscillator = this.audioContext.createOscillator();
if (!this.oscillator.start) {
this.oscillator.start = this.oscillator.noteOn;
@@ -6528,6 +6916,12 @@ Note.prototype.play = function () {
this.oscillator.start(0);
};
+Note.prototype.setVolume = function (volume) {
+ this.stop();
+ this.volume = volume;
+ this.play();
+}
+
Note.prototype.stop = function () {
if (this.oscillator) {
this.oscillator.stop(0);
@@ -6956,7 +7350,7 @@ WatcherMorph.prototype.object = function () {
WatcherMorph.prototype.isGlobal = function (selector) {
return contains(
- ['getLastAnswer', 'getLastMessage', 'getTempo', 'getTimer',
+ ['getLastAnswer', 'getKeysPressed', 'getLastMessage', 'getTempo', 'getTimer',
'reportMouseX', 'reportMouseY', 'reportThreadCount'],
selector
);
@@ -6964,32 +7358,51 @@ WatcherMorph.prototype.isGlobal = function (selector) {
// WatcherMorph slider accessing:
-WatcherMorph.prototype.setSliderMin = function (num) {
+WatcherMorph.prototype.setSliderMin = function (num, noUpdate) {
if (this.target instanceof VariableFrame) {
- this.sliderMorph.setSize(1);
- this.sliderMorph.setStart(num);
- this.sliderMorph.setSize(this.sliderMorph.rangeSize() / 5);
+ this.sliderMorph.setSize(1, noUpdate);
+ this.sliderMorph.setStart(num, noUpdate);
+ this.sliderMorph.setSize(this.sliderMorph.rangeSize() / 5, noUpdate);
}
};
-WatcherMorph.prototype.setSliderMax = function (num) {
+WatcherMorph.prototype.setSliderMax = function (num, noUpdate) {
if (this.target instanceof VariableFrame) {
- this.sliderMorph.setSize(1);
- this.sliderMorph.setStop(num);
- this.sliderMorph.setSize(this.sliderMorph.rangeSize() / 5);
+ this.sliderMorph.setSize(1, noUpdate);
+ this.sliderMorph.setStop(num, noUpdate);
+ this.sliderMorph.setSize(this.sliderMorph.rangeSize() / 5, noUpdate);
}
};
// WatcherMorph updating:
WatcherMorph.prototype.update = function () {
- var newValue,
- num;
+ var newValue, sprite, num;
+
if (this.target && this.getter) {
this.updateLabel();
if (this.target instanceof VariableFrame) {
newValue = this.target.vars[this.getter] ?
this.target.vars[this.getter].value : undefined;
+ if (newValue === undefined && this.target.owner) {
+ sprite = this.target.owner;
+ if (contains(sprite.inheritedVariableNames(), this.getter)) {
+ newValue = this.target.getVar(this.getter);
+ // ghost cell color
+ this.cellMorph.setColor(
+ SpriteMorph.prototype.blockColor.variables
+ .lighter(35)
+ );
+ } else {
+ this.destroy();
+ return;
+ }
+ } else {
+ // un-ghost the cell color
+ this.cellMorph.setColor(
+ SpriteMorph.prototype.blockColor.variables
+ );
+ }
} else {
newValue = this.target[this.getter]();
}
@@ -7074,7 +7487,11 @@ WatcherMorph.prototype.fixLayout = function () {
this.sliderMorph.button.pressColor.b += 100;
this.sliderMorph.setHeight(fontSize);
this.sliderMorph.action = function (num) {
- myself.target.vars[myself.getter].value = Math.round(num);
+ myself.target.setVar(
+ myself.getter,
+ Math.round(num),
+ myself.target.owner
+ );
};
this.add(this.sliderMorph);
}
diff --git a/octokit.js b/octokit.js
new file mode 160000
+Subproject 579cef9893626da93ab8afbd601e4a3cf0d6d27
diff --git a/paint.js b/paint.js
index 65ae1f6..7e39671 100644
--- a/paint.js
+++ b/paint.js
@@ -657,6 +657,13 @@ PaintCanvasMorph.prototype.drawcrosshair = function (context) {
ctx.strokeStyle = 'black';
ctx.clearRect(0, 0, this.mask.width, this.mask.height);
+ //draw rotation center coordinates near crosshairs
+ ctx.globalAlpha = 1;
+ ctx.fillStyle = "blue";
+ ctx.font = "bold 10px Arial";
+ var coordinates = -Math.floor((this.mask.width/2 - rp.x)) + ', ' + Math.floor((this.mask.height/2 - rp.y));
+ ctx.fillText(coordinates, rp.x + 20, rp.y - 20);
+
// draw crosshairs:
ctx.globalAlpha = 0.5;
diff --git a/peer.js b/peer.js
new file mode 100644
index 0000000..bccf5c1
--- /dev/null
+++ b/peer.js
@@ -0,0 +1,2711 @@
+/*! peerjs.js build:0.3.9, development. Copyright(c) 2013 Michelle Bu <michelle@michellebu.com> */
+(function(exports){
+var binaryFeatures = {};
+binaryFeatures.useBlobBuilder = (function(){
+ try {
+ new Blob([]);
+ return false;
+ } catch (e) {
+ return true;
+ }
+})();
+
+binaryFeatures.useArrayBufferView = !binaryFeatures.useBlobBuilder && (function(){
+ try {
+ return (new Blob([new Uint8Array([])])).size === 0;
+ } catch (e) {
+ return true;
+ }
+})();
+
+exports.binaryFeatures = binaryFeatures;
+exports.BlobBuilder = window.WebKitBlobBuilder || window.MozBlobBuilder || window.MSBlobBuilder || window.BlobBuilder;
+
+function BufferBuilder(){
+ this._pieces = [];
+ this._parts = [];
+}
+
+BufferBuilder.prototype.append = function(data) {
+ if(typeof data === 'number') {
+ this._pieces.push(data);
+ } else {
+ this.flush();
+ this._parts.push(data);
+ }
+};
+
+BufferBuilder.prototype.flush = function() {
+ if (this._pieces.length > 0) {
+ var buf = new Uint8Array(this._pieces);
+ if(!binaryFeatures.useArrayBufferView) {
+ buf = buf.buffer;
+ }
+ this._parts.push(buf);
+ this._pieces = [];
+ }
+};
+
+BufferBuilder.prototype.getBuffer = function() {
+ this.flush();
+ if(binaryFeatures.useBlobBuilder) {
+ var builder = new BlobBuilder();
+ for(var i = 0, ii = this._parts.length; i < ii; i++) {
+ builder.append(this._parts[i]);
+ }
+ return builder.getBlob();
+ } else {
+ return new Blob(this._parts);
+ }
+};
+exports.BinaryPack = {
+ unpack: function(data){
+ var unpacker = new Unpacker(data);
+ return unpacker.unpack();
+ },
+ pack: function(data){
+ var packer = new Packer();
+ packer.pack(data);
+ var buffer = packer.getBuffer();
+ return buffer;
+ }
+};
+
+function Unpacker (data){
+ // Data is ArrayBuffer
+ this.index = 0;
+ this.dataBuffer = data;
+ this.dataView = new Uint8Array(this.dataBuffer);
+ this.length = this.dataBuffer.byteLength;
+}
+
+
+Unpacker.prototype.unpack = function(){
+ var type = this.unpack_uint8();
+ if (type < 0x80){
+ var positive_fixnum = type;
+ return positive_fixnum;
+ } else if ((type ^ 0xe0) < 0x20){
+ var negative_fixnum = (type ^ 0xe0) - 0x20;
+ return negative_fixnum;
+ }
+ var size;
+ if ((size = type ^ 0xa0) <= 0x0f){
+ return this.unpack_raw(size);
+ } else if ((size = type ^ 0xb0) <= 0x0f){
+ return this.unpack_string(size);
+ } else if ((size = type ^ 0x90) <= 0x0f){
+ return this.unpack_array(size);
+ } else if ((size = type ^ 0x80) <= 0x0f){
+ return this.unpack_map(size);
+ }
+ switch(type){
+ case 0xc0:
+ return null;
+ case 0xc1:
+ return undefined;
+ case 0xc2:
+ return false;
+ case 0xc3:
+ return true;
+ case 0xca:
+ return this.unpack_float();
+ case 0xcb:
+ return this.unpack_double();
+ case 0xcc:
+ return this.unpack_uint8();
+ case 0xcd:
+ return this.unpack_uint16();
+ case 0xce:
+ return this.unpack_uint32();
+ case 0xcf:
+ return this.unpack_uint64();
+ case 0xd0:
+ return this.unpack_int8();
+ case 0xd1:
+ return this.unpack_int16();
+ case 0xd2:
+ return this.unpack_int32();
+ case 0xd3:
+ return this.unpack_int64();
+ case 0xd4:
+ return undefined;
+ case 0xd5:
+ return undefined;
+ case 0xd6:
+ return undefined;
+ case 0xd7:
+ return undefined;
+ case 0xd8:
+ size = this.unpack_uint16();
+ return this.unpack_string(size);
+ case 0xd9:
+ size = this.unpack_uint32();
+ return this.unpack_string(size);
+ case 0xda:
+ size = this.unpack_uint16();
+ return this.unpack_raw(size);
+ case 0xdb:
+ size = this.unpack_uint32();
+ return this.unpack_raw(size);
+ case 0xdc:
+ size = this.unpack_uint16();
+ return this.unpack_array(size);
+ case 0xdd:
+ size = this.unpack_uint32();
+ return this.unpack_array(size);
+ case 0xde:
+ size = this.unpack_uint16();
+ return this.unpack_map(size);
+ case 0xdf:
+ size = this.unpack_uint32();
+ return this.unpack_map(size);
+ }
+}
+
+Unpacker.prototype.unpack_uint8 = function(){
+ var byte = this.dataView[this.index] & 0xff;
+ this.index++;
+ return byte;
+};
+
+Unpacker.prototype.unpack_uint16 = function(){
+ var bytes = this.read(2);
+ var uint16 =
+ ((bytes[0] & 0xff) * 256) + (bytes[1] & 0xff);
+ this.index += 2;
+ return uint16;
+}
+
+Unpacker.prototype.unpack_uint32 = function(){
+ var bytes = this.read(4);
+ var uint32 =
+ ((bytes[0] * 256 +
+ bytes[1]) * 256 +
+ bytes[2]) * 256 +
+ bytes[3];
+ this.index += 4;
+ return uint32;
+}
+
+Unpacker.prototype.unpack_uint64 = function(){
+ var bytes = this.read(8);
+ var uint64 =
+ ((((((bytes[0] * 256 +
+ bytes[1]) * 256 +
+ bytes[2]) * 256 +
+ bytes[3]) * 256 +
+ bytes[4]) * 256 +
+ bytes[5]) * 256 +
+ bytes[6]) * 256 +
+ bytes[7];
+ this.index += 8;
+ return uint64;
+}
+
+
+Unpacker.prototype.unpack_int8 = function(){
+ var uint8 = this.unpack_uint8();
+ return (uint8 < 0x80 ) ? uint8 : uint8 - (1 << 8);
+};
+
+Unpacker.prototype.unpack_int16 = function(){
+ var uint16 = this.unpack_uint16();
+ return (uint16 < 0x8000 ) ? uint16 : uint16 - (1 << 16);
+}
+
+Unpacker.prototype.unpack_int32 = function(){
+ var uint32 = this.unpack_uint32();
+ return (uint32 < Math.pow(2, 31) ) ? uint32 :
+ uint32 - Math.pow(2, 32);
+}
+
+Unpacker.prototype.unpack_int64 = function(){
+ var uint64 = this.unpack_uint64();
+ return (uint64 < Math.pow(2, 63) ) ? uint64 :
+ uint64 - Math.pow(2, 64);
+}
+
+Unpacker.prototype.unpack_raw = function(size){
+ if ( this.length < this.index + size){
+ throw new Error('BinaryPackFailure: index is out of range'
+ + ' ' + this.index + ' ' + size + ' ' + this.length);
+ }
+ var buf = this.dataBuffer.slice(this.index, this.index + size);
+ this.index += size;
+
+ //buf = util.bufferToString(buf);
+
+ return buf;
+}
+
+Unpacker.prototype.unpack_string = function(size){
+ var bytes = this.read(size);
+ var i = 0, str = '', c, code;
+ while(i < size){
+ c = bytes[i];
+ if ( c < 128){
+ str += String.fromCharCode(c);
+ i++;
+ } else if ((c ^ 0xc0) < 32){
+ code = ((c ^ 0xc0) << 6) | (bytes[i+1] & 63);
+ str += String.fromCharCode(code);
+ i += 2;
+ } else {
+ code = ((c & 15) << 12) | ((bytes[i+1] & 63) << 6) |
+ (bytes[i+2] & 63);
+ str += String.fromCharCode(code);
+ i += 3;
+ }
+ }
+ this.index += size;
+ return str;
+}
+
+Unpacker.prototype.unpack_array = function(size){
+ var objects = new Array(size);
+ for(var i = 0; i < size ; i++){
+ objects[i] = this.unpack();
+ }
+ return objects;
+}
+
+Unpacker.prototype.unpack_map = function(size){
+ var map = {};
+ for(var i = 0; i < size ; i++){
+ var key = this.unpack();
+ var value = this.unpack();
+ map[key] = value;
+ }
+ return map;
+}
+
+Unpacker.prototype.unpack_float = function(){
+ var uint32 = this.unpack_uint32();
+ var sign = uint32 >> 31;
+ var exp = ((uint32 >> 23) & 0xff) - 127;
+ var fraction = ( uint32 & 0x7fffff ) | 0x800000;
+ return (sign == 0 ? 1 : -1) *
+ fraction * Math.pow(2, exp - 23);
+}
+
+Unpacker.prototype.unpack_double = function(){
+ var h32 = this.unpack_uint32();
+ var l32 = this.unpack_uint32();
+ var sign = h32 >> 31;
+ var exp = ((h32 >> 20) & 0x7ff) - 1023;
+ var hfrac = ( h32 & 0xfffff ) | 0x100000;
+ var frac = hfrac * Math.pow(2, exp - 20) +
+ l32 * Math.pow(2, exp - 52);
+ return (sign == 0 ? 1 : -1) * frac;
+}
+
+Unpacker.prototype.read = function(length){
+ var j = this.index;
+ if (j + length <= this.length) {
+ return this.dataView.subarray(j, j + length);
+ } else {
+ throw new Error('BinaryPackFailure: read index out of range');
+ }
+}
+
+function Packer(){
+ this.bufferBuilder = new BufferBuilder();
+}
+
+Packer.prototype.getBuffer = function(){
+ return this.bufferBuilder.getBuffer();
+}
+
+Packer.prototype.pack = function(value){
+ var type = typeof(value);
+ if (type == 'string'){
+ this.pack_string(value);
+ } else if (type == 'number'){
+ if (Math.floor(value) === value){
+ this.pack_integer(value);
+ } else{
+ this.pack_double(value);
+ }
+ } else if (type == 'boolean'){
+ if (value === true){
+ this.bufferBuilder.append(0xc3);
+ } else if (value === false){
+ this.bufferBuilder.append(0xc2);
+ }
+ } else if (type == 'undefined'){
+ this.bufferBuilder.append(0xc0);
+ } else if (type == 'object'){
+ if (value === null){
+ this.bufferBuilder.append(0xc0);
+ } else {
+ var constructor = value.constructor;
+ if (constructor == Array){
+ this.pack_array(value);
+ } else if (constructor == Blob || constructor == File) {
+ this.pack_bin(value);
+ } else if (constructor == ArrayBuffer) {
+ if(binaryFeatures.useArrayBufferView) {
+ this.pack_bin(new Uint8Array(value));
+ } else {
+ this.pack_bin(value);
+ }
+ } else if ('BYTES_PER_ELEMENT' in value){
+ if(binaryFeatures.useArrayBufferView) {
+ this.pack_bin(new Uint8Array(value.buffer));
+ } else {
+ this.pack_bin(value.buffer);
+ }
+ } else if (constructor == Object){
+ this.pack_object(value);
+ } else if (constructor == Date){
+ this.pack_string(value.toString());
+ } else if (typeof value.toBinaryPack == 'function'){
+ this.bufferBuilder.append(value.toBinaryPack());
+ } else {
+ throw new Error('Type "' + constructor.toString() + '" not yet supported');
+ }
+ }
+ } else {
+ throw new Error('Type "' + type + '" not yet supported');
+ }
+ this.bufferBuilder.flush();
+}
+
+
+Packer.prototype.pack_bin = function(blob){
+ var length = blob.length || blob.byteLength || blob.size;
+ if (length <= 0x0f){
+ this.pack_uint8(0xa0 + length);
+ } else if (length <= 0xffff){
+ this.bufferBuilder.append(0xda) ;
+ this.pack_uint16(length);
+ } else if (length <= 0xffffffff){
+ this.bufferBuilder.append(0xdb);
+ this.pack_uint32(length);
+ } else{
+ throw new Error('Invalid length');
+ return;
+ }
+ this.bufferBuilder.append(blob);
+}
+
+Packer.prototype.pack_string = function(str){
+ var length = utf8Length(str);
+
+ if (length <= 0x0f){
+ this.pack_uint8(0xb0 + length);
+ } else if (length <= 0xffff){
+ this.bufferBuilder.append(0xd8) ;
+ this.pack_uint16(length);
+ } else if (length <= 0xffffffff){
+ this.bufferBuilder.append(0xd9);
+ this.pack_uint32(length);
+ } else{
+ throw new Error('Invalid length');
+ return;
+ }
+ this.bufferBuilder.append(str);
+}
+
+Packer.prototype.pack_array = function(ary){
+ var length = ary.length;
+ if (length <= 0x0f){
+ this.pack_uint8(0x90 + length);
+ } else if (length <= 0xffff){
+ this.bufferBuilder.append(0xdc)
+ this.pack_uint16(length);
+ } else if (length <= 0xffffffff){
+ this.bufferBuilder.append(0xdd);
+ this.pack_uint32(length);
+ } else{
+ throw new Error('Invalid length');
+ }
+ for(var i = 0; i < length ; i++){
+ this.pack(ary[i]);
+ }
+}
+
+Packer.prototype.pack_integer = function(num){
+ if ( -0x20 <= num && num <= 0x7f){
+ this.bufferBuilder.append(num & 0xff);
+ } else if (0x00 <= num && num <= 0xff){
+ this.bufferBuilder.append(0xcc);
+ this.pack_uint8(num);
+ } else if (-0x80 <= num && num <= 0x7f){
+ this.bufferBuilder.append(0xd0);
+ this.pack_int8(num);
+ } else if ( 0x0000 <= num && num <= 0xffff){
+ this.bufferBuilder.append(0xcd);
+ this.pack_uint16(num);
+ } else if (-0x8000 <= num && num <= 0x7fff){
+ this.bufferBuilder.append(0xd1);
+ this.pack_int16(num);
+ } else if ( 0x00000000 <= num && num <= 0xffffffff){
+ this.bufferBuilder.append(0xce);
+ this.pack_uint32(num);
+ } else if (-0x80000000 <= num && num <= 0x7fffffff){
+ this.bufferBuilder.append(0xd2);
+ this.pack_int32(num);
+ } else if (-0x8000000000000000 <= num && num <= 0x7FFFFFFFFFFFFFFF){
+ this.bufferBuilder.append(0xd3);
+ this.pack_int64(num);
+ } else if (0x0000000000000000 <= num && num <= 0xFFFFFFFFFFFFFFFF){
+ this.bufferBuilder.append(0xcf);
+ this.pack_uint64(num);
+ } else{
+ throw new Error('Invalid integer');
+ }
+}
+
+Packer.prototype.pack_double = function(num){
+ var sign = 0;
+ if (num < 0){
+ sign = 1;
+ num = -num;
+ }
+ var exp = Math.floor(Math.log(num) / Math.LN2);
+ var frac0 = num / Math.pow(2, exp) - 1;
+ var frac1 = Math.floor(frac0 * Math.pow(2, 52));
+ var b32 = Math.pow(2, 32);
+ var h32 = (sign << 31) | ((exp+1023) << 20) |
+ (frac1 / b32) & 0x0fffff;
+ var l32 = frac1 % b32;
+ this.bufferBuilder.append(0xcb);
+ this.pack_int32(h32);
+ this.pack_int32(l32);
+}
+
+Packer.prototype.pack_object = function(obj){
+ var keys = Object.keys(obj);
+ var length = keys.length;
+ if (length <= 0x0f){
+ this.pack_uint8(0x80 + length);
+ } else if (length <= 0xffff){
+ this.bufferBuilder.append(0xde);
+ this.pack_uint16(length);
+ } else if (length <= 0xffffffff){
+ this.bufferBuilder.append(0xdf);
+ this.pack_uint32(length);
+ } else{
+ throw new Error('Invalid length');
+ }
+ for(var prop in obj){
+ if (obj.hasOwnProperty(prop)){
+ this.pack(prop);
+ this.pack(obj[prop]);
+ }
+ }
+}
+
+Packer.prototype.pack_uint8 = function(num){
+ this.bufferBuilder.append(num);
+}
+
+Packer.prototype.pack_uint16 = function(num){
+ this.bufferBuilder.append(num >> 8);
+ this.bufferBuilder.append(num & 0xff);
+}
+
+Packer.prototype.pack_uint32 = function(num){
+ var n = num & 0xffffffff;
+ this.bufferBuilder.append((n & 0xff000000) >>> 24);
+ this.bufferBuilder.append((n & 0x00ff0000) >>> 16);
+ this.bufferBuilder.append((n & 0x0000ff00) >>> 8);
+ this.bufferBuilder.append((n & 0x000000ff));
+}
+
+Packer.prototype.pack_uint64 = function(num){
+ var high = num / Math.pow(2, 32);
+ var low = num % Math.pow(2, 32);
+ this.bufferBuilder.append((high & 0xff000000) >>> 24);
+ this.bufferBuilder.append((high & 0x00ff0000) >>> 16);
+ this.bufferBuilder.append((high & 0x0000ff00) >>> 8);
+ this.bufferBuilder.append((high & 0x000000ff));
+ this.bufferBuilder.append((low & 0xff000000) >>> 24);
+ this.bufferBuilder.append((low & 0x00ff0000) >>> 16);
+ this.bufferBuilder.append((low & 0x0000ff00) >>> 8);
+ this.bufferBuilder.append((low & 0x000000ff));
+}
+
+Packer.prototype.pack_int8 = function(num){
+ this.bufferBuilder.append(num & 0xff);
+}
+
+Packer.prototype.pack_int16 = function(num){
+ this.bufferBuilder.append((num & 0xff00) >> 8);
+ this.bufferBuilder.append(num & 0xff);
+}
+
+Packer.prototype.pack_int32 = function(num){
+ this.bufferBuilder.append((num >>> 24) & 0xff);
+ this.bufferBuilder.append((num & 0x00ff0000) >>> 16);
+ this.bufferBuilder.append((num & 0x0000ff00) >>> 8);
+ this.bufferBuilder.append((num & 0x000000ff));
+}
+
+Packer.prototype.pack_int64 = function(num){
+ var high = Math.floor(num / Math.pow(2, 32));
+ var low = num % Math.pow(2, 32);
+ this.bufferBuilder.append((high & 0xff000000) >>> 24);
+ this.bufferBuilder.append((high & 0x00ff0000) >>> 16);
+ this.bufferBuilder.append((high & 0x0000ff00) >>> 8);
+ this.bufferBuilder.append((high & 0x000000ff));
+ this.bufferBuilder.append((low & 0xff000000) >>> 24);
+ this.bufferBuilder.append((low & 0x00ff0000) >>> 16);
+ this.bufferBuilder.append((low & 0x0000ff00) >>> 8);
+ this.bufferBuilder.append((low & 0x000000ff));
+}
+
+function _utf8Replace(m){
+ var code = m.charCodeAt(0);
+
+ if(code <= 0x7ff) return '00';
+ if(code <= 0xffff) return '000';
+ if(code <= 0x1fffff) return '0000';
+ if(code <= 0x3ffffff) return '00000';
+ return '000000';
+}
+
+function utf8Length(str){
+ if (str.length > 600) {
+ // Blob method faster for large strings
+ return (new Blob([str])).size;
+ } else {
+ return str.replace(/[^\u0000-\u007F]/g, _utf8Replace).length;
+ }
+}
+/**
+ * Light EventEmitter. Ported from Node.js/events.js
+ * Eric Zhang
+ */
+
+/**
+ * EventEmitter class
+ * Creates an object with event registering and firing methods
+ */
+function EventEmitter() {
+ // Initialise required storage variables
+ this._events = {};
+}
+
+var isArray = Array.isArray;
+
+
+EventEmitter.prototype.addListener = function(type, listener, scope, once) {
+ if ('function' !== typeof listener) {
+ throw new Error('addListener only takes instances of Function');
+ }
+
+ // To avoid recursion in the case that type == "newListeners"! Before
+ // adding it to the listeners, first emit "newListeners".
+ this.emit('newListener', type, typeof listener.listener === 'function' ?
+ listener.listener : listener);
+
+ if (!this._events[type]) {
+ // Optimize the case of one listener. Don't need the extra array object.
+ this._events[type] = listener;
+ } else if (isArray(this._events[type])) {
+
+ // If we've already got an array, just append.
+ this._events[type].push(listener);
+
+ } else {
+ // Adding the second element, need to change to array.
+ this._events[type] = [this._events[type], listener];
+ }
+ return this;
+};
+
+EventEmitter.prototype.on = EventEmitter.prototype.addListener;
+
+EventEmitter.prototype.once = function(type, listener, scope) {
+ if ('function' !== typeof listener) {
+ throw new Error('.once only takes instances of Function');
+ }
+
+ var self = this;
+ function g() {
+ self.removeListener(type, g);
+ listener.apply(this, arguments);
+ };
+
+ g.listener = listener;
+ self.on(type, g);
+
+ return this;
+};
+
+EventEmitter.prototype.removeListener = function(type, listener, scope) {
+ if ('function' !== typeof listener) {
+ throw new Error('removeListener only takes instances of Function');
+ }
+
+ // does not use listeners(), so no side effect of creating _events[type]
+ if (!this._events[type]) return this;
+
+ var list = this._events[type];
+
+ if (isArray(list)) {
+ var position = -1;
+ for (var i = 0, length = list.length; i < length; i++) {
+ if (list[i] === listener ||
+ (list[i].listener && list[i].listener === listener))
+ {
+ position = i;
+ break;
+ }
+ }
+
+ if (position < 0) return this;
+ list.splice(position, 1);
+ if (list.length == 0)
+ delete this._events[type];
+ } else if (list === listener ||
+ (list.listener && list.listener === listener))
+ {
+ delete this._events[type];
+ }
+
+ return this;
+};
+
+
+EventEmitter.prototype.off = EventEmitter.prototype.removeListener;
+
+
+EventEmitter.prototype.removeAllListeners = function(type) {
+ if (arguments.length === 0) {
+ this._events = {};
+ return this;
+ }
+
+ // does not use listeners(), so no side effect of creating _events[type]
+ if (type && this._events && this._events[type]) this._events[type] = null;
+ return this;
+};
+
+EventEmitter.prototype.listeners = function(type) {
+ if (!this._events[type]) this._events[type] = [];
+ if (!isArray(this._events[type])) {
+ this._events[type] = [this._events[type]];
+ }
+ return this._events[type];
+};
+
+EventEmitter.prototype.emit = function(type) {
+ var type = arguments[0];
+ var handler = this._events[type];
+ if (!handler) return false;
+
+ if (typeof handler == 'function') {
+ switch (arguments.length) {
+ // fast cases
+ case 1:
+ handler.call(this);
+ break;
+ case 2:
+ handler.call(this, arguments[1]);
+ break;
+ case 3:
+ handler.call(this, arguments[1], arguments[2]);
+ break;
+ // slower
+ default:
+ var l = arguments.length;
+ var args = new Array(l - 1);
+ for (var i = 1; i < l; i++) args[i - 1] = arguments[i];
+ handler.apply(this, args);
+ }
+ return true;
+
+ } else if (isArray(handler)) {
+ var l = arguments.length;
+ var args = new Array(l - 1);
+ for (var i = 1; i < l; i++) args[i - 1] = arguments[i];
+
+ var listeners = handler.slice();
+ for (var i = 0, l = listeners.length; i < l; i++) {
+ listeners[i].apply(this, args);
+ }
+ return true;
+ } else {
+ return false;
+ }
+};
+
+
+
+/**
+ * Reliable transfer for Chrome Canary DataChannel impl.
+ * Author: @michellebu
+ */
+function Reliable(dc, debug) {
+ if (!(this instanceof Reliable)) return new Reliable(dc);
+ this._dc = dc;
+
+ util.debug = debug;
+
+ // Messages sent/received so far.
+ // id: { ack: n, chunks: [...] }
+ this._outgoing = {};
+ // id: { ack: ['ack', id, n], chunks: [...] }
+ this._incoming = {};
+ this._received = {};
+
+ // Window size.
+ this._window = 1000;
+ // MTU.
+ this._mtu = 500;
+ // Interval for setInterval. In ms.
+ this._interval = 0;
+
+ // Messages sent.
+ this._count = 0;
+
+ // Outgoing message queue.
+ this._queue = [];
+
+ this._setupDC();
+};
+
+// Send a message reliably.
+Reliable.prototype.send = function(msg) {
+ // Determine if chunking is necessary.
+ var bl = util.pack(msg);
+ if (bl.size < this._mtu) {
+ this._handleSend(['no', bl]);
+ return;
+ }
+
+ this._outgoing[this._count] = {
+ ack: 0,
+ chunks: this._chunk(bl)
+ };
+
+ if (util.debug) {
+ this._outgoing[this._count].timer = new Date();
+ }
+
+ // Send prelim window.
+ this._sendWindowedChunks(this._count);
+ this._count += 1;
+};
+
+// Set up interval for processing queue.
+Reliable.prototype._setupInterval = function() {
+ // TODO: fail gracefully.
+
+ var self = this;
+ this._timeout = setInterval(function() {
+ // FIXME: String stuff makes things terribly async.
+ var msg = self._queue.shift();
+ if (msg._multiple) {
+ for (var i = 0, ii = msg.length; i < ii; i += 1) {
+ self._intervalSend(msg[i]);
+ }
+ } else {
+ self._intervalSend(msg);
+ }
+ }, this._interval);
+};
+
+Reliable.prototype._intervalSend = function(msg) {
+ var self = this;
+ msg = util.pack(msg);
+ util.blobToBinaryString(msg, function(str) {
+ self._dc.send(str);
+ });
+ if (self._queue.length === 0) {
+ clearTimeout(self._timeout);
+ self._timeout = null;
+ //self._processAcks();
+ }
+};
+
+// Go through ACKs to send missing pieces.
+Reliable.prototype._processAcks = function() {
+ for (var id in this._outgoing) {
+ if (this._outgoing.hasOwnProperty(id)) {
+ this._sendWindowedChunks(id);
+ }
+ }
+};
+
+// Handle sending a message.
+// FIXME: Don't wait for interval time for all messages...
+Reliable.prototype._handleSend = function(msg) {
+ var push = true;
+ for (var i = 0, ii = this._queue.length; i < ii; i += 1) {
+ var item = this._queue[i];
+ if (item === msg) {
+ push = false;
+ } else if (item._multiple && item.indexOf(msg) !== -1) {
+ push = false;
+ }
+ }
+ if (push) {
+ this._queue.push(msg);
+ if (!this._timeout) {
+ this._setupInterval();
+ }
+ }
+};
+
+// Set up DataChannel handlers.
+Reliable.prototype._setupDC = function() {
+ // Handle various message types.
+ var self = this;
+ this._dc.onmessage = function(e) {
+ var msg = e.data;
+ var datatype = msg.constructor;
+ // FIXME: msg is String until binary is supported.
+ // Once that happens, this will have to be smarter.
+ if (datatype === String) {
+ var ab = util.binaryStringToArrayBuffer(msg);
+ msg = util.unpack(ab);
+ self._handleMessage(msg);
+ }
+ };
+};
+
+// Handles an incoming message.
+Reliable.prototype._handleMessage = function(msg) {
+ var id = msg[1];
+ var idata = this._incoming[id];
+ var odata = this._outgoing[id];
+ var data;
+ switch (msg[0]) {
+ // No chunking was done.
+ case 'no':
+ var message = id;
+ if (!!message) {
+ this.onmessage(util.unpack(message));
+ }
+ break;
+ // Reached the end of the message.
+ case 'end':
+ data = idata;
+
+ // In case end comes first.
+ this._received[id] = msg[2];
+
+ if (!data) {
+ break;
+ }
+
+ this._ack(id);
+ break;
+ case 'ack':
+ data = odata;
+ if (!!data) {
+ var ack = msg[2];
+ // Take the larger ACK, for out of order messages.
+ data.ack = Math.max(ack, data.ack);
+
+ // Clean up when all chunks are ACKed.
+ if (data.ack >= data.chunks.length) {
+ util.log('Time: ', new Date() - data.timer);
+ delete this._outgoing[id];
+ } else {
+ this._processAcks();
+ }
+ }
+ // If !data, just ignore.
+ break;
+ // Received a chunk of data.
+ case 'chunk':
+ // Create a new entry if none exists.
+ data = idata;
+ if (!data) {
+ var end = this._received[id];
+ if (end === true) {
+ break;
+ }
+ data = {
+ ack: ['ack', id, 0],
+ chunks: []
+ };
+ this._incoming[id] = data;
+ }
+
+ var n = msg[2];
+ var chunk = msg[3];
+ data.chunks[n] = new Uint8Array(chunk);
+
+ // If we get the chunk we're looking for, ACK for next missing.
+ // Otherwise, ACK the same N again.
+ if (n === data.ack[2]) {
+ this._calculateNextAck(id);
+ }
+ this._ack(id);
+ break;
+ default:
+ // Shouldn't happen, but would make sense for message to just go
+ // through as is.
+ this._handleSend(msg);
+ break;
+ }
+};
+
+// Chunks BL into smaller messages.
+Reliable.prototype._chunk = function(bl) {
+ var chunks = [];
+ var size = bl.size;
+ var start = 0;
+ while (start < size) {
+ var end = Math.min(size, start + this._mtu);
+ var b = bl.slice(start, end);
+ var chunk = {
+ payload: b
+ }
+ chunks.push(chunk);
+ start = end;
+ }
+ util.log('Created', chunks.length, 'chunks.');
+ return chunks;
+};
+
+// Sends ACK N, expecting Nth blob chunk for message ID.
+Reliable.prototype._ack = function(id) {
+ var ack = this._incoming[id].ack;
+
+ // if ack is the end value, then call _complete.
+ if (this._received[id] === ack[2]) {
+ this._complete(id);
+ this._received[id] = true;
+ }
+
+ this._handleSend(ack);
+};
+
+// Calculates the next ACK number, given chunks.
+Reliable.prototype._calculateNextAck = function(id) {
+ var data = this._incoming[id];
+ var chunks = data.chunks;
+ for (var i = 0, ii = chunks.length; i < ii; i += 1) {
+ // This chunk is missing!!! Better ACK for it.
+ if (chunks[i] === undefined) {
+ data.ack[2] = i;
+ return;
+ }
+ }
+ data.ack[2] = chunks.length;
+};
+
+// Sends the next window of chunks.
+Reliable.prototype._sendWindowedChunks = function(id) {
+ util.log('sendWindowedChunks for: ', id);
+ var data = this._outgoing[id];
+ var ch = data.chunks;
+ var chunks = [];
+ var limit = Math.min(data.ack + this._window, ch.length);
+ for (var i = data.ack; i < limit; i += 1) {
+ if (!ch[i].sent || i === data.ack) {
+ ch[i].sent = true;
+ chunks.push(['chunk', id, i, ch[i].payload]);
+ }
+ }
+ if (data.ack + this._window >= ch.length) {
+ chunks.push(['end', id, ch.length])
+ }
+ chunks._multiple = true;
+ this._handleSend(chunks);
+};
+
+// Puts together a message from chunks.
+Reliable.prototype._complete = function(id) {
+ util.log('Completed called for', id);
+ var self = this;
+ var chunks = this._incoming[id].chunks;
+ var bl = new Blob(chunks);
+ util.blobToArrayBuffer(bl, function(ab) {
+ self.onmessage(util.unpack(ab));
+ });
+ delete this._incoming[id];
+};
+
+// Ups bandwidth limit on SDP. Meant to be called during offer/answer.
+Reliable.higherBandwidthSDP = function(sdp) {
+ // AS stands for Application-Specific Maximum.
+ // Bandwidth number is in kilobits / sec.
+ // See RFC for more info: http://www.ietf.org/rfc/rfc2327.txt
+
+ // Chrome 31+ doesn't want us munging the SDP, so we'll let them have their
+ // way.
+ var version = navigator.appVersion.match(/Chrome\/(.*?) /);
+ if (version) {
+ version = parseInt(version[1].split('.').shift());
+ if (version < 31) {
+ var parts = sdp.split('b=AS:30');
+ var replace = 'b=AS:102400'; // 100 Mbps
+ if (parts.length > 1) {
+ return parts[0] + replace + parts[1];
+ }
+ }
+ }
+
+ return sdp;
+};
+
+// Overwritten, typically.
+Reliable.prototype.onmessage = function(msg) {};
+
+exports.Reliable = Reliable;
+exports.RTCSessionDescription = window.RTCSessionDescription || window.mozRTCSessionDescription;
+exports.RTCPeerConnection = window.RTCPeerConnection || window.mozRTCPeerConnection || window.webkitRTCPeerConnection;
+exports.RTCIceCandidate = window.RTCIceCandidate || window.mozRTCIceCandidate;
+var defaultConfig = {'iceServers': [{ 'url': 'stun:stun.l.google.com:19302' }]};
+var dataCount = 1;
+
+var util = {
+ noop: function() {},
+
+ CLOUD_HOST: '0.peerjs.com',
+ CLOUD_PORT: 9000,
+
+ // Browsers that need chunking:
+ chunkedBrowsers: {'Chrome': 1},
+ chunkedMTU: 16300, // The original 60000 bytes setting does not work when sending data from Firefox to Chrome, which is "cut off" after 16384 bytes and delivered individually.
+
+ // Logging logic
+ logLevel: 0,
+ setLogLevel: function(level) {
+ var debugLevel = parseInt(level, 10);
+ if (!isNaN(parseInt(level, 10))) {
+ util.logLevel = debugLevel;
+ } else {
+ // If they are using truthy/falsy values for debug
+ util.logLevel = level ? 3 : 0;
+ }
+ util.log = util.warn = util.error = util.noop;
+ if (util.logLevel > 0) {
+ util.error = util._printWith('ERROR');
+ }
+ if (util.logLevel > 1) {
+ util.warn = util._printWith('WARNING');
+ }
+ if (util.logLevel > 2) {
+ util.log = util._print;
+ }
+ },
+ setLogFunction: function(fn) {
+ if (fn.constructor !== Function) {
+ util.warn('The log function you passed in is not a function. Defaulting to regular logs.');
+ } else {
+ util._print = fn;
+ }
+ },
+
+ _printWith: function(prefix) {
+ return function() {
+ var copy = Array.prototype.slice.call(arguments);
+ copy.unshift(prefix);
+ util._print.apply(util, copy);
+ };
+ },
+ _print: function () {
+ var err = false;
+ var copy = Array.prototype.slice.call(arguments);
+ copy.unshift('PeerJS: ');
+ for (var i = 0, l = copy.length; i < l; i++){
+ if (copy[i] instanceof Error) {
+ copy[i] = '(' + copy[i].name + ') ' + copy[i].message;
+ err = true;
+ }
+ }
+ err ? console.error.apply(console, copy) : console.log.apply(console, copy);
+ },
+ //
+
+ // Returns browser-agnostic default config
+ defaultConfig: defaultConfig,
+ //
+
+ // Returns the current browser.
+ browser: (function() {
+ if (window.mozRTCPeerConnection) {
+ return 'Firefox';
+ } else if (window.webkitRTCPeerConnection) {
+ return 'Chrome';
+ } else if (window.RTCPeerConnection) {
+ return 'Supported';
+ } else {
+ return 'Unsupported';
+ }
+ })(),
+ //
+
+ // Lists which features are supported
+ supports: (function() {
+ if (typeof RTCPeerConnection === 'undefined') {
+ return {};
+ }
+
+ var data = true;
+ var audioVideo = true;
+
+ var binaryBlob = false;
+ var sctp = false;
+ var onnegotiationneeded = !!window.webkitRTCPeerConnection;
+
+ var pc, dc;
+ try {
+ pc = new RTCPeerConnection(defaultConfig, {optional: [{RtpDataChannels: true}]});
+ } catch (e) {
+ data = false;
+ audioVideo = false;
+ }
+
+ if (data) {
+ try {
+ dc = pc.createDataChannel('_PEERJSTEST');
+ } catch (e) {
+ data = false;
+ }
+ }
+
+ if (data) {
+ // Binary test
+ try {
+ dc.binaryType = 'blob';
+ binaryBlob = true;
+ } catch (e) {
+ }
+
+ // Reliable test.
+ // Unfortunately Chrome is a bit unreliable about whether or not they
+ // support reliable.
+ var reliablePC = new RTCPeerConnection(defaultConfig, {});
+ try {
+ var reliableDC = reliablePC.createDataChannel('_PEERJSRELIABLETEST', {});
+ sctp = reliableDC.reliable;
+ } catch (e) {
+ }
+ reliablePC.close();
+ }
+
+ // FIXME: not really the best check...
+ if (audioVideo) {
+ audioVideo = !!pc.addStream;
+ }
+
+ // FIXME: this is not great because in theory it doesn't work for
+ // av-only browsers (?).
+ if (!onnegotiationneeded && data) {
+ // sync default check.
+ var negotiationPC = new RTCPeerConnection(defaultConfig, {optional: [{RtpDataChannels: true}]});
+ negotiationPC.onnegotiationneeded = function() {
+ onnegotiationneeded = true;
+ // async check.
+ if (util && util.supports) {
+ util.supports.onnegotiationneeded = true;
+ }
+ };
+ var negotiationDC = negotiationPC.createDataChannel('_PEERJSNEGOTIATIONTEST');
+
+ setTimeout(function() {
+ negotiationPC.close();
+ }, 1000);
+ }
+
+ if (pc) {
+ pc.close();
+ }
+
+ return {
+ audioVideo: audioVideo,
+ data: data,
+ binaryBlob: binaryBlob,
+ binary: sctp, // deprecated; sctp implies binary support.
+ reliable: sctp, // deprecated; sctp implies reliable data.
+ sctp: sctp,
+ onnegotiationneeded: onnegotiationneeded
+ };
+ }()),
+ //
+
+ // Ensure alphanumeric ids
+ validateId: function(id) {
+ // Allow empty ids
+ return !id || /^[A-Za-z0-9]+(?:[ _-][A-Za-z0-9]+)*$/.exec(id);
+ },
+
+ validateKey: function(key) {
+ // Allow empty keys
+ return !key || /^[A-Za-z0-9]+(?:[ _-][A-Za-z0-9]+)*$/.exec(key);
+ },
+
+
+ debug: false,
+
+ inherits: function(ctor, superCtor) {
+ ctor.super_ = superCtor;
+ ctor.prototype = Object.create(superCtor.prototype, {
+ constructor: {
+ value: ctor,
+ enumerable: false,
+ writable: true,
+ configurable: true
+ }
+ });
+ },
+ extend: function(dest, source) {
+ for(var key in source) {
+ if(source.hasOwnProperty(key)) {
+ dest[key] = source[key];
+ }
+ }
+ return dest;
+ },
+ pack: BinaryPack.pack,
+ unpack: BinaryPack.unpack,
+
+ log: function () {
+ if (util.debug) {
+ var err = false;
+ var copy = Array.prototype.slice.call(arguments);
+ copy.unshift('PeerJS: ');
+ for (var i = 0, l = copy.length; i < l; i++){
+ if (copy[i] instanceof Error) {
+ copy[i] = '(' + copy[i].name + ') ' + copy[i].message;
+ err = true;
+ }
+ }
+ err ? console.error.apply(console, copy) : console.log.apply(console, copy);
+ }
+ },
+
+ setZeroTimeout: (function(global) {
+ var timeouts = [];
+ var messageName = 'zero-timeout-message';
+
+ // Like setTimeout, but only takes a function argument. There's
+ // no time argument (always zero) and no arguments (you have to
+ // use a closure).
+ function setZeroTimeoutPostMessage(fn) {
+ timeouts.push(fn);
+ global.postMessage(messageName, '*');
+ }
+
+ function handleMessage(event) {
+ if (event.source == global && event.data == messageName) {
+ if (event.stopPropagation) {
+ event.stopPropagation();
+ }
+ if (timeouts.length) {
+ timeouts.shift()();
+ }
+ }
+ }
+ if (global.addEventListener) {
+ global.addEventListener('message', handleMessage, true);
+ } else if (global.attachEvent) {
+ global.attachEvent('onmessage', handleMessage);
+ }
+ return setZeroTimeoutPostMessage;
+ }(this)),
+
+ // Binary stuff
+
+ // chunks a blob.
+ chunk: function(bl) {
+ var chunks = [];
+ var size = bl.size;
+ var start = index = 0;
+ var total = Math.ceil(size / util.chunkedMTU);
+ while (start < size) {
+ var end = Math.min(size, start + util.chunkedMTU);
+ var b = bl.slice(start, end);
+
+ var chunk = {
+ __peerData: dataCount,
+ n: index,
+ data: b,
+ total: total
+ };
+
+ chunks.push(chunk);
+
+ start = end;
+ index += 1;
+ }
+ dataCount += 1;
+ return chunks;
+ },
+
+ blobToArrayBuffer: function(blob, cb){
+ var fr = new FileReader();
+ fr.onload = function(evt) {
+ cb(evt.target.result);
+ };
+ fr.readAsArrayBuffer(blob);
+ },
+ blobToBinaryString: function(blob, cb){
+ var fr = new FileReader();
+ fr.onload = function(evt) {
+ cb(evt.target.result);
+ };
+ fr.readAsBinaryString(blob);
+ },
+ binaryStringToArrayBuffer: function(binary) {
+ var byteArray = new Uint8Array(binary.length);
+ for (var i = 0; i < binary.length; i++) {
+ byteArray[i] = binary.charCodeAt(i) & 0xff;
+ }
+ return byteArray.buffer;
+ },
+ randomToken: function () {
+ return Math.random().toString(36).substr(2);
+ },
+ //
+
+ isSecure: function() {
+ return location.protocol === 'https:';
+ }
+};
+
+exports.util = util;
+/**
+ * A peer who can initiate connections with other peers.
+ */
+function Peer(id, options) {
+ if (!(this instanceof Peer)) return new Peer(id, options);
+ EventEmitter.call(this);
+
+ // Deal with overloading
+ if (id && id.constructor == Object) {
+ options = id;
+ id = undefined;
+ } else if (id) {
+ // Ensure id is a string
+ id = id.toString();
+ }
+ //
+
+ // Configurize options
+ options = util.extend({
+ debug: 0, // 1: Errors, 2: Warnings, 3: All logs
+ host: util.CLOUD_HOST,
+ port: util.CLOUD_PORT,
+ key: 'peerjs',
+ path: '/',
+ token: util.randomToken(),
+ config: util.defaultConfig
+ }, options);
+ this.options = options;
+ // Detect relative URL host.
+ if (options.host === '/') {
+ options.host = window.location.hostname;
+ }
+ // Set path correctly.
+ if (options.path[0] !== '/') {
+ options.path = '/' + options.path;
+ }
+ if (options.path[options.path.length - 1] !== '/') {
+ options.path += '/';
+ }
+
+ // Set whether we use SSL to same as current host
+ if (options.secure === undefined && options.host !== util.CLOUD_HOST) {
+ options.secure = util.isSecure();
+ }
+ // Set a custom log function if present
+ if (options.logFunction) {
+ util.setLogFunction(options.logFunction);
+ }
+ util.setLogLevel(options.debug);
+ //
+
+ // Sanity checks
+ // Ensure WebRTC supported
+ if (!util.supports.audioVideo && !util.supports.data ) {
+ this._delayedAbort('browser-incompatible', 'The current browser does not support WebRTC');
+ return;
+ }
+ // Ensure alphanumeric id
+ if (!util.validateId(id)) {
+ this._delayedAbort('invalid-id', 'ID "' + id + '" is invalid');
+ return;
+ }
+ // Ensure valid key
+ if (!util.validateKey(options.key)) {
+ this._delayedAbort('invalid-key', 'API KEY "' + options.key + '" is invalid');
+ return;
+ }
+ // Ensure not using unsecure cloud server on SSL page
+ if (options.secure && options.host === '0.peerjs.com') {
+ this._delayedAbort('ssl-unavailable',
+ 'The cloud server currently does not support HTTPS. Please run your own PeerServer to use HTTPS.');
+ return;
+ }
+ //
+
+ // States.
+ this.destroyed = false; // Connections have been killed
+ this.disconnected = false; // Connection to PeerServer killed but P2P connections still active
+ this.open = false; // Sockets and such are not yet open.
+ //
+
+ // References
+ this.connections = {}; // DataConnections for this peer.
+ this._lostMessages = {}; // src => [list of messages]
+ //
+
+ // Start the server connection
+ this._initializeServerConnection();
+ if (id) {
+ this._initialize(id);
+ } else {
+ this._retrieveId();
+ }
+ //
+};
+
+util.inherits(Peer, EventEmitter);
+
+// Initialize the 'socket' (which is actually a mix of XHR streaming and
+// websockets.)
+Peer.prototype._initializeServerConnection = function() {
+ var self = this;
+ this.socket = new Socket(this.options.secure, this.options.host, this.options.port, this.options.path, this.options.key);
+ this.socket.on('message', function(data) {
+ self._handleMessage(data);
+ });
+ this.socket.on('error', function(error) {
+ self._abort('socket-error', error);
+ });
+ this.socket.on('disconnected', function() {
+ // If we haven't explicitly disconnected, emit error and disconnect.
+ if (!self.disconnected) {
+ self.emitError('network', 'Lost connection to server.')
+ self.disconnect();
+ }
+ });
+ this.socket.on('close', function() {
+ // If we haven't explicitly disconnected, emit error.
+ if (!self.disconnected) {
+ self._abort('socket-closed', 'Underlying socket is already closed.');
+ }
+ });
+};
+
+/** Get a unique ID from the server via XHR. */
+Peer.prototype._retrieveId = function(cb) {
+ var self = this;
+ var http = new XMLHttpRequest();
+ var protocol = this.options.secure ? 'https://' : 'http://';
+ var url = protocol + this.options.host + ':' + this.options.port
+ + this.options.path + this.options.key + '/id';
+ var queryString = '?ts=' + new Date().getTime() + '' + Math.random();
+ url += queryString;
+
+ // If there's no ID we need to wait for one before trying to init socket.
+ http.open('get', url, true);
+ http.onerror = function(e) {
+ util.error('Error retrieving ID', e);
+ var pathError = '';
+ if (self.options.path === '/' && self.options.host !== util.CLOUD_HOST) {
+ pathError = ' If you passed in a `path` to your self-hosted PeerServer, '
+ + 'you\'ll also need to pass in that same path when creating a new'
+ + ' Peer.';
+ }
+ self._abort('server-error', 'Could not get an ID from the server.' + pathError);
+ }
+ http.onreadystatechange = function() {
+ if (http.readyState !== 4) {
+ return;
+ }
+ if (http.status !== 200) {
+ http.onerror();
+ return;
+ }
+ self._initialize(http.responseText);
+ };
+ http.send(null);
+};
+
+/** Initialize a connection with the server. */
+Peer.prototype._initialize = function(id) {
+ this.id = id;
+ this.socket.start(this.id, this.options.token);
+}
+
+/** Handles messages from the server. */
+Peer.prototype._handleMessage = function(message) {
+ var type = message.type;
+ var payload = message.payload;
+ var peer = message.src;
+
+ switch (type) {
+ case 'OPEN': // The connection to the server is open.
+ this.emit('open', this.id);
+ this.open = true;
+ break;
+ case 'ERROR': // Server error.
+ this._abort('server-error', payload.msg);
+ break;
+ case 'ID-TAKEN': // The selected ID is taken.
+ this._abort('unavailable-id', 'ID `' + this.id + '` is taken');
+ break;
+ case 'INVALID-KEY': // The given API key cannot be found.
+ this._abort('invalid-key', 'API KEY "' + this.options.key + '" is invalid');
+ break;
+
+ //
+ case 'LEAVE': // Another peer has closed its connection to this peer.
+ util.log('Received leave message from', peer);
+ this._cleanupPeer(peer);
+ break;
+
+ case 'EXPIRE': // The offer sent to a peer has expired without response.
+ this.emitError('peer-unavailable', 'Could not connect to peer ' + peer);
+ break;
+ case 'OFFER': // we should consider switching this to CALL/CONNECT, but this is the least breaking option.
+ var connectionId = payload.connectionId;
+ var connection = this.getConnection(peer, connectionId);
+
+ if (connection) {
+ util.warn('Offer received for existing Connection ID:', connectionId);
+ //connection.handleMessage(message);
+ } else {
+ // Create a new connection.
+ if (payload.type === 'media') {
+ var connection = new MediaConnection(peer, this, {
+ connectionId: connectionId,
+ _payload: payload,
+ metadata: payload.metadata
+ });
+ this._addConnection(peer, connection);
+ this.emit('call', connection);
+ } else if (payload.type === 'data') {
+ connection = new DataConnection(peer, this, {
+ connectionId: connectionId,
+ _payload: payload,
+ metadata: payload.metadata,
+ label: payload.label,
+ serialization: payload.serialization,
+ reliable: payload.reliable
+ });
+ this._addConnection(peer, connection);
+ this.emit('connection', connection);
+ } else {
+ util.warn('Received malformed connection type:', payload.type);
+ return;
+ }
+ // Find messages.
+ var messages = this._getMessages(connectionId);
+ for (var i = 0, ii = messages.length; i < ii; i += 1) {
+ connection.handleMessage(messages[i]);
+ }
+ }
+ break;
+ default:
+ if (!payload) {
+ util.warn('You received a malformed message from ' + peer + ' of type ' + type);
+ return;
+ }
+
+ var id = payload.connectionId;
+ var connection = this.getConnection(peer, id);
+
+ if (connection && connection.pc) {
+ // Pass it on.
+ connection.handleMessage(message);
+ } else if (id) {
+ // Store for possible later use
+ this._storeMessage(id, message);
+ } else {
+ util.warn('You received an unrecognized message:', message);
+ }
+ break;
+ }
+}
+
+/** Stores messages without a set up connection, to be claimed later. */
+Peer.prototype._storeMessage = function(connectionId, message) {
+ if (!this._lostMessages[connectionId]) {
+ this._lostMessages[connectionId] = [];
+ }
+ this._lostMessages[connectionId].push(message);
+}
+
+/** Retrieve messages from lost message store */
+Peer.prototype._getMessages = function(connectionId) {
+ var messages = this._lostMessages[connectionId];
+ if (messages) {
+ delete this._lostMessages[connectionId];
+ return messages;
+ } else {
+ return [];
+ }
+}
+
+/**
+ * Returns a DataConnection to the specified peer. See documentation for a
+ * complete list of options.
+ */
+Peer.prototype.connect = function(peer, options) {
+ if (this.disconnected) {
+ util.warn('You cannot connect to a new Peer because you called '
+ + '.disconnect() on this Peer and ended your connection with the'
+ + ' server. You can create a new Peer to reconnect, or call reconnect'
+ + ' on this peer if you believe its ID to still be available.');
+ this.emitError('disconnected', 'Cannot connect to new Peer after disconnecting from server.');
+ return;
+ }
+ var connection = new DataConnection(peer, this, options);
+ this._addConnection(peer, connection);
+ return connection;
+}
+
+/**
+ * Returns a MediaConnection to the specified peer. See documentation for a
+ * complete list of options.
+ */
+Peer.prototype.call = function(peer, stream, options) {
+ if (this.disconnected) {
+ util.warn('You cannot connect to a new Peer because you called '
+ + '.disconnect() on this Peer and ended your connection with the'
+ + ' server. You can create a new Peer to reconnect.');
+ this.emitError('disconnected', 'Cannot connect to new Peer after disconnecting from server.');
+ return;
+ }
+ if (!stream) {
+ util.error('To call a peer, you must provide a stream from your browser\'s `getUserMedia`.');
+ return;
+ }
+ options = options || {};
+ options._stream = stream;
+ var call = new MediaConnection(peer, this, options);
+ this._addConnection(peer, call);
+ return call;
+}
+
+/** Add a data/media connection to this peer. */
+Peer.prototype._addConnection = function(peer, connection) {
+ if (!this.connections[peer]) {
+ this.connections[peer] = [];
+ }
+ this.connections[peer].push(connection);
+}
+
+/** Retrieve a data/media connection for this peer. */
+Peer.prototype.getConnection = function(peer, id) {
+ var connections = this.connections[peer];
+ if (!connections) {
+ return null;
+ }
+ for (var i = 0, ii = connections.length; i < ii; i++) {
+ if (connections[i].id === id) {
+ return connections[i];
+ }
+ }
+ return null;
+}
+
+Peer.prototype._delayedAbort = function(type, message) {
+ var self = this;
+ util.setZeroTimeout(function(){
+ self._abort(type, message);
+ });
+}
+
+/**
+ * Destroys the Peer and emits an error message.
+ * The Peer is not destroyed if it's in a disconnected state, in which case
+ * it retains its disconnected state and its existing connections.
+ */
+Peer.prototype._abort = function(type, message) {
+ util.error('Aborting!');
+ if (!this._lastServerId) {
+ this.destroy();
+ } else {
+ this.disconnect();
+ }
+ this.emitError(type, message);
+};
+
+/** Emits a typed error message. */
+Peer.prototype.emitError = function(type, err) {
+ util.error('Error:', err);
+ if (typeof err === 'string') {
+ err = new Error(err);
+ }
+ err.type = type;
+ this.emit('error', err);
+};
+
+/**
+ * Destroys the Peer: closes all active connections as well as the connection
+ * to the server.
+ * Warning: The peer can no longer create or accept connections after being
+ * destroyed.
+ */
+Peer.prototype.destroy = function() {
+ if (!this.destroyed) {
+ this._cleanup();
+ this.disconnect();
+ this.destroyed = true;
+ }
+}
+
+
+/** Disconnects every connection on this peer. */
+Peer.prototype._cleanup = function() {
+ if (this.connections) {
+ var peers = Object.keys(this.connections);
+ for (var i = 0, ii = peers.length; i < ii; i++) {
+ this._cleanupPeer(peers[i]);
+ }
+ }
+ this.emit('close');
+}
+
+/** Closes all connections to this peer. */
+Peer.prototype._cleanupPeer = function(peer) {
+ var connections = this.connections[peer];
+ for (var j = 0, jj = connections.length; j < jj; j += 1) {
+ connections[j].close();
+ }
+}
+
+/**
+ * Disconnects the Peer's connection to the PeerServer. Does not close any
+ * active connections.
+ * Warning: The peer can no longer create or accept connections after being
+ * disconnected. It also cannot reconnect to the server.
+ */
+Peer.prototype.disconnect = function() {
+ var self = this;
+ util.setZeroTimeout(function(){
+ if (!self.disconnected) {
+ self.disconnected = true;
+ self.open = false;
+ if (self.socket) {
+ self.socket.close();
+ }
+ self.emit('disconnected', self.id);
+ self._lastServerId = self.id;
+ self.id = null;
+ }
+ });
+}
+
+/** Attempts to reconnect with the same ID. */
+Peer.prototype.reconnect = function() {
+ if (this.disconnected && !this.destroyed) {
+ util.log('Attempting reconnection to server with ID ' + this._lastServerId);
+ this.disconnected = false;
+ this._initializeServerConnection();
+ this._initialize(this._lastServerId);
+ } else if (this.destroyed) {
+ throw new Error('This peer cannot reconnect to the server. It has already been destroyed.');
+ } else if (!this.disconnected && !this.open) {
+ // Do nothing. We're still connecting the first time.
+ util.error('In a hurry? We\'re still trying to make the initial connection!');
+ } else {
+ throw new Error('Peer ' + this.id + ' cannot reconnect because it is not disconnected from the server!');
+ }
+};
+
+/**
+ * Get a list of available peer IDs. If you're running your own server, you'll
+ * want to set allow_discovery: true in the PeerServer options. If you're using
+ * the cloud server, email team@peerjs.com to get the functionality enabled for
+ * your key.
+ */
+Peer.prototype.listAllPeers = function(cb) {
+ cb = cb || function() {};
+ var self = this;
+ var http = new XMLHttpRequest();
+ var protocol = this.options.secure ? 'https://' : 'http://';
+ var url = protocol + this.options.host + ':' + this.options.port
+ + this.options.path + this.options.key + '/peers';
+ var queryString = '?ts=' + new Date().getTime() + '' + Math.random();
+ url += queryString;
+
+ // If there's no ID we need to wait for one before trying to init socket.
+ http.open('get', url, true);
+ http.onerror = function(e) {
+ self._abort('server-error', 'Could not get peers from the server.');
+ cb([]);
+ }
+ http.onreadystatechange = function() {
+ if (http.readyState !== 4) {
+ return;
+ }
+ if (http.status === 401) {
+ var helpfulError = '';
+ if (self.options.host !== util.CLOUD_HOST) {
+ helpfulError = 'It looks like you\'re using the cloud server. You can email '
+ + 'team@peerjs.com to enable peer listing for your API key.';
+ } else {
+ helpfulError = 'You need to enable `allow_discovery` on your self-hosted'
+ + ' PeerServer to use this feature.';
+ }
+ throw new Error('It doesn\'t look like you have permission to list peers IDs. ' + helpfulError);
+ cb([]);
+ } else if (http.status !== 200) {
+ cb([]);
+ } else {
+ cb(JSON.parse(http.responseText));
+ }
+ };
+ http.send(null);
+}
+
+exports.Peer = Peer;
+/**
+ * Wraps a DataChannel between two Peers.
+ */
+function DataConnection(peer, provider, options) {
+ if (!(this instanceof DataConnection)) return new DataConnection(peer, provider, options);
+ EventEmitter.call(this);
+
+ this.options = util.extend({
+ serialization: 'binary',
+ reliable: false
+ }, options);
+
+ // Connection is not open yet.
+ this.open = false;
+ this.type = 'data';
+ this.peer = peer;
+ this.provider = provider;
+
+ this.id = this.options.connectionId || DataConnection._idPrefix + util.randomToken();
+
+ this.label = this.options.label || this.id;
+ this.metadata = this.options.metadata;
+ this.serialization = this.options.serialization;
+ this.reliable = this.options.reliable;
+
+ // Data channel buffering.
+ this._buffer = [];
+ this._buffering = false;
+ this.bufferSize = 0;
+
+ // For storing large data.
+ this._chunkedData = {};
+
+ if (this.options._payload) {
+ this._peerBrowser = this.options._payload.browser;
+ }
+
+ Negotiator.startConnection(
+ this,
+ this.options._payload || {
+ originator: true
+ }
+ );
+}
+
+util.inherits(DataConnection, EventEmitter);
+
+DataConnection._idPrefix = 'dc_';
+
+/** Called by the Negotiator when the DataChannel is ready. */
+DataConnection.prototype.initialize = function(dc) {
+ this._dc = this.dataChannel = dc;
+ this._configureDataChannel();
+}
+
+DataConnection.prototype._configureDataChannel = function() {
+ var self = this;
+ if (util.supports.sctp) {
+ this._dc.binaryType = 'arraybuffer';
+ }
+ this._dc.onopen = function() {
+ util.log('Data channel connection success');
+ self.open = true;
+ self.emit('open');
+ }
+
+ // Use the Reliable shim for non Firefox browsers
+ if (!util.supports.sctp && this.reliable) {
+ this._reliable = new Reliable(this._dc, util.debug);
+ }
+
+ if (this._reliable) {
+ this._reliable.onmessage = function(msg) {
+ self.emit('data', msg);
+ };
+ } else {
+ this._dc.onmessage = function(e) {
+ self._handleDataMessage(e);
+ };
+ }
+ this._dc.onclose = function(e) {
+ util.log('DataChannel closed for:', self.peer);
+ self.close();
+ };
+}
+
+// Handles a DataChannel message.
+DataConnection.prototype._handleDataMessage = function(e) {
+ var self = this;
+ var data = e.data;
+ var datatype = data.constructor;
+ if (this.serialization === 'binary' || this.serialization === 'binary-utf8') {
+ if (datatype === Blob) {
+ // Datatype should never be blob
+ util.blobToArrayBuffer(data, function(ab) {
+ data = util.unpack(ab);
+ self.emit('data', data);
+ });
+ return;
+ } else if (datatype === ArrayBuffer) {
+ data = util.unpack(data);
+ } else if (datatype === String) {
+ // String fallback for binary data for browsers that don't support binary yet
+ var ab = util.binaryStringToArrayBuffer(data);
+ data = util.unpack(ab);
+ }
+ } else if (this.serialization === 'json') {
+ data = JSON.parse(data);
+ }
+
+ // Check if we've chunked--if so, piece things back together.
+ // We're guaranteed that this isn't 0.
+ if (data.__peerData) {
+ var id = data.__peerData;
+ var chunkInfo = this._chunkedData[id] || {data: [], count: 0, total: data.total};
+
+ chunkInfo.data[data.n] = data.data;
+ chunkInfo.count += 1;
+
+ if (chunkInfo.total === chunkInfo.count) {
+ // Clean up before making the recursive call to `_handleDataMessage`.
+ delete this._chunkedData[id];
+
+ // We've received all the chunks--time to construct the complete data.
+ data = new Blob(chunkInfo.data);
+ this._handleDataMessage({data: data});
+ }
+
+ this._chunkedData[id] = chunkInfo;
+ return;
+ }
+
+ this.emit('data', data);
+}
+
+/**
+ * Exposed functionality for users.
+ */
+
+/** Allows user to close connection. */
+DataConnection.prototype.close = function() {
+ if (!this.open) {
+ return;
+ }
+ this.open = false;
+ Negotiator.cleanup(this);
+ this.emit('close');
+}
+
+/** Allows user to send data. */
+DataConnection.prototype.send = function(data, chunked) {
+ if (!this.open) {
+ this.emit('error', new Error('Connection is not open. You should listen for the `open` event before sending messages.'));
+ return;
+ }
+ if (this._reliable) {
+ // Note: reliable shim sending will make it so that you cannot customize
+ // serialization.
+ this._reliable.send(data);
+ return;
+ }
+ var self = this;
+ if (this.serialization === 'json') {
+ this._bufferedSend(JSON.stringify(data));
+ } else if (this.serialization === 'binary' || this.serialization === 'binary-utf8') {
+ var blob = util.pack(data);
+
+ // For Chrome-Firefox interoperability, we need to make Firefox "chunk"
+ // the data it sends out.
+ var needsChunking = util.chunkedBrowsers[this._peerBrowser] || util.chunkedBrowsers[util.browser];
+ if (needsChunking && !chunked && blob.size > util.chunkedMTU) {
+ this._sendChunks(blob);
+ return;
+ }
+
+ // DataChannel currently only supports strings.
+ if (!util.supports.sctp) {
+ util.blobToBinaryString(blob, function(str) {
+ self._bufferedSend(str);
+ });
+ } else if (!util.supports.binaryBlob) {
+ // We only do this if we really need to (e.g. blobs are not supported),
+ // because this conversion is costly.
+ util.blobToArrayBuffer(blob, function(ab) {
+ self._bufferedSend(ab);
+ });
+ } else {
+ this._bufferedSend(blob);
+ }
+ } else {
+ this._bufferedSend(data);
+ }
+}
+
+DataConnection.prototype._bufferedSend = function(msg) {
+ if (this._buffering || !this._trySend(msg)) {
+ this._buffer.push(msg);
+ this.bufferSize = this._buffer.length;
+ }
+}
+
+// Returns true if the send succeeds.
+DataConnection.prototype._trySend = function(msg) {
+ try {
+ this._dc.send(msg);
+ } catch (e) {
+ this._buffering = true;
+
+ var self = this;
+ setTimeout(function() {
+ // Try again.
+ self._buffering = false;
+ self._tryBuffer();
+ }, 100);
+ return false;
+ }
+ return true;
+}
+
+// Try to send the first message in the buffer.
+DataConnection.prototype._tryBuffer = function() {
+ if (this._buffer.length === 0) {
+ return;
+ }
+
+ var msg = this._buffer[0];
+
+ if (this._trySend(msg)) {
+ this._buffer.shift();
+ this.bufferSize = this._buffer.length;
+ this._tryBuffer();
+ }
+}
+
+DataConnection.prototype._sendChunks = function(blob) {
+ var blobs = util.chunk(blob);
+ for (var i = 0, ii = blobs.length; i < ii; i += 1) {
+ var blob = blobs[i];
+ this.send(blob, true);
+ }
+}
+
+DataConnection.prototype.handleMessage = function(message) {
+ var payload = message.payload;
+
+ switch (message.type) {
+ case 'ANSWER':
+ this._peerBrowser = payload.browser;
+
+ // Forward to negotiator
+ Negotiator.handleSDP(message.type, this, payload.sdp);
+ break;
+ case 'CANDIDATE':
+ Negotiator.handleCandidate(this, payload.candidate);
+ break;
+ default:
+ util.warn('Unrecognized message type:', message.type, 'from peer:', this.peer);
+ break;
+ }
+}
+/**
+ * Wraps the streaming interface between two Peers.
+ */
+function MediaConnection(peer, provider, options) {
+ if (!(this instanceof MediaConnection)) return new MediaConnection(peer, provider, options);
+ EventEmitter.call(this);
+
+ this.options = util.extend({}, options);
+
+ this.open = false;
+ this.type = 'media';
+ this.peer = peer;
+ this.provider = provider;
+ this.metadata = this.options.metadata;
+ this.localStream = this.options._stream;
+
+ this.id = this.options.connectionId || MediaConnection._idPrefix + util.randomToken();
+ if (this.localStream) {
+ Negotiator.startConnection(
+ this,
+ {_stream: this.localStream, originator: true}
+ );
+ }
+};
+
+util.inherits(MediaConnection, EventEmitter);
+
+MediaConnection._idPrefix = 'mc_';
+
+MediaConnection.prototype.addStream = function(remoteStream) {
+ util.log('Receiving stream', remoteStream);
+
+ this.remoteStream = remoteStream;
+ this.emit('stream', remoteStream); // Should we call this `open`?
+
+};
+
+MediaConnection.prototype.handleMessage = function(message) {
+ var payload = message.payload;
+
+ switch (message.type) {
+ case 'ANSWER':
+ // Forward to negotiator
+ Negotiator.handleSDP(message.type, this, payload.sdp);
+ this.open = true;
+ break;
+ case 'CANDIDATE':
+ Negotiator.handleCandidate(this, payload.candidate);
+ break;
+ default:
+ util.warn('Unrecognized message type:', message.type, 'from peer:', this.peer);
+ break;
+ }
+}
+
+MediaConnection.prototype.answer = function(stream) {
+ if (this.localStream) {
+ util.warn('Local stream already exists on this MediaConnection. Are you answering a call twice?');
+ return;
+ }
+
+ this.options._payload._stream = stream;
+
+ this.localStream = stream;
+ Negotiator.startConnection(
+ this,
+ this.options._payload
+ )
+ // Retrieve lost messages stored because PeerConnection not set up.
+ var messages = this.provider._getMessages(this.id);
+ for (var i = 0, ii = messages.length; i < ii; i += 1) {
+ this.handleMessage(messages[i]);
+ }
+ this.open = true;
+};
+
+/**
+ * Exposed functionality for users.
+ */
+
+/** Allows user to close connection. */
+MediaConnection.prototype.close = function() {
+ if (!this.open) {
+ return;
+ }
+ this.open = false;
+ Negotiator.cleanup(this);
+ this.emit('close')
+};
+/**
+ * Manages all negotiations between Peers.
+ */
+var Negotiator = {
+ pcs: {
+ data: {},
+ media: {}
+ }, // type => {peerId: {pc_id: pc}}.
+ //providers: {}, // provider's id => providers (there may be multiple providers/client.
+ queue: [] // connections that are delayed due to a PC being in use.
+}
+
+Negotiator._idPrefix = 'pc_';
+
+/** Returns a PeerConnection object set up correctly (for data, media). */
+Negotiator.startConnection = function(connection, options) {
+ var pc = Negotiator._getPeerConnection(connection, options);
+
+ if (connection.type === 'media' && options._stream) {
+ // Add the stream.
+ pc.addStream(options._stream);
+ }
+
+ // Set the connection's PC.
+ connection.pc = connection.peerConnection = pc;
+ // What do we need to do now?
+ if (options.originator) {
+ if (connection.type === 'data') {
+ // Create the datachannel.
+ var config = {};
+ // Dropping reliable:false support, since it seems to be crashing
+ // Chrome.
+ /*if (util.supports.sctp && !options.reliable) {
+ // If we have canonical reliable support...
+ config = {maxRetransmits: 0};
+ }*/
+ // Fallback to ensure older browsers don't crash.
+ if (!util.supports.sctp) {
+ config = {reliable: options.reliable};
+ }
+ var dc = pc.createDataChannel(connection.label, config);
+ connection.initialize(dc);
+ }
+
+ if (!util.supports.onnegotiationneeded) {
+ Negotiator._makeOffer(connection);
+ }
+ } else {
+ Negotiator.handleSDP('OFFER', connection, options.sdp);
+ }
+}
+
+Negotiator._getPeerConnection = function(connection, options) {
+ if (!Negotiator.pcs[connection.type]) {
+ util.error(connection.type + ' is not a valid connection type. Maybe you overrode the `type` property somewhere.');
+ }
+
+ if (!Negotiator.pcs[connection.type][connection.peer]) {
+ Negotiator.pcs[connection.type][connection.peer] = {};
+ }
+ var peerConnections = Negotiator.pcs[connection.type][connection.peer];
+
+ var pc;
+ // Not multiplexing while FF and Chrome have not-great support for it.
+ /*if (options.multiplex) {
+ ids = Object.keys(peerConnections);
+ for (var i = 0, ii = ids.length; i < ii; i += 1) {
+ pc = peerConnections[ids[i]];
+ if (pc.signalingState === 'stable') {
+ break; // We can go ahead and use this PC.
+ }
+ }
+ } else */
+ if (options.pc) { // Simplest case: PC id already provided for us.
+ pc = Negotiator.pcs[connection.type][connection.peer][options.pc];
+ }
+
+ if (!pc || pc.signalingState !== 'stable') {
+ pc = Negotiator._startPeerConnection(connection);
+ }
+ return pc;
+}
+
+/*
+Negotiator._addProvider = function(provider) {
+ if ((!provider.id && !provider.disconnected) || !provider.socket.open) {
+ // Wait for provider to obtain an ID.
+ provider.on('open', function(id) {
+ Negotiator._addProvider(provider);
+ });
+ } else {
+ Negotiator.providers[provider.id] = provider;
+ }
+}*/
+
+
+/** Start a PC. */
+Negotiator._startPeerConnection = function(connection) {
+ util.log('Creating RTCPeerConnection.');
+
+ var id = Negotiator._idPrefix + util.randomToken();
+ var optional = {};
+
+ if (connection.type === 'data' && !util.supports.sctp) {
+ optional = {optional: [{RtpDataChannels: true}]};
+ } else if (connection.type === 'media') {
+ // Interop req for chrome.
+ optional = {optional: [{DtlsSrtpKeyAgreement: true}]};
+ }
+
+ var pc = new RTCPeerConnection(connection.provider.options.config, optional);
+ Negotiator.pcs[connection.type][connection.peer][id] = pc;
+
+ Negotiator._setupListeners(connection, pc, id);
+
+ return pc;
+}
+
+/** Set up various WebRTC listeners. */
+Negotiator._setupListeners = function(connection, pc, pc_id) {
+ var peerId = connection.peer;
+ var connectionId = connection.id;
+ var provider = connection.provider;
+
+ // ICE CANDIDATES.
+ util.log('Listening for ICE candidates.');
+ pc.onicecandidate = function(evt) {
+ if (evt.candidate) {
+ util.log('Received ICE candidates for:', connection.peer);
+ provider.socket.send({
+ type: 'CANDIDATE',
+ payload: {
+ candidate: evt.candidate,
+ type: connection.type,
+ connectionId: connection.id
+ },
+ dst: peerId
+ });
+ }
+ };
+
+ pc.oniceconnectionstatechange = function() {
+ switch (pc.iceConnectionState) {
+ case 'disconnected':
+ case 'failed':
+ util.log('iceConnectionState is disconnected, closing connections to ' + peerId);
+ connection.close();
+ break;
+ case 'completed':
+ pc.onicecandidate = util.noop;
+ break;
+ }
+ };
+
+ // Fallback for older Chrome impls.
+ pc.onicechange = pc.oniceconnectionstatechange;
+
+ // ONNEGOTIATIONNEEDED (Chrome)
+ util.log('Listening for `negotiationneeded`');
+ pc.onnegotiationneeded = function() {
+ util.log('`negotiationneeded` triggered');
+ if (pc.signalingState == 'stable') {
+ Negotiator._makeOffer(connection);
+ } else {
+ util.log('onnegotiationneeded triggered when not stable. Is another connection being established?');
+ }
+ };
+
+ // DATACONNECTION.
+ util.log('Listening for data channel');
+ // Fired between offer and answer, so options should already be saved
+ // in the options hash.
+ pc.ondatachannel = function(evt) {
+ util.log('Received data channel');
+ var dc = evt.channel;
+ var connection = provider.getConnection(peerId, connectionId);
+ connection.initialize(dc);
+ };
+
+ // MEDIACONNECTION.
+ util.log('Listening for remote stream');
+ pc.onaddstream = function(evt) {
+ util.log('Received remote stream');
+ var stream = evt.stream;
+ provider.getConnection(peerId, connectionId).addStream(stream);
+ };
+}
+
+Negotiator.cleanup = function(connection) {
+ util.log('Cleaning up PeerConnection to ' + connection.peer);
+
+ var pc = connection.pc;
+
+ if (!!pc && (pc.readyState !== 'closed' || pc.signalingState !== 'closed')) {
+ pc.close();
+ connection.pc = null;
+ }
+}
+
+Negotiator._makeOffer = function(connection) {
+ var pc = connection.pc;
+ pc.createOffer(function(offer) {
+ util.log('Created offer.');
+
+ if (!util.supports.sctp && connection.type === 'data' && connection.reliable) {
+ offer.sdp = Reliable.higherBandwidthSDP(offer.sdp);
+ }
+
+ pc.setLocalDescription(offer, function() {
+ util.log('Set localDescription: offer', 'for:', connection.peer);
+ connection.provider.socket.send({
+ type: 'OFFER',
+ payload: {
+ sdp: offer,
+ type: connection.type,
+ label: connection.label,
+ connectionId: connection.id,
+ reliable: connection.reliable,
+ serialization: connection.serialization,
+ metadata: connection.metadata,
+ browser: util.browser
+ },
+ dst: connection.peer
+ });
+ }, function(err) {
+ connection.provider.emitError('webrtc', err);
+ util.log('Failed to setLocalDescription, ', err);
+ });
+ }, function(err) {
+ connection.provider.emitError('webrtc', err);
+ util.log('Failed to createOffer, ', err);
+ }, connection.options.constraints);
+}
+
+Negotiator._makeAnswer = function(connection) {
+ var pc = connection.pc;
+
+ pc.createAnswer(function(answer) {
+ util.log('Created answer.');
+
+ if (!util.supports.sctp && connection.type === 'data' && connection.reliable) {
+ answer.sdp = Reliable.higherBandwidthSDP(answer.sdp);
+ }
+
+ pc.setLocalDescription(answer, function() {
+ util.log('Set localDescription: answer', 'for:', connection.peer);
+ connection.provider.socket.send({
+ type: 'ANSWER',
+ payload: {
+ sdp: answer,
+ type: connection.type,
+ connectionId: connection.id,
+ browser: util.browser
+ },
+ dst: connection.peer
+ });
+ }, function(err) {
+ connection.provider.emitError('webrtc', err);
+ util.log('Failed to setLocalDescription, ', err);
+ });
+ }, function(err) {
+ connection.provider.emitError('webrtc', err);
+ util.log('Failed to create answer, ', err);
+ });
+}
+
+/** Handle an SDP. */
+Negotiator.handleSDP = function(type, connection, sdp) {
+ sdp = new RTCSessionDescription(sdp);
+ var pc = connection.pc;
+
+ util.log('Setting remote description', sdp);
+ pc.setRemoteDescription(sdp, function() {
+ util.log('Set remoteDescription:', type, 'for:', connection.peer);
+
+ if (type === 'OFFER') {
+ Negotiator._makeAnswer(connection);
+ }
+ }, function(err) {
+ connection.provider.emitError('webrtc', err);
+ util.log('Failed to setRemoteDescription, ', err);
+ });
+}
+
+/** Handle a candidate. */
+Negotiator.handleCandidate = function(connection, ice) {
+ var candidate = ice.candidate;
+ var sdpMLineIndex = ice.sdpMLineIndex;
+ connection.pc.addIceCandidate(new RTCIceCandidate({
+ sdpMLineIndex: sdpMLineIndex,
+ candidate: candidate
+ }));
+ util.log('Added ICE candidate for:', connection.peer);
+}
+/**
+ * An abstraction on top of WebSockets and XHR streaming to provide fastest
+ * possible connection for peers.
+ */
+function Socket(secure, host, port, path, key) {
+ if (!(this instanceof Socket)) return new Socket(secure, host, port, path, key);
+
+ EventEmitter.call(this);
+
+ // Disconnected manually.
+ this.disconnected = false;
+ this._queue = [];
+
+ var httpProtocol = secure ? 'https://' : 'http://';
+ var wsProtocol = secure ? 'wss://' : 'ws://';
+ this._httpUrl = httpProtocol + host + ':' + port + path + key;
+ this._wsUrl = wsProtocol + host + ':' + port + path + 'peerjs?key=' + key;
+}
+
+util.inherits(Socket, EventEmitter);
+
+
+/** Check in with ID or get one from server. */
+Socket.prototype.start = function(id, token) {
+ this.id = id;
+
+ this._httpUrl += '/' + id + '/' + token;
+ this._wsUrl += '&id=' + id + '&token=' + token;
+
+ this._startXhrStream();
+ this._startWebSocket();
+}
+
+
+/** Start up websocket communications. */
+Socket.prototype._startWebSocket = function(id) {
+ var self = this;
+
+ if (this._socket) {
+ return;
+ }
+
+ this._socket = new WebSocket(this._wsUrl);
+
+ this._socket.onmessage = function(event) {
+ try {
+ var data = JSON.parse(event.data);
+ self.emit('message', data);
+ } catch(e) {
+ util.log('Invalid server message', event.data);
+ return;
+ }
+ };
+
+ this._socket.onclose = function(event) {
+ util.log('Socket closed.');
+ self.disconnected = true;
+ self.emit('disconnected');
+ };
+
+ // Take care of the queue of connections if necessary and make sure Peer knows
+ // socket is open.
+ this._socket.onopen = function() {
+ if (self._timeout) {
+ clearTimeout(self._timeout);
+ setTimeout(function(){
+ self._http.abort();
+ self._http = null;
+ }, 5000);
+ }
+ self._sendQueuedMessages();
+ util.log('Socket open');
+ };
+}
+
+/** Start XHR streaming. */
+Socket.prototype._startXhrStream = function(n) {
+ try {
+ var self = this;
+ this._http = new XMLHttpRequest();
+ this._http._index = 1;
+ this._http._streamIndex = n || 0;
+ this._http.open('post', this._httpUrl + '/id?i=' + this._http._streamIndex, true);
+ this._http.onreadystatechange = function() {
+ if (this.readyState == 2 && this.old) {
+ this.old.abort();
+ delete this.old;
+ } else if (this.readyState > 2 && this.status === 200 && this.responseText) {
+ self._handleStream(this);
+ } else if (this.status !== 200) {
+ // If we get a different status code, likely something went wrong.
+ // Stop streaming.
+ clearTimeout(self._timeout);
+ self.emit('disconnected');
+ }
+ };
+ this._http.send(null);
+ this._setHTTPTimeout();
+ } catch(e) {
+ util.log('XMLHttpRequest not available; defaulting to WebSockets');
+ }
+}
+
+
+/** Handles onreadystatechange response as a stream. */
+Socket.prototype._handleStream = function(http) {
+ // 3 and 4 are loading/done state. All others are not relevant.
+ var messages = http.responseText.split('\n');
+
+ // Check to see if anything needs to be processed on buffer.
+ if (http._buffer) {
+ while (http._buffer.length > 0) {
+ var index = http._buffer.shift();
+ var bufferedMessage = messages[index];
+ try {
+ bufferedMessage = JSON.parse(bufferedMessage);
+ } catch(e) {
+ http._buffer.shift(index);
+ break;
+ }
+ this.emit('message', bufferedMessage);
+ }
+ }
+
+ var message = messages[http._index];
+ if (message) {
+ http._index += 1;
+ // Buffering--this message is incomplete and we'll get to it next time.
+ // This checks if the httpResponse ended in a `\n`, in which case the last
+ // element of messages should be the empty string.
+ if (http._index === messages.length) {
+ if (!http._buffer) {
+ http._buffer = [];
+ }
+ http._buffer.push(http._index - 1);
+ } else {
+ try {
+ message = JSON.parse(message);
+ } catch(e) {
+ util.log('Invalid server message', message);
+ return;
+ }
+ this.emit('message', message);
+ }
+ }
+}
+
+Socket.prototype._setHTTPTimeout = function() {
+ var self = this;
+ this._timeout = setTimeout(function() {
+ var old = self._http;
+ if (!self._wsOpen()) {
+ self._startXhrStream(old._streamIndex + 1);
+ self._http.old = old;
+ } else {
+ old.abort();
+ }
+ }, 25000);
+}
+
+/** Is the websocket currently open? */
+Socket.prototype._wsOpen = function() {
+ return this._socket && this._socket.readyState == 1;
+}
+
+/** Send queued messages. */
+Socket.prototype._sendQueuedMessages = function() {
+ for (var i = 0, ii = this._queue.length; i < ii; i += 1) {
+ this.send(this._queue[i]);
+ }
+}
+
+/** Exposed send for DC & Peer. */
+Socket.prototype.send = function(data) {
+ if (this.disconnected) {
+ return;
+ }
+
+ // If we didn't get an ID yet, we can't yet send anything so we should queue
+ // up these messages.
+ if (!this.id) {
+ this._queue.push(data);
+ return;
+ }
+
+ if (!data.type) {
+ this.emit('error', 'Invalid message');
+ return;
+ }
+
+ var message = JSON.stringify(data);
+ if (this._wsOpen()) {
+ this._socket.send(message);
+ } else {
+ var http = new XMLHttpRequest();
+ var url = this._httpUrl + '/' + data.type.toLowerCase();
+ http.open('post', url, true);
+ http.setRequestHeader('Content-Type', 'application/json');
+ http.send(message);
+ }
+}
+
+Socket.prototype.close = function() {
+ if (!this.disconnected && this._wsOpen()) {
+ this._socket.close();
+ this.disconnected = true;
+ }
+}
+
+})(this);
diff --git a/promise-1.0.0.js b/promise-1.0.0.js
new file mode 100644
index 0000000..5619cfa
--- /dev/null
+++ b/promise-1.0.0.js
@@ -0,0 +1,684 @@
+(function() {
+var define, requireModule, require, requirejs;
+
+(function() {
+ var registry = {}, seen = {};
+
+ define = function(name, deps, callback) {
+ registry[name] = { deps: deps, callback: callback };
+ };
+
+ requirejs = require = requireModule = function(name) {
+ requirejs._eak_seen = registry;
+
+ if (seen[name]) { return seen[name]; }
+ seen[name] = {};
+
+ if (!registry[name]) {
+ throw new Error("Could not find module " + name);
+ }
+
+ var mod = registry[name],
+ deps = mod.deps,
+ callback = mod.callback,
+ reified = [],
+ exports;
+
+ for (var i=0, l=deps.length; i<l; i++) {
+ if (deps[i] === 'exports') {
+ reified.push(exports = {});
+ } else {
+ reified.push(requireModule(resolve(deps[i])));
+ }
+ }
+
+ var value = callback.apply(this, reified);
+ return seen[name] = exports || value;
+
+ function resolve(child) {
+ if (child.charAt(0) !== '.') { return child; }
+ var parts = child.split("/");
+ var parentBase = name.split("/").slice(0, -1);
+
+ for (var i=0, l=parts.length; i<l; i++) {
+ var part = parts[i];
+
+ if (part === '..') { parentBase.pop(); }
+ else if (part === '.') { continue; }
+ else { parentBase.push(part); }
+ }
+
+ return parentBase.join("/");
+ }
+ };
+})();
+
+define("promise/all",
+ ["./utils","exports"],
+ function(__dependency1__, __exports__) {
+ "use strict";
+ /* global toString */
+
+ var isArray = __dependency1__.isArray;
+ var isFunction = __dependency1__.isFunction;
+
+ /**
+ Returns a promise that is fulfilled when all the given promises have been
+ fulfilled, or rejected if any of them become rejected. The return promise
+ is fulfilled with an array that gives all the values in the order they were
+ passed in the `promises` array argument.
+
+ Example:
+
+ ```javascript
+ var promise1 = RSVP.resolve(1);
+ var promise2 = RSVP.resolve(2);
+ var promise3 = RSVP.resolve(3);
+ var promises = [ promise1, promise2, promise3 ];
+
+ RSVP.all(promises).then(function(array){
+ // The array here would be [ 1, 2, 3 ];
+ });
+ ```
+
+ If any of the `promises` given to `RSVP.all` are rejected, the first promise
+ that is rejected will be given as an argument to the returned promises's
+ rejection handler. For example:
+
+ Example:
+
+ ```javascript
+ var promise1 = RSVP.resolve(1);
+ var promise2 = RSVP.reject(new Error("2"));
+ var promise3 = RSVP.reject(new Error("3"));
+ var promises = [ promise1, promise2, promise3 ];
+
+ RSVP.all(promises).then(function(array){
+ // Code here never runs because there are rejected promises!
+ }, function(error) {
+ // error.message === "2"
+ });
+ ```
+
+ @method all
+ @for RSVP
+ @param {Array} promises
+ @param {String} label
+ @return {Promise} promise that is fulfilled when all `promises` have been
+ fulfilled, or rejected if any of them become rejected.
+ */
+ function all(promises) {
+ /*jshint validthis:true */
+ var Promise = this;
+
+ if (!isArray(promises)) {
+ throw new TypeError('You must pass an array to all.');
+ }
+
+ return new Promise(function(resolve, reject) {
+ var results = [], remaining = promises.length,
+ promise;
+
+ if (remaining === 0) {
+ resolve([]);
+ }
+
+ function resolver(index) {
+ return function(value) {
+ resolveAll(index, value);
+ };
+ }
+
+ function resolveAll(index, value) {
+ results[index] = value;
+ if (--remaining === 0) {
+ resolve(results);
+ }
+ }
+
+ for (var i = 0; i < promises.length; i++) {
+ promise = promises[i];
+
+ if (promise && isFunction(promise.then)) {
+ promise.then(resolver(i), reject);
+ } else {
+ resolveAll(i, promise);
+ }
+ }
+ });
+ }
+
+ __exports__.all = all;
+ });
+define("promise/asap",
+ ["exports"],
+ function(__exports__) {
+ "use strict";
+ var browserGlobal = (typeof window !== 'undefined') ? window : {};
+ var BrowserMutationObserver = browserGlobal.MutationObserver || browserGlobal.WebKitMutationObserver;
+ var local = (typeof global !== 'undefined') ? global : (this === undefined? window:this);
+
+ // node
+ function useNextTick() {
+ return function() {
+ process.nextTick(flush);
+ };
+ }
+
+ function useMutationObserver() {
+ var iterations = 0;
+ var observer = new BrowserMutationObserver(flush);
+ var node = document.createTextNode('');
+ observer.observe(node, { characterData: true });
+
+ return function() {
+ node.data = (iterations = ++iterations % 2);
+ };
+ }
+
+ function useSetTimeout() {
+ return function() {
+ local.setTimeout(flush, 1);
+ };
+ }
+
+ var queue = [];
+ function flush() {
+ for (var i = 0; i < queue.length; i++) {
+ var tuple = queue[i];
+ var callback = tuple[0], arg = tuple[1];
+ callback(arg);
+ }
+ queue = [];
+ }
+
+ var scheduleFlush;
+
+ // Decide what async method to use to triggering processing of queued callbacks:
+ if (typeof process !== 'undefined' && {}.toString.call(process) === '[object process]') {
+ scheduleFlush = useNextTick();
+ } else if (BrowserMutationObserver) {
+ scheduleFlush = useMutationObserver();
+ } else {
+ scheduleFlush = useSetTimeout();
+ }
+
+ function asap(callback, arg) {
+ var length = queue.push([callback, arg]);
+ if (length === 1) {
+ // If length is 1, that means that we need to schedule an async flush.
+ // If additional callbacks are queued before the queue is flushed, they
+ // will be processed by this flush that we are scheduling.
+ scheduleFlush();
+ }
+ }
+
+ __exports__.asap = asap;
+ });
+define("promise/config",
+ ["exports"],
+ function(__exports__) {
+ "use strict";
+ var config = {
+ instrument: false
+ };
+
+ function configure(name, value) {
+ if (arguments.length === 2) {
+ config[name] = value;
+ } else {
+ return config[name];
+ }
+ }
+
+ __exports__.config = config;
+ __exports__.configure = configure;
+ });
+define("promise/polyfill",
+ ["./promise","./utils","exports"],
+ function(__dependency1__, __dependency2__, __exports__) {
+ "use strict";
+ /*global self*/
+ var RSVPPromise = __dependency1__.Promise;
+ var isFunction = __dependency2__.isFunction;
+
+ function polyfill() {
+ var local;
+
+ if (typeof global !== 'undefined') {
+ local = global;
+ } else if (typeof window !== 'undefined' && window.document) {
+ local = window;
+ } else {
+ local = self;
+ }
+
+ var es6PromiseSupport =
+ "Promise" in local &&
+ // Some of these methods are missing from
+ // Firefox/Chrome experimental implementations
+ "resolve" in local.Promise &&
+ "reject" in local.Promise &&
+ "all" in local.Promise &&
+ "race" in local.Promise &&
+ // Older version of the spec had a resolver object
+ // as the arg rather than a function
+ (function() {
+ var resolve;
+ new local.Promise(function(r) { resolve = r; });
+ return isFunction(resolve);
+ }());
+
+ if (!es6PromiseSupport) {
+ local.Promise = RSVPPromise;
+ }
+ }
+
+ __exports__.polyfill = polyfill;
+ });
+define("promise/promise",
+ ["./config","./utils","./all","./race","./resolve","./reject","./asap","exports"],
+ function(__dependency1__, __dependency2__, __dependency3__, __dependency4__, __dependency5__, __dependency6__, __dependency7__, __exports__) {
+ "use strict";
+ var config = __dependency1__.config;
+ var configure = __dependency1__.configure;
+ var objectOrFunction = __dependency2__.objectOrFunction;
+ var isFunction = __dependency2__.isFunction;
+ var now = __dependency2__.now;
+ var all = __dependency3__.all;
+ var race = __dependency4__.race;
+ var staticResolve = __dependency5__.resolve;
+ var staticReject = __dependency6__.reject;
+ var asap = __dependency7__.asap;
+
+ var counter = 0;
+
+ config.async = asap; // default async is asap;
+
+ function Promise(resolver) {
+ if (!isFunction(resolver)) {
+ throw new TypeError('You must pass a resolver function as the first argument to the promise constructor');
+ }
+
+ if (!(this instanceof Promise)) {
+ throw new TypeError("Failed to construct 'Promise': Please use the 'new' operator, this object constructor cannot be called as a function.");
+ }
+
+ this._subscribers = [];
+
+ invokeResolver(resolver, this);
+ }
+
+ function invokeResolver(resolver, promise) {
+ function resolvePromise(value) {
+ resolve(promise, value);
+ }
+
+ function rejectPromise(reason) {
+ reject(promise, reason);
+ }
+
+ try {
+ resolver(resolvePromise, rejectPromise);
+ } catch(e) {
+ rejectPromise(e);
+ }
+ }
+
+ function invokeCallback(settled, promise, callback, detail) {
+ var hasCallback = isFunction(callback),
+ value, error, succeeded, failed;
+
+ if (hasCallback) {
+ try {
+ value = callback(detail);
+ succeeded = true;
+ } catch(e) {
+ failed = true;
+ error = e;
+ }
+ } else {
+ value = detail;
+ succeeded = true;
+ }
+
+ if (handleThenable(promise, value)) {
+ return;
+ } else if (hasCallback && succeeded) {
+ resolve(promise, value);
+ } else if (failed) {
+ reject(promise, error);
+ } else if (settled === FULFILLED) {
+ resolve(promise, value);
+ } else if (settled === REJECTED) {
+ reject(promise, value);
+ }
+ }
+
+ var PENDING = void 0;
+ var SEALED = 0;
+ var FULFILLED = 1;
+ var REJECTED = 2;
+
+ function subscribe(parent, child, onFulfillment, onRejection) {
+ var subscribers = parent._subscribers;
+ var length = subscribers.length;
+
+ subscribers[length] = child;
+ subscribers[length + FULFILLED] = onFulfillment;
+ subscribers[length + REJECTED] = onRejection;
+ }
+
+ function publish(promise, settled) {
+ var child, callback, subscribers = promise._subscribers, detail = promise._detail;
+
+ for (var i = 0; i < subscribers.length; i += 3) {
+ child = subscribers[i];
+ callback = subscribers[i + settled];
+
+ invokeCallback(settled, child, callback, detail);
+ }
+
+ promise._subscribers = null;
+ }
+
+ Promise.prototype = {
+ constructor: Promise,
+
+ _state: undefined,
+ _detail: undefined,
+ _subscribers: undefined,
+
+ then: function(onFulfillment, onRejection) {
+ var promise = this;
+
+ var thenPromise = new this.constructor(function() {});
+
+ if (this._state) {
+ var callbacks = arguments;
+ config.async(function invokePromiseCallback() {
+ invokeCallback(promise._state, thenPromise, callbacks[promise._state - 1], promise._detail);
+ });
+ } else {
+ subscribe(this, thenPromise, onFulfillment, onRejection);
+ }
+
+ return thenPromise;
+ },
+
+ 'catch': function(onRejection) {
+ return this.then(null, onRejection);
+ }
+ };
+
+ Promise.all = all;
+ Promise.race = race;
+ Promise.resolve = staticResolve;
+ Promise.reject = staticReject;
+
+ function handleThenable(promise, value) {
+ var then = null,
+ resolved;
+
+ try {
+ if (promise === value) {
+ throw new TypeError("A promises callback cannot return that same promise.");
+ }
+
+ if (objectOrFunction(value)) {
+ then = value.then;
+
+ if (isFunction(then)) {
+ then.call(value, function(val) {
+ if (resolved) { return true; }
+ resolved = true;
+
+ if (value !== val) {
+ resolve(promise, val);
+ } else {
+ fulfill(promise, val);
+ }
+ }, function(val) {
+ if (resolved) { return true; }
+ resolved = true;
+
+ reject(promise, val);
+ });
+
+ return true;
+ }
+ }
+ } catch (error) {
+ if (resolved) { return true; }
+ reject(promise, error);
+ return true;
+ }
+
+ return false;
+ }
+
+ function resolve(promise, value) {
+ if (promise === value) {
+ fulfill(promise, value);
+ } else if (!handleThenable(promise, value)) {
+ fulfill(promise, value);
+ }
+ }
+
+ function fulfill(promise, value) {
+ if (promise._state !== PENDING) { return; }
+ promise._state = SEALED;
+ promise._detail = value;
+
+ config.async(publishFulfillment, promise);
+ }
+
+ function reject(promise, reason) {
+ if (promise._state !== PENDING) { return; }
+ promise._state = SEALED;
+ promise._detail = reason;
+
+ config.async(publishRejection, promise);
+ }
+
+ function publishFulfillment(promise) {
+ publish(promise, promise._state = FULFILLED);
+ }
+
+ function publishRejection(promise) {
+ publish(promise, promise._state = REJECTED);
+ }
+
+ __exports__.Promise = Promise;
+ });
+define("promise/race",
+ ["./utils","exports"],
+ function(__dependency1__, __exports__) {
+ "use strict";
+ /* global toString */
+ var isArray = __dependency1__.isArray;
+
+ /**
+ `RSVP.race` allows you to watch a series of promises and act as soon as the
+ first promise given to the `promises` argument fulfills or rejects.
+
+ Example:
+
+ ```javascript
+ var promise1 = new RSVP.Promise(function(resolve, reject){
+ setTimeout(function(){
+ resolve("promise 1");
+ }, 200);
+ });
+
+ var promise2 = new RSVP.Promise(function(resolve, reject){
+ setTimeout(function(){
+ resolve("promise 2");
+ }, 100);
+ });
+
+ RSVP.race([promise1, promise2]).then(function(result){
+ // result === "promise 2" because it was resolved before promise1
+ // was resolved.
+ });
+ ```
+
+ `RSVP.race` is deterministic in that only the state of the first completed
+ promise matters. For example, even if other promises given to the `promises`
+ array argument are resolved, but the first completed promise has become
+ rejected before the other promises became fulfilled, the returned promise
+ will become rejected:
+
+ ```javascript
+ var promise1 = new RSVP.Promise(function(resolve, reject){
+ setTimeout(function(){
+ resolve("promise 1");
+ }, 200);
+ });
+
+ var promise2 = new RSVP.Promise(function(resolve, reject){
+ setTimeout(function(){
+ reject(new Error("promise 2"));
+ }, 100);
+ });
+
+ RSVP.race([promise1, promise2]).then(function(result){
+ // Code here never runs because there are rejected promises!
+ }, function(reason){
+ // reason.message === "promise2" because promise 2 became rejected before
+ // promise 1 became fulfilled
+ });
+ ```
+
+ @method race
+ @for RSVP
+ @param {Array} promises array of promises to observe
+ @param {String} label optional string for describing the promise returned.
+ Useful for tooling.
+ @return {Promise} a promise that becomes fulfilled with the value the first
+ completed promises is resolved with if the first completed promise was
+ fulfilled, or rejected with the reason that the first completed promise
+ was rejected with.
+ */
+ function race(promises) {
+ /*jshint validthis:true */
+ var Promise = this;
+
+ if (!isArray(promises)) {
+ throw new TypeError('You must pass an array to race.');
+ }
+ return new Promise(function(resolve, reject) {
+ var results = [], promise;
+
+ for (var i = 0; i < promises.length; i++) {
+ promise = promises[i];
+
+ if (promise && typeof promise.then === 'function') {
+ promise.then(resolve, reject);
+ } else {
+ resolve(promise);
+ }
+ }
+ });
+ }
+
+ __exports__.race = race;
+ });
+define("promise/reject",
+ ["exports"],
+ function(__exports__) {
+ "use strict";
+ /**
+ `RSVP.reject` returns a promise that will become rejected with the passed
+ `reason`. `RSVP.reject` is essentially shorthand for the following:
+
+ ```javascript
+ var promise = new RSVP.Promise(function(resolve, reject){
+ reject(new Error('WHOOPS'));
+ });
+
+ promise.then(function(value){
+ // Code here doesn't run because the promise is rejected!
+ }, function(reason){
+ // reason.message === 'WHOOPS'
+ });
+ ```
+
+ Instead of writing the above, your code now simply becomes the following:
+
+ ```javascript
+ var promise = RSVP.reject(new Error('WHOOPS'));
+
+ promise.then(function(value){
+ // Code here doesn't run because the promise is rejected!
+ }, function(reason){
+ // reason.message === 'WHOOPS'
+ });
+ ```
+
+ @method reject
+ @for RSVP
+ @param {Any} reason value that the returned promise will be rejected with.
+ @param {String} label optional string for identifying the returned promise.
+ Useful for tooling.
+ @return {Promise} a promise that will become rejected with the given
+ `reason`.
+ */
+ function reject(reason) {
+ /*jshint validthis:true */
+ var Promise = this;
+
+ return new Promise(function (resolve, reject) {
+ reject(reason);
+ });
+ }
+
+ __exports__.reject = reject;
+ });
+define("promise/resolve",
+ ["exports"],
+ function(__exports__) {
+ "use strict";
+ function resolve(value) {
+ /*jshint validthis:true */
+ if (value && typeof value === 'object' && value.constructor === this) {
+ return value;
+ }
+
+ var Promise = this;
+
+ return new Promise(function(resolve) {
+ resolve(value);
+ });
+ }
+
+ __exports__.resolve = resolve;
+ });
+define("promise/utils",
+ ["exports"],
+ function(__exports__) {
+ "use strict";
+ function objectOrFunction(x) {
+ return isFunction(x) || (typeof x === "object" && x !== null);
+ }
+
+ function isFunction(x) {
+ return typeof x === "function";
+ }
+
+ function isArray(x) {
+ return Object.prototype.toString.call(x) === "[object Array]";
+ }
+
+ // Date.now is not available in browsers < IE9
+ // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date/now#Compatibility
+ var now = Date.now || function() { return new Date().getTime(); };
+
+
+ __exports__.objectOrFunction = objectOrFunction;
+ __exports__.isFunction = isFunction;
+ __exports__.isArray = isArray;
+ __exports__.now = now;
+ });
+requireModule('promise/polyfill').polyfill();
+}()); \ No newline at end of file
diff --git a/snap.html b/snap.html
index e3c54dc..9178719 100755
--- a/snap.html
+++ b/snap.html
@@ -2,7 +2,7 @@
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
- <title>Snap! Build Your Own Blocks. Beta</title>
+ <title>Snap! Build Your Own Blocks. Development (OOP)</title>
<link rel="shortcut icon" href="favicon.ico">
<script type="text/javascript" src="morphic.js"></script>
<script type="text/javascript" src="widgets.js"></script>
@@ -17,13 +17,21 @@
<script type="text/javascript" src="store.js"></script>
<script type="text/javascript" src="locale.js"></script>
<script type="text/javascript" src="cloud.js"></script>
+ <script type="text/javascript" src="promise-1.0.0.js"></script>
+ <script type="text/javascript" src="octokit.js/octokit.js"></script>
+ <script type="text/javascript" src="github.js"></script>
<script type="text/javascript" src="sha512.js"></script>
-
+ <script type="text/javascript" src="Snapin8r/snapin8r.min.js"></script>
+ <script type="text/javascript" src="vkBeautify/vkbeautify.js"></script>
+ <script type="text/javascript" src="diff-merge/dist/diff.js"></script>
+ <script type="text/javascript" src="peer.js"></script>
<script type="text/javascript">
var world;
window.onload = function () {
world = new WorldMorph(document.getElementById('world'));
world.worldCanvas.focus();
+ new IDE_Morph().openIn(world);
+ setInterval(loop, 10);
var ide = new IDE_Morph()
ide.openIn(world);
setInterval(loop, 1);
diff --git a/store.js b/store.js
index b17fb49..4fe8fb0 100644
--- a/store.js
+++ b/store.js
@@ -61,7 +61,7 @@ SyntaxElementMorph, Variable*/
// Global stuff ////////////////////////////////////////////////////////
-modules.store = '2015-February-28';
+modules.store = '2015-March-24';
// XML_Serializer ///////////////////////////////////////////////////////
@@ -97,7 +97,7 @@ XML_Serializer.prototype.serialize = function (object) {
this.flushMedia();
xml = this.store(object);
this.flush();
- return xml;
+ return vkbeautify.xml(xml);
};
XML_Serializer.prototype.store = function (object, mediaID) {
@@ -135,7 +135,7 @@ XML_Serializer.prototype.mediaXML = function () {
);
xml = xml + str;
});
- return xml + '</media>';
+ return vkbeautify.xml(xml + '</media>');
};
XML_Serializer.prototype.add = function (object) {
@@ -459,9 +459,17 @@ SnapSerializer.prototype.rawLoadProjectModel = function (xmlNode) {
myself.loadValue(model);
});
- // restore nesting associations
+ // restore inheritance and nesting associations
myself.project.stage.children.forEach(function (sprite) {
- var anchor;
+ var exemplar, anchor;
+ if (sprite.inheritanceInfo) { // only sprites can inherit
+ exemplar = myself.project.sprites[
+ sprite.inheritanceInfo.exemplar
+ ];
+ if (exemplar) {
+ sprite.setExemplar(exemplar);
+ }
+ }
if (sprite.nestingInfo) { // only sprites may have nesting info
anchor = myself.project.sprites[sprite.nestingInfo.anchor];
if (anchor) {
@@ -471,6 +479,7 @@ SnapSerializer.prototype.rawLoadProjectModel = function (xmlNode) {
}
});
myself.project.stage.children.forEach(function (sprite) {
+ delete sprite.inheritanceInfo;
if (sprite.nestingInfo) { // only sprites may have nesting info
sprite.nestingScale = +(sprite.nestingInfo.scale || sprite.scale);
delete sprite.nestingInfo;
@@ -531,8 +540,8 @@ SnapSerializer.prototype.rawLoadProjectModel = function (xmlNode) {
}
watcher.setStyle(model.attributes.style || 'normal');
if (watcher.style === 'slider') {
- watcher.setSliderMin(model.attributes.min || '1');
- watcher.setSliderMax(model.attributes.max || '100');
+ watcher.setSliderMin(model.attributes.min || '1', true);
+ watcher.setSliderMax(model.attributes.max || '100', true);
}
watcher.setPosition(
project.stage.topLeft().add(new Point(
@@ -639,9 +648,17 @@ SnapSerializer.prototype.loadSprites = function (xmlString, ide) {
myself.loadObject(sprite, model);
});
- // restore nesting associations
+ // restore inheritance and nesting associations
project.stage.children.forEach(function (sprite) {
- var anchor;
+ var exemplar, anchor;
+ if (sprite.inheritanceInfo) { // only sprites can inherit
+ exemplar = project.sprites[
+ sprite.inheritanceInfo.exemplar
+ ];
+ if (exemplar) {
+ sprite.setExemplar(exemplar);
+ }
+ }
if (sprite.nestingInfo) { // only sprites may have nesting info
anchor = project.sprites[sprite.nestingInfo.anchor];
if (anchor) {
@@ -651,6 +668,7 @@ SnapSerializer.prototype.loadSprites = function (xmlString, ide) {
}
});
project.stage.children.forEach(function (sprite) {
+ delete sprite.inheritanceInfo;
if (sprite.nestingInfo) { // only sprites may have nesting info
sprite.nestingScale = +(sprite.nestingInfo.scale || sprite.scale);
delete sprite.nestingInfo;
@@ -690,6 +708,7 @@ SnapSerializer.prototype.loadMediaModel = function (xmlNode) {
SnapSerializer.prototype.loadObject = function (object, model) {
// private
var blocks = model.require('blocks');
+ this.loadInheritanceInfo(object, model);
this.loadNestingInfo(object, model);
this.loadCostumes(object, model);
this.loadSounds(object, model);
@@ -699,6 +718,14 @@ SnapSerializer.prototype.loadObject = function (object, model) {
this.loadScripts(object.scripts, model.require('scripts'));
};
+SnapSerializer.prototype.loadInheritanceInfo = function (object, model) {
+ // private
+ var info = model.childNamed('inherit');
+ if (info) {
+ object.inheritanceInfo = info.attributes;
+ }
+};
+
SnapSerializer.prototype.loadNestingInfo = function (object, model) {
// private
var info = model.childNamed('nest');
@@ -782,7 +809,7 @@ SnapSerializer.prototype.loadCustomBlocks = function (
names = definition.parseSpec(definition.spec).filter(
function (str) {
- return str.charAt(0) === '%';
+ return str.charAt(0) === '%' && str.length > 1;
}
).map(function (str) {
return str.substr(1);
@@ -1037,7 +1064,7 @@ SnapSerializer.prototype.obsoleteBlock = function (isReporter) {
: new CommandBlockMorph();
block.selector = 'nop';
block.color = new Color(200, 0, 20);
- block.setSpec('Obsolete!');
+ block.setSpec(localize('Obsolete!'));
block.isDraggable = true;
return block;
};
@@ -1467,6 +1494,7 @@ SpriteMorph.prototype.toXML = function (serializer) {
' draggable="@"' +
'%' +
' costume="@" color="@,@,@" pen="@" ~>' +
+ '%' + // inheritance info
'%' + // nesting info
'<costumes>%</costumes>' +
'<sounds>%</sounds>' +
@@ -1489,6 +1517,13 @@ SpriteMorph.prototype.toXML = function (serializer) {
this.color.b,
this.penPoint,
+ // inheritance info
+ this.exemplar
+ ? '<inherit exemplar="' +
+ this.exemplar.name
+ + '"/>'
+ : '',
+
// nesting info
this.anchor
? '<nest anchor="' +
diff --git a/threads.js b/threads.js
index 1f45162..f28704d 100644
--- a/threads.js
+++ b/threads.js
@@ -83,7 +83,7 @@ ArgLabelMorph, localize, XML_Element, hex_sha512*/
// Global stuff ////////////////////////////////////////////////////////
-modules.threads = '2015-February-28';
+modules.threads = '2015-March-23';
var ThreadManager;
var Process;
@@ -128,6 +128,15 @@ function snapEquals(a, b) {
return x === y;
}
+// stricter alternative to parseFloat
+// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/parseFloat/
+function filterFloat(value) {
+ if(/^(\-|\+)?([0-9]+(\.[0-9]+)?|Infinity)$/
+ .test(value))
+ return Number(value);
+ return NaN;
+}
+
// ThreadManager ///////////////////////////////////////////////////////
function ThreadManager() {
@@ -441,11 +450,20 @@ Process.prototype.pause = function () {
if (this.context && this.context.startTime) {
this.pauseOffset = Date.now() - this.context.startTime;
}
+ if (this.context.activeNote) {
+ this.context.activeNote.stop();
+ }
};
Process.prototype.resume = function () {
this.isPaused = false;
this.pauseOffset = null;
+ if (this.context.activeNote) {
+ if (this.context.activeNote.oscillator === null) {
+ // prevents Note from resuming twice
+ this.context.activeNote.play();
+ }
+ }
};
Process.prototype.pauseStep = function () {
@@ -735,6 +753,13 @@ Process.prototype.expectReport = function () {
// Process Exception Handling
+Process.prototype.checkIfList = function (maybeList) {
+ if (!(maybeList instanceof List)) {
+ maybeList = new List();
+ }
+ return maybeList;
+};
+
Process.prototype.handleError = function (error, element) {
var m = element;
this.stop();
@@ -1159,7 +1184,7 @@ Process.prototype.doSetVar = function (varName, value) {
name = name.expression.blockSpec;
}
}
- varFrame.setVar(name, value);
+ varFrame.setVar(name, value, this.blockReceiver());
};
Process.prototype.doChangeVar = function (varName, value) {
@@ -1171,7 +1196,7 @@ Process.prototype.doChangeVar = function (varName, value) {
name = name.expression.blockSpec;
}
}
- varFrame.changeVar(name, value);
+ varFrame.changeVar(name, value, this.blockReceiver());
};
Process.prototype.reportGetVar = function () {
@@ -1216,7 +1241,7 @@ Process.prototype.doShowVar = function (varName) {
}
// if no watcher exists, create a new one
isGlobal = contains(
- this.homeContext.receiver.variables.parentFrame.names(),
+ this.homeContext.receiver.globalVariables().names(),
varName
);
if (isGlobal || target.owner) {
@@ -1295,6 +1320,77 @@ Process.prototype.doRemoveTemporaries = function () {
}
};
+// Peer to peer primitives
+
+Process.prototype.sendPeerMessage = function (message, peer) {
+ var myself = this;
+ var stage = this.homeContext.receiver.parentThatIsA(StageMorph),
+ ide = this.homeContext.receiver.parentThatIsA(IDE_Morph);
+
+ if (peer instanceof List) {
+ peer.asArray().forEach(function (singlePeer) {
+ myself.sendPeerMessage(message, singlePeer);
+ });
+ return;
+ }
+ if (peer == ide.peer.id) {
+ stage.newPeerMessage(message, peer);
+ return;
+ }
+
+ var connection = ide.peer.connect(peer, {reliable: true});
+ connection.on('open', function () {
+ var data;
+ if (typeof message == "string") {
+ data = message;
+ } else if (typeof message == "function") {
+ data = message.toString();
+ } else {
+ data = ide.serializer.serialize(message);
+ }
+ connection.send(data);
+ });
+};
+
+Process.prototype.reportPeerList = function () {
+ var myself = this;
+
+ if (!this.context.wait) {
+ this.context.wait = true;
+ var ide = this.homeContext.receiver.parentThatIsA(IDE_Morph);
+ ide.peer.listAllPeers(function (peers) {
+ myself.context.result = new List(peers);
+ });
+ } else if (this.context.result) {
+ return this.context.result;
+ }
+
+ this.pushContext('doYield');
+ this.pushContext();
+};
+
+Process.prototype.reportPeerId = function () {
+ var ide = this.homeContext.receiver.parentThatIsA(IDE_Morph);
+ return ide.peer.id;
+};
+
+// Process sprite inheritance primitives
+
+Process.prototype.doDeleteAttr = function (attrName) {
+ // currently only variables are deletable
+ var name = attrName,
+ rcvr = this.blockReceiver();
+
+ if (name instanceof Context) {
+ if (name.expression.selector === 'reportGetVar') {
+ name = name.expression.blockSpec;
+ }
+ }
+ if (contains(rcvr.inheritedVariableNames(true), name)) {
+ rcvr.deleteVariable(name);
+ }
+};
+
// Process lists primitives
Process.prototype.reportNewList = function (elements) {
@@ -1306,15 +1402,19 @@ Process.prototype.reportCONS = function (car, cdr) {
};
Process.prototype.reportCDR = function (list) {
+ list = this.checkIfList(list);
return list.cdr();
};
Process.prototype.doAddToList = function (element, list) {
+ list = this.checkIfList(list);
list.add(element);
};
Process.prototype.doDeleteFromList = function (index, list) {
var idx = index;
+ list = this.checkIfList(list);
+
if (this.inputOption(index) === 'all') {
return list.clear();
}
@@ -1329,6 +1429,8 @@ Process.prototype.doDeleteFromList = function (index, list) {
Process.prototype.doInsertInList = function (element, index, list) {
var idx = index;
+ list = this.checkIfList(list);
+
if (index === '') {
return null;
}
@@ -1343,6 +1445,8 @@ Process.prototype.doInsertInList = function (element, index, list) {
Process.prototype.doReplaceInList = function (index, list, element) {
var idx = index;
+ list = this.checkIfList(list);
+
if (index === '') {
return null;
}
@@ -1357,6 +1461,8 @@ Process.prototype.doReplaceInList = function (index, list, element) {
Process.prototype.reportListItem = function (index, list) {
var idx = index;
+ list = this.checkIfList(list);
+
if (index === '') {
return '';
}
@@ -1370,10 +1476,12 @@ Process.prototype.reportListItem = function (index, list) {
};
Process.prototype.reportListLength = function (list) {
+ list = this.checkIfList(list);
return list.length();
};
Process.prototype.reportListContainsItem = function (list, element) {
+ list = this.checkIfList(list);
return list.contains(element);
};
@@ -1429,7 +1537,7 @@ Process.prototype.doStopAll = function () {
if (this.homeContext.receiver) {
stage = this.homeContext.receiver.parentThatIsA(StageMorph);
if (stage) {
- stage.threads.resumeAll(stage);
+ //stage.threads.resumeAll(stage); // leads to a strange Note bug
stage.keysPressed = {};
stage.threads.stopAll();
stage.stopAllActiveSounds();
@@ -1632,6 +1740,8 @@ Process.prototype.reportMap = function (reporter, list) {
// documented in each of the variants' code (linked or arrayed) below
var next;
+ list = this.checkIfList(list);
+
if (list.isLinked) {
// this.context.inputs:
// [0] - reporter
@@ -1920,6 +2030,7 @@ Process.prototype.reportIsA = function (thing, typeString) {
Process.prototype.reportTypeOf = function (thing) {
// answer a string denoting the argument's type
var exp;
+
if (thing === null || (thing === undefined)) {
return 'nothing';
}
@@ -2921,18 +3032,32 @@ Process.prototype.doPlayNote = function (pitch, beats) {
Process.prototype.doPlayNoteForSecs = function (pitch, secs) {
// interpolated
+ var receiver = this.homeContext.receiver;
+ var volume = receiver.volume;
+ var muted = receiver.parentThatIsA(StageMorph).muted;
+
+ if (muted === true) {
+ volume = 0;
+ }
+
if (!this.context.startTime) {
this.context.startTime = Date.now();
- this.context.activeNote = new Note(pitch);
+ this.context.activeNote = new Note(pitch, volume);
this.context.activeNote.play();
}
+
if ((Date.now() - this.context.startTime) >= (secs * 1000)) {
if (this.context.activeNote) {
this.context.activeNote.stop();
this.context.activeNote = null;
}
return null;
+ } else if (this.context.activeNote) {
+ if (this.context.activeNote.volume !== volume) {
+ this.context.activeNote.setVolume(volume);
+ }
}
+
this.pushContext('doYield');
this.pushContext();
};
@@ -3448,35 +3573,50 @@ VariableFrame.prototype.silentFind = function (name) {
return null;
};
-VariableFrame.prototype.setVar = function (name, value) {
-/*
- change the specified variable if it exists
- else throw an error, because variables need to be
- declared explicitly (e.g. through a "script variables" block),
- before they can be accessed.
-*/
+VariableFrame.prototype.setVar = function (name, value, sender) {
+ // change the specified variable if it exists
+ // else throw an error, because variables need to be
+ // declared explicitly (e.g. through a "script variables" block),
+ // before they can be accessed.
+ // if the found frame is inherited by the sender sprite
+ // shadow it (create an explicit one for the sender)
+ // before setting the value ("create-on-write")
+
var frame = this.find(name);
if (frame) {
- frame.vars[name].value = value;
+ if (sender instanceof SpriteMorph &&
+ (frame.owner instanceof SpriteMorph) &&
+ (sender !== frame.owner)) {
+ sender.shadowVar(name, value);
+ } else {
+ frame.vars[name].value = value;
+ }
}
};
-VariableFrame.prototype.changeVar = function (name, delta) {
-/*
- change the specified variable if it exists
- else throw an error, because variables need to be
- declared explicitly (e.g. through a "script variables" block,
- before they can be accessed.
-*/
+VariableFrame.prototype.changeVar = function (name, delta, sender) {
+ // change the specified variable if it exists
+ // else throw an error, because variables need to be
+ // declared explicitly (e.g. through a "script variables" block,
+ // before they can be accessed.
+ // if the found frame is inherited by the sender sprite
+ // shadow it (create an explicit one for the sender)
+ // before changing the value ("create-on-write")
+
var frame = this.find(name),
- value;
+ value,
+ newValue;
if (frame) {
value = parseFloat(frame.vars[name].value);
- if (isNaN(value)) {
- frame.vars[name].value = delta;
+ newValue = isNaN(value) ? delta : value + parseFloat(delta);
+ if (sender instanceof SpriteMorph &&
+ (frame.owner instanceof SpriteMorph) &&
+ (sender !== frame.owner)) {
+ sender.shadowVar(name, newValue);
} else {
- frame.vars[name].value = value + parseFloat(delta);
+ frame.vars[name].value = newValue;
}
+
}
};
diff --git a/tools.xml b/tools.xml
index e05d7bb..1f93675 100644
--- a/tools.xml
+++ b/tools.xml
@@ -1 +1 @@
-<blocks app="Snap! 4.0, http://snap.berkeley.edu" version="1"><block-definition s="map %&apos;function&apos; over %&apos;lists&apos;" type="reporter" category="lists"><header></header><code></code><inputs><input type="%repRing"></input><input type="%mult%l"></input></inputs><script><block s="doWarp"><script><block s="doDeclareVariables"><list><l>mapone</l><l>mapmany</l></list></block><block s="doSetVar"><l>mapone</l><block s="reifyScript"><script><block s="doIf"><custom-block s="empty? %l"><block var="data"/></custom-block><script><block s="doReport"><block s="reportNewList"><list></list></block></block></script></block><block s="doReport"><block s="reportCONS"><block s="evaluate"><block var="function"/><list><block s="reportListItem"><l>1</l><block var="data"/></block></list></block><block s="evaluate"><block var="mapone"/><list><block s="reportCDR"><block var="data"/></block></list></block></block></block></script><list><l>data</l></list></block></block><block s="doSetVar"><l>mapmany</l><block s="reifyScript"><script><block s="doIf"><custom-block s="empty? %l"><block s="reportListItem"><l>1</l><block var="data lists"/></block></custom-block><script><block s="doReport"><block s="reportNewList"><list></list></block></block></script></block><block s="doReport"><block s="reportCONS"><block s="evaluate"><block var="function"/><custom-block s="map %repRing over %mult%l"><block s="reifyReporter"><autolambda><block s="reportListItem"><l>1</l><l/></block></autolambda><list></list></block><list><block var="data lists"/></list></custom-block></block><block s="evaluate"><block var="mapmany"/><list><custom-block s="map %repRing over %mult%l"><block s="reifyReporter"><autolambda><block s="reportCDR"><l/></block></autolambda><list></list></block><list><block var="data lists"/></list></custom-block></list></block></block></block></script><list><l>data lists</l></list></block></block><block s="doIfElse"><custom-block s="empty? %l"><block s="reportCDR"><block var="lists"/></block></custom-block><script><block s="doReport"><block s="evaluate"><block var="mapone"/><list><block s="reportListItem"><l>1</l><block var="lists"/></block></list></block></block></script><script><block s="doReport"><block s="evaluate"><block var="mapmany"/><list><block var="lists"/></list></block></block></script></block></script></block></script></block-definition><block-definition s="empty? %&apos;data&apos;" type="predicate" category="lists"><header></header><code></code><inputs><input type="%l"></input></inputs><script><block s="doReport"><block s="reportEquals"><block var="data"/><block s="reportNewList"><list></list></block></block></block></script></block-definition><block-definition s="keep items such that %&apos;pred&apos; from %&apos;data&apos;" type="reporter" category="lists"><header></header><code></code><inputs><input type="%predRing"></input><input type="%l"></input></inputs><script><block s="doWarp"><script><block s="doIf"><custom-block s="empty? %l"><block var="data"/></custom-block><script><block s="doReport"><block s="reportNewList"><list></list></block></block></script></block><block s="doIfElse"><block s="evaluate"><block var="pred"/><list><block s="reportListItem"><l>1</l><block var="data"/></block></list></block><script><block s="doReport"><block s="reportCONS"><block s="reportListItem"><l>1</l><block var="data"/></block><custom-block s="keep items such that %predRing from %l"><block var="pred"/><block s="reportCDR"><block var="data"/></block></custom-block></block></block></script><script><block s="doReport"><custom-block s="keep items such that %predRing from %l"><block var="pred"/><block s="reportCDR"><block var="data"/></block></custom-block></block></script></block></script></block></script></block-definition><block-definition s="combine with %&apos;function&apos; items of %&apos;data&apos;" type="reporter" category="lists"><header></header><code></code><inputs><input type="%repRing"></input><input type="%l"></input></inputs><script><block s="doWarp"><script><block s="doIf"><custom-block s="empty? %l"><block s="reportCDR"><block var="data"/></block></custom-block><script><block s="doReport"><block s="reportListItem"><l>1</l><block var="data"/></block></block></script></block><block s="doReport"><block s="evaluate"><block var="function"/><list><block s="reportListItem"><l>1</l><block var="data"/></block><custom-block s="combine with %repRing items of %l"><block var="function"/><block s="reportCDR"><block var="data"/></block></custom-block></list></block></block></script></block></script></block-definition><block-definition s="if %&apos;test&apos; then %&apos;true&apos; else %&apos;false&apos;" type="reporter" category="control"><header></header><code></code><inputs><input type="%b"></input><input type="%anyUE"></input><input type="%anyUE"></input></inputs><script><block s="doIfElse"><block var="test"/><script><block s="doReport"><block s="evaluate"><block var="true"/><list></list></block></block></script><script><block s="doReport"><block s="evaluate"><block var="false"/><list></list></block></block></script></block></script></block-definition><block-definition s="for %&apos;i&apos; = %&apos;start&apos; to %&apos;end&apos; %&apos;action&apos;" type="command" category="control"><header></header><code></code><inputs><input type="%upvar"></input><input type="%n">1</input><input type="%n">10</input><input type="%cs"></input></inputs><script><block s="doDeclareVariables"><list><l>step</l><l>tester</l></list></block><block s="doIfElse"><block s="reportGreaterThan"><block var="start"/><block var="end"/></block><script><block s="doSetVar"><l>step</l><l>-1</l></block><block s="doSetVar"><l>tester</l><block s="reifyReporter"><autolambda><block s="reportLessThan"><block var="i"/><block var="end"/></block></autolambda><list></list></block></block></script><script><block s="doSetVar"><l>step</l><l>1</l></block><block s="doSetVar"><l>tester</l><block s="reifyReporter"><autolambda><block s="reportGreaterThan"><block var="i"/><block var="end"/></block></autolambda><list></list></block></block></script></block><block s="doSetVar"><l>i</l><block var="start"/></block><block s="doUntil"><block s="evaluate"><block var="tester"/><list></list></block><script><block s="doRun"><block var="action"/><list></list></block><block s="doChangeVar"><l>i</l><block var="step"/></block></script></block></script></block-definition><block-definition s="join words %&apos;words&apos;" type="reporter" category="operators"><header></header><code></code><inputs><input type="%mult%txt"></input></inputs><script><block s="doWarp"><script><block s="doIf"><custom-block s="empty? %l"><block s="reportCDR"><block var="words"/></block></custom-block><script><block s="doReport"><block s="reportListItem"><l>1</l><block var="words"/></block></block></script></block><block s="doReport"><block s="reportJoinWords"><list><block s="reportListItem"><l>1</l><block var="words"/></block><block s="reportJoinWords"><list><l> </l><block s="evaluate"><block s="reifyReporter"><autolambda><custom-block s="join words %mult%txt"><block s="reportCDR"><block var="words"/></block></custom-block></autolambda><list></list></block><list></list></block></list></block></list></block></block></script></block></script></block-definition><block-definition s="list $arrowRight sentence %&apos;data&apos;" type="reporter" category="operators"><header></header><code></code><inputs><input type="%l"></input></inputs><script><block s="doWarp"><script><block s="doReport"><custom-block s="combine with %repRing items of %l"><block s="reifyReporter"><autolambda><custom-block s="join words %mult%txt"><list><l></l><l></l></list></custom-block></autolambda><list></list></block><block var="data"/></custom-block></block></script></block></script></block-definition><block-definition s="sentence $arrowRight list %&apos;text&apos;" type="reporter" category="operators"><header></header><code></code><inputs><input type="%txt"></input></inputs><script><block s="doWarp"><script><block s="doReport"><block s="reportTextSplit"><block var="text"/><l><option>whitespace</option></l></block></block></script></block></script></block-definition><block-definition s="catch %&apos;tag&apos; %&apos;action&apos;" type="command" category="control"><header></header><code></code><inputs><input type="%upvar"></input><input type="%cs"></input></inputs><script><block s="doCallCC"><block s="reifyScript"><script><block s="doSetVar"><l>tag</l><block var="cont"/></block><block s="doRun"><block var="action"/><list></list></block></script><list><l>cont</l></list></block></block></script></block-definition><block-definition s="throw %&apos;cont&apos;" type="command" category="control"><header></header><code></code><inputs><input type="%s">catchtag</input></inputs><script><block s="doRun"><block var="cont"/><list></list></block></script></block-definition><block-definition s="catch %&apos;tag&apos; %&apos;value&apos;" type="reporter" category="control"><header></header><code></code><inputs><input type="%upvar"></input><input type="%anyUE"></input></inputs><script><block s="doCallCC"><block s="reifyScript"><script><block s="doSetVar"><l>tag</l><block var="cont"/></block><block s="doReport"><block s="evaluate"><block var="value"/><list></list></block></block></script><list><l>cont</l></list></block></block></script></block-definition><block-definition s="throw %&apos;tag&apos; %&apos;value&apos;" type="command" category="control"><header></header><code></code><inputs><input type="%s">catchtag</input><input type="%s"></input></inputs><script><block s="doRun"><block var="tag"/><list><block var="value"/></list></block></script></block-definition><block-definition s="for each %&apos;item&apos; of %&apos;data&apos; %&apos;action&apos;" type="command" category="lists"><header></header><code></code><inputs><input type="%upvar"></input><input type="%l"></input><input type="%cs"></input></inputs><script><block s="doUntil"><custom-block s="empty? %l"><block var="data"/></custom-block><script><block s="doSetVar"><l>item</l><block s="reportListItem"><l>1</l><block var="data"/></block></block><block s="doRun"><block var="action"/><list><block s="reportListItem"><l>1</l><block var="data"/></block></list></block><block s="doSetVar"><l>data</l><block s="reportCDR"><block var="data"/></block></block></script></block></script></block-definition><block-definition s="if %&apos;test&apos; do %&apos;action&apos; and pause all $pause-1-255-220-0" type="command" category="control"><header></header><code></code><inputs><input type="%boolUE"></input><input type="%cs"></input></inputs><script><block s="doDeclareVariables"><list><l>breakpoint</l></list></block><block s="doIf"><block s="evaluate"><block var="test"/><list></list></block><script><block s="doSetVar"><l>breakpoint</l><block var="test"/></block><block s="doShowVar"><l>breakpoint</l></block><block s="doRun"><block var="action"/><list></list></block><block s="doPauseAll"></block><block s="doHideVar"><l></l></block></script></block></script></block-definition><block-definition s="word $arrowRight list %&apos;word&apos;" type="reporter" category="operators"><header></header><code></code><inputs><input type="%txt"></input></inputs><script><block s="doWarp"><script><block s="doReport"><block s="reportTextSplit"><block var="word"/><l><option>letter</option></l></block></block></script></block></script></block-definition><block-definition s="ignore %&apos;x&apos;" type="command" category="control"><header></header><code></code><inputs><input type="%s"></input></inputs></block-definition></blocks> \ No newline at end of file
+<blocks app="Snap! 4.0, http://snap.berkeley.edu" version="1"><block-definition s="map %&apos;function&apos; over %&apos;lists&apos;" type="reporter" category="lists"><header></header><code></code><inputs><input type="%repRing"></input><input type="%mult%l"></input></inputs><script><block s="doWarp"><script><block s="doDeclareVariables"><list><l>mapone</l><l>mapmany</l></list></block><block s="doSetVar"><l>mapone</l><block s="reifyScript"><script><block s="doIf"><custom-block s="empty? %l"><block var="data"/></custom-block><script><block s="doReport"><block s="reportNewList"><list></list></block></block></script></block><block s="doReport"><block s="reportCONS"><block s="evaluate"><block var="function"/><list><block s="reportListItem"><l>1</l><block var="data"/></block></list></block><block s="evaluate"><block var="mapone"/><list><block s="reportCDR"><block var="data"/></block></list></block></block></block></script><list><l>data</l></list></block></block><block s="doSetVar"><l>mapmany</l><block s="reifyScript"><script><block s="doIf"><custom-block s="empty? %l"><block s="reportListItem"><l>1</l><block var="data lists"/></block></custom-block><script><block s="doReport"><block s="reportNewList"><list></list></block></block></script></block><block s="doReport"><block s="reportCONS"><block s="evaluate"><block var="function"/><custom-block s="map %repRing over %mult%l"><block s="reifyReporter"><autolambda><block s="reportListItem"><l>1</l><l/></block></autolambda><list></list></block><list><block var="data lists"/></list></custom-block></block><block s="evaluate"><block var="mapmany"/><list><custom-block s="map %repRing over %mult%l"><block s="reifyReporter"><autolambda><block s="reportCDR"><l/></block></autolambda><list></list></block><list><block var="data lists"/></list></custom-block></list></block></block></block></script><list><l>data lists</l></list></block></block><block s="doIfElse"><custom-block s="empty? %l"><block s="reportCDR"><block var="lists"/></block></custom-block><script><block s="doReport"><block s="evaluate"><block var="mapone"/><list><block s="reportListItem"><l>1</l><block var="lists"/></block></list></block></block></script><script><block s="doReport"><block s="evaluate"><block var="mapmany"/><list><block var="lists"/></list></block></block></script></block></script></block></script></block-definition><block-definition s="empty? %&apos;data&apos;" type="predicate" category="lists"><header></header><code></code><inputs><input type="%l"></input></inputs><script><block s="doReport"><block s="reportEquals"><block var="data"/><block s="reportNewList"><list></list></block></block></block></script></block-definition><block-definition s="keep items such that %&apos;pred&apos; from %&apos;data&apos;" type="reporter" category="lists"><header></header><code></code><inputs><input type="%predRing"></input><input type="%l"></input></inputs><script><block s="doWarp"><script><block s="doIf"><custom-block s="empty? %l"><block var="data"/></custom-block><script><block s="doReport"><block s="reportNewList"><list></list></block></block></script></block><block s="doIfElse"><block s="evaluate"><block var="pred"/><list><block s="reportListItem"><l>1</l><block var="data"/></block></list></block><script><block s="doReport"><block s="reportCONS"><block s="reportListItem"><l>1</l><block var="data"/></block><custom-block s="keep items such that %predRing from %l"><block var="pred"/><block s="reportCDR"><block var="data"/></block></custom-block></block></block></script><script><block s="doReport"><custom-block s="keep items such that %predRing from %l"><block var="pred"/><block s="reportCDR"><block var="data"/></block></custom-block></block></script></block></script></block></script></block-definition><block-definition s="combine with %&apos;function&apos; items of %&apos;data&apos;" type="reporter" category="lists"><header></header><code></code><inputs><input type="%repRing"></input><input type="%l"></input></inputs><script><block s="doWarp"><script><block s="doIf"><custom-block s="empty? %l"><block s="reportCDR"><block var="data"/></block></custom-block><script><block s="doReport"><block s="reportListItem"><l>1</l><block var="data"/></block></block></script></block><block s="doReport"><block s="evaluate"><block var="function"/><list><block s="reportListItem"><l>1</l><block var="data"/></block><custom-block s="combine with %repRing items of %l"><block var="function"/><block s="reportCDR"><block var="data"/></block></custom-block></list></block></block></script></block></script></block-definition><block-definition s="if %&apos;test&apos; then %&apos;true&apos; else %&apos;false&apos;" type="reporter" category="control"><header></header><code></code><inputs><input type="%b"></input><input type="%anyUE"></input><input type="%anyUE"></input></inputs><script><block s="doIfElse"><block var="test"/><script><block s="doReport"><block s="evaluate"><block var="true"/><list></list></block></block></script><script><block s="doReport"><block s="evaluate"><block var="false"/><list></list></block></block></script></block></script></block-definition><block-definition s="for %&apos;i&apos; = %&apos;start&apos; to %&apos;end&apos; %&apos;action&apos;" type="command" category="control"><header></header><code></code><inputs><input type="%upvar"></input><input type="%n">1</input><input type="%n">10</input><input type="%cs"></input></inputs><script><block s="doDeclareVariables"><list><l>step</l><l>tester</l></list></block><block s="doIfElse"><block s="reportGreaterThan"><block var="start"/><block var="end"/></block><script><block s="doSetVar"><l>step</l><l>-1</l></block><block s="doSetVar"><l>tester</l><block s="reifyReporter"><autolambda><block s="reportLessThan"><block var="i"/><block var="end"/></block></autolambda><list></list></block></block></script><script><block s="doSetVar"><l>step</l><l>1</l></block><block s="doSetVar"><l>tester</l><block s="reifyReporter"><autolambda><block s="reportGreaterThan"><block var="i"/><block var="end"/></block></autolambda><list></list></block></block></script></block><block s="doSetVar"><l>i</l><block var="start"/></block><block s="doUntil"><block s="evaluate"><block var="tester"/><list></list></block><script><block s="doRun"><block var="action"/><list></list></block><block s="doChangeVar"><l>i</l><block var="step"/></block></script></block></script></block-definition><block-definition s="join words %&apos;words&apos;" type="reporter" category="operators"><header></header><code></code><inputs><input type="%mult%txt"></input></inputs><script><block s="doWarp"><script><block s="doIf"><custom-block s="empty? %l"><block s="reportCDR"><block var="words"/></block></custom-block><script><block s="doReport"><block s="reportListItem"><l>1</l><block var="words"/></block></block></script></block><block s="doReport"><block s="reportJoinWords"><list><block s="reportListItem"><l>1</l><block var="words"/></block><block s="reportJoinWords"><list><l> </l><block s="evaluate"><block s="reifyReporter"><autolambda><custom-block s="join words %mult%txt"><block s="reportCDR"><block var="words"/></block></custom-block></autolambda><list></list></block><list></list></block></list></block></list></block></block></script></block></script></block-definition><block-definition s="list $arrowRight sentence %&apos;data&apos;" type="reporter" category="operators"><header></header><code></code><inputs><input type="%l"></input></inputs><script><block s="doWarp"><script><block s="doReport"><custom-block s="combine with %repRing items of %l"><block s="reifyReporter"><autolambda><custom-block s="join words %mult%txt"><list><l></l><l></l></list></custom-block></autolambda><list></list></block><block var="data"/></custom-block></block></script></block></script></block-definition><block-definition s="sentence $arrowRight list %&apos;text&apos;" type="reporter" category="operators"><header></header><code></code><inputs><input type="%txt"></input></inputs><script><block s="doWarp"><script><block s="doReport"><block s="reportTextSplit"><block var="text"/><l><option>whitespace</option></l></block></block></script></block></script></block-definition><block-definition s="catch %&apos;tag&apos; %&apos;action&apos;" type="command" category="control"><header></header><code></code><inputs><input type="%upvar"></input><input type="%cs"></input></inputs><script><block s="doCallCC"><block s="reifyScript"><script><block s="doSetVar"><l>tag</l><block var="cont"/></block><block s="doRun"><block var="action"/><list></list></block></script><list><l>cont</l></list></block></block></script></block-definition><block-definition s="throw %&apos;cont&apos;" type="command" category="control"><header></header><code></code><inputs><input type="%s">catchtag</input></inputs><script><block s="doRun"><block var="cont"/><list></list></block></script></block-definition><block-definition s="catch %&apos;tag&apos; %&apos;value&apos;" type="reporter" category="control"><header></header><code></code><inputs><input type="%upvar"></input><input type="%anyUE"></input></inputs><script><block s="doCallCC"><block s="reifyScript"><script><block s="doSetVar"><l>tag</l><block var="cont"/></block><block s="doReport"><block s="evaluate"><block var="value"/><list></list></block></block></script><list><l>cont</l></list></block></block></script></block-definition><block-definition s="throw %&apos;tag&apos; %&apos;value&apos;" type="command" category="control"><header></header><code></code><inputs><input type="%s">catchtag</input><input type="%s"></input></inputs><script><block s="doRun"><block var="tag"/><list><block var="value"/></list></block></script></block-definition><block-definition s="for each %&apos;item&apos; of %&apos;data&apos; %&apos;action&apos;" type="command" category="lists"><header></header><code></code><inputs><input type="%upvar"></input><input type="%l"></input><input type="%cs"></input></inputs><script><block s="doUntil"><custom-block s="empty? %l"><block var="data"/></custom-block><script><block s="doSetVar"><l>item</l><block s="reportListItem"><l>1</l><block var="data"/></block></block><block s="doRun"><block var="action"/><list><block s="reportListItem"><l>1</l><block var="data"/></block></list></block><block s="doSetVar"><l>data</l><block s="reportCDR"><block var="data"/></block></block></script></block></script></block-definition><block-definition s="if %&apos;test&apos; do %&apos;action&apos; and pause all $pause-1-255-220-0" type="command" category="control"><header></header><code></code><inputs><input type="%boolUE"></input><input type="%cs"></input></inputs><script><block s="doDeclareVariables"><list><l>breakpoint</l></list></block><block s="doIf"><block s="evaluate"><block var="test"/><list></list></block><script><block s="doSetVar"><l>breakpoint</l><block var="test"/></block><block s="doShowVar"><l>breakpoint</l></block><block s="doRun"><block var="action"/><list></list></block><block s="doPauseAll"></block><block s="doHideVar"><l></l></block></script></block></script></block-definition><block-definition s="word $arrowRight list %&apos;word&apos;" type="reporter" category="operators"><header></header><code></code><inputs><input type="%txt"></input></inputs><script><block s="doWarp"><script><block s="doReport"><block s="reportTextSplit"><block var="word"/><l><option>letter</option></l></block></block></script></block></script></block-definition><block-definition s="ignore %&apos;x&apos;" type="command" category="control"><header></header><code></code><inputs><input type="%s"></input></inputs></block-definition><block-definition s="ask for %&apos;reporter&apos; from %&apos;sprite&apos;" type="reporter" category="sensing"><header></header><code></code><inputs><input type="%repRing"></input><input type="%txt"></input></inputs><script><block s="doReport"><block s="evaluate"><block s="reportAttributeOf"><block var="reporter"/><block var="sprite"/></block><list></list></block></block></script></block-definition><block-definition s="tell %&apos;sprite&apos; to %&apos;commands&apos;" type="command" category="sensing"><header></header><code></code><inputs><input type="%txt"></input><input type="%cs"></input></inputs><script><block s="doRun"><block s="reportAttributeOf"><block var="commands"/><block var="sprite"/></block><list></list></block></script></block-definition></blocks> \ No newline at end of file
diff --git a/vkBeautify b/vkBeautify
new file mode 160000
+Subproject ecfc3b9e2b911ad8ebd49600c4089aca30619f2
diff --git a/widgets.js b/widgets.js
index bb250a8..b52782b 100644
--- a/widgets.js
+++ b/widgets.js
@@ -7,7 +7,7 @@
written by Jens Mönig
jens@moenig.org
- Copyright (C) 2014 by Jens Mönig
+ Copyright (C) 2015 by Jens Mönig
This file is part of Snap!.
@@ -74,7 +74,7 @@ HTMLCanvasElement, fontHeight, SymbolMorph, localize, SpeechBubbleMorph,
ArrowMorph, MenuMorph, isString, isNil, SliderMorph, MorphicPreferences,
ScrollFrameMorph*/
-modules.widgets = '2014-February-13';
+modules.widgets = '2015-March-21';
var PushButtonMorph;
var ToggleButtonMorph;
@@ -493,7 +493,7 @@ function ToggleButtonMorph(
labelString,
query, // predicate/selector
environment,
- hint,
+ hint, // optional, String or Function
template, // optional, for cached background images
minWidth, // <num> optional, if specified label will left-align
hasPreview, // <bool> show press color on left edge (e.g. category)
@@ -560,12 +560,13 @@ ToggleButtonMorph.prototype.init = function (
// ToggleButtonMorph events
ToggleButtonMorph.prototype.mouseEnter = function () {
+ var contents = this.hint instanceof Function ? this.hint() : this.hint;
if (!this.state) {
this.image = this.highlightImage;
this.changed();
}
- if (this.hint) {
- this.bubbleHelp(this.hint);
+ if (contents) {
+ this.bubbleHelp(contents);
}
};