diff --git a/blocks.js b/blocks.js index 5988eede..2e05e972 100644 --- a/blocks.js +++ b/blocks.js @@ -156,7 +156,7 @@ DialogBoxMorph, BlockInputFragmentMorph, PrototypeHatBlockMorph, Costume*/ // Global stuff //////////////////////////////////////////////////////// -modules.blocks = '2015-November-17'; +modules.blocks = '2015-December-15'; var SyntaxElementMorph; var BlockMorph; @@ -485,6 +485,10 @@ SyntaxElementMorph.prototype.replaceInput = function (oldArg, newArg) { if ((idx === -1) || (scripts === null)) { return null; } + + if (oldArg.cachedSlotSpec) {oldArg.cachedSlotSpec = null; } + if (newArg.cachedSlotSpec) {newArg.cachedSlotSpec = null; } + this.startLayout(); if (newArg.parent) { newArg.parent.removeChild(newArg); @@ -532,6 +536,9 @@ SyntaxElementMorph.prototype.silentReplaceInput = function (oldArg, newArg) { return; } + if (oldArg.cachedSlotSpec) {oldArg.cachedSlotSpec = null; } + if (newArg.cachedSlotSpec) {newArg.cachedSlotSpec = null; } + if (newArg.parent) { newArg.parent.removeChild(newArg); } @@ -628,6 +635,10 @@ SyntaxElementMorph.prototype.getVarNamesDict = function () { rcvr = block.receiver(); block.allParents().forEach(function (morph) { if (morph instanceof PrototypeHatBlockMorph) { + tempVars.push.apply( + tempVars, + morph.variableNames() + ); tempVars.push.apply( tempVars, morph.inputs()[0].inputFragmentNames() @@ -780,6 +791,10 @@ SyntaxElementMorph.prototype.labelPart = function (spec) { part = new MultiArgMorph('%t', null, 1, spec); part.canBeEmpty = false; break; + case '%blockVars': + part = new MultiArgMorph('%t', 'block variables', 0, spec); + part.canBeEmpty = false; + break; case '%parms': part = new MultiArgMorph('%t', 'Input Names:', 0, spec); part.canBeEmpty = false; @@ -2131,11 +2146,9 @@ BlockMorph.prototype.setSpec = function (spec, silently) { if (part instanceof RingMorph) { part.fixBlockColor(); } - if (part instanceof MultiArgMorph - || contains( - [CommandSlotMorph, RingCommandSlotMorph], - part.constructor - )) { + if (part instanceof MultiArgMorph || + part.constructor === CommandSlotMorph || + part.constructor === RingCommandSlotMorph) { part.fixLayout(); } if (myself.isPrototype) { @@ -2386,7 +2399,7 @@ BlockMorph.prototype.developersMenu = function () { new DialogBoxMorph( this, - this.setSpec, + this.userSetSpec, this ).prompt( menu.title + '\nspec', @@ -2873,7 +2886,7 @@ BlockMorph.prototype.codeMappingHeader = function () { BlockMorph.prototype.eraseHoles = function (context) { var myself = this, - isReporter = this instanceof ReporterBlockMorph, + isRing = this instanceof RingMorph, shift = this.edge * 0.5, gradient, rightX, @@ -2914,7 +2927,7 @@ BlockMorph.prototype.eraseHoles = function (context) { context.clearRect( hole.bounds.origin.x - myself.bounds.origin.x + 1, hole.bounds.origin.y - myself.bounds.origin.y + 1, - isReporter ? w - 2 : w + 1, + isRing ? w - 2 : w + 1, h ); }); @@ -3193,6 +3206,8 @@ BlockMorph.prototype.fullCopy = function () { block.cachedInputs = null; if (block instanceof InputSlotMorph) { block.contents().clearSelection(); + } else if (block.definition) { + block.initializeVariables(); } } else if (block instanceof CursorMorph) { block.destroy(); @@ -3432,7 +3447,10 @@ BlockMorph.prototype.stackWidth = function () { }; BlockMorph.prototype.snap = function () { - var top = this.topBlock(); + var top = this.topBlock(), + receiver, + stage, + ide; top.allComments().forEach(function (comment) { comment.align(top); }); @@ -3443,6 +3461,21 @@ BlockMorph.prototype.snap = function () { if (top.getHighlight()) { top.addHighlight(top.removeHighlight()); } + // register generic hat blocks + if (this.selector === 'receiveCondition') { + receiver = top.receiver(); + if (receiver) { + stage = receiver.parentThatIsA(StageMorph); + if (stage) { + stage.enableCustomHatBlocks = true; + stage.threads.pauseCustomHatBlocks = false; + ide = stage.parentThatIsA(IDE_Morph); + if (ide) { + ide.controlBar.stopButton.refresh(); + } + } + } + } }; // CommandBlockMorph /////////////////////////////////////////////////// @@ -3480,6 +3513,7 @@ CommandBlockMorph.prototype.init = function (silently) { this.setExtent(new Point(200, 100), silently); this.partOfCustomCommand = false; this.exitTag = null; + // this.cachedNextBlock = null; // don't serialize }; // CommandBlockMorph enumerating: @@ -3507,6 +3541,7 @@ CommandBlockMorph.prototype.nextBlock = function (block) { var nb = this.nextBlock(), affected = this.parentThatIsA(CommandSlotMorph); this.add(block); + // this.cachedNextBlock = block; if (nb) { block.bottomBlock().nextBlock(nb); } @@ -3520,6 +3555,18 @@ CommandBlockMorph.prototype.nextBlock = function (block) { affected.fixLayout(); } } else { + /* cachedNextBlock - has issues, disabled for now + if (!this.cachedNextBlock) { + this.cachedNextBlock = detect( + this.children, + function (child) { + return child instanceof CommandBlockMorph + && !child.isPrototype; + } + ); + } + return this.cachedNextBlock; + */ return detect( this.children, function (child) { @@ -4342,6 +4389,7 @@ ReporterBlockMorph.prototype.init = function (isPredicate, silently) { ReporterBlockMorph.uber.init.call(this, silently); this.isPredicate = isPredicate || false; this.setExtent(new Point(200, 80), silently); + this.cachedSlotSpec = null; // don't serialize }; // ReporterBlockMorph drag & drop: @@ -4352,6 +4400,7 @@ ReporterBlockMorph.prototype.snap = function (hand) { nb, target; + this.cachedSlotSpec = null; if (!scripts instanceof ScriptsMorph) { return null; } @@ -4400,6 +4449,7 @@ ReporterBlockMorph.prototype.prepareToBeGrabbed = function (handMorph) { } ReporterBlockMorph.uber.prepareToBeGrabbed.call(this, handMorph); this.alpha = 0.85; + this.cachedSlotSpec = null; }; // ReporterBlockMorph enumerating @@ -4412,11 +4462,12 @@ ReporterBlockMorph.prototype.blockSequence = function () { // ReporterBlockMorph evaluating ReporterBlockMorph.prototype.isUnevaluated = function () { -/* - answer whether my parent block's slot is designated to be of an - 'unevaluated' kind, denoting a spedial form -*/ - return contains(['%anyUE', '%boolUE', '%f'], this.getSlotSpec()); + // answer whether my parent block's slot is designated to be of an + // 'unevaluated' kind, denoting a spedial form + var spec = this.getSlotSpec(); + return spec === '%anyUE' || + spec === '%boolUE' || + spec === '%f'; }; ReporterBlockMorph.prototype.isLocked = function () { @@ -4426,6 +4477,28 @@ ReporterBlockMorph.prototype.isLocked = function () { ReporterBlockMorph.prototype.getSlotSpec = function () { // answer the spec of the slot I'm in, if any + // cached for performance + if (!this.cachedSlotSpec) { + this.cachedSlotSpec = this.determineSlotSpec(); + /* + } else { + // debug slot spec caching + var real = this.determineSlotSpec(); + if (real !== this.cachedSlotSpec) { + throw new Error( + 'cached slot spec ' + + this.cachedSlotSpec + + ' does not match: ' + + real + ); + } + */ + } + return this.cachedSlotSpec; +}; + +ReporterBlockMorph.prototype.determineSlotSpec = function () { + // private - answer the spec of the slot I'm in, if any var parts, idx; if (this.parent instanceof BlockMorph) { parts = this.parent.parts().filter( @@ -4452,19 +4525,25 @@ ReporterBlockMorph.prototype.getSlotSpec = function () { // ReporterBlockMorph events ReporterBlockMorph.prototype.mouseClickLeft = function (pos) { - var isRing; + var label; if (this.parent instanceof BlockInputFragmentMorph) { return this.parent.mouseClickLeft(); } if (this.parent instanceof TemplateSlotMorph) { - isRing = this.parent.parent && this.parent.parent.parent && - this.parent.parent.parent instanceof RingMorph; + if (this.parent.parent && this.parent.parent.parent && + this.parent.parent.parent instanceof RingMorph) { + label = "Input name"; + } else if (this.parent.parent.elementSpec === '%blockVars') { + label = "Block variable name"; + } else { + label = "Script variable name"; + } new DialogBoxMorph( this, this.userSetSpec, this ).prompt( - isRing ? "Input name" : "Script variable name", + label, this.blockSpec, this.world() ); @@ -9731,7 +9810,8 @@ MultiArgMorph.prototype.addInput = function (contents) { // newPart.alpha = this.alpha ? 1 : (1 - this.alpha) / 2; if (contents) { newPart.setContents(contents); - } else if (this.elementSpec === '%scriptVars') { + } else if (this.elementSpec === '%scriptVars' || + this.elementSpec === '%blockVars') { name = ''; i = idx; while (i > 0) { diff --git a/byob.js b/byob.js index 689c668d..4cfbfe92 100644 --- a/byob.js +++ b/byob.js @@ -104,11 +104,11 @@ ArrowMorph, PushButtonMorph, contains, InputSlotMorph, ShadowMorph, ToggleButtonMorph, IDE_Morph, MenuMorph, copy, ToggleElementMorph, Morph, fontHeight, StageMorph, SyntaxElementMorph, SnapSerializer, CommentMorph, localize, CSlotMorph, SpeechBubbleMorph, MorphicPreferences, -SymbolMorph, isNil, CursorMorph*/ +SymbolMorph, isNil, CursorMorph, VariableFrame*/ // Global stuff //////////////////////////////////////////////////////// -modules.byob = '2015-November-16'; +modules.byob = '2015-December-15'; // Declarations @@ -142,6 +142,7 @@ function CustomBlockDefinition(spec, receiver) { this.spec = spec || ''; // format: {'inputName' : [type, default, options, readonly]} this.declarations = {}; + this.variableNames = []; this.comment = null; this.codeMapping = null; // experimental, generate text code this.codeHeader = null; // experimental, generate text code @@ -384,7 +385,7 @@ CustomBlockDefinition.prototype.scriptsModel = function () { proto.allComments().forEach(function (comment) { comment.align(proto); }); - proto.children[0].fixLayout(); + proto.parts()[0].fixLayout(); scripts.fixMultiArgs(); return scripts; }; @@ -409,11 +410,21 @@ CustomCommandBlockMorph.prototype.init = function (definition, isProto) { CustomCommandBlockMorph.uber.init.call(this, true); // silently this.category = definition.category; this.selector = 'evaluateCustomBlock'; + this.variables = null; + this.initializeVariables(); if (definition) { // needed for de-serializing this.refresh(); } }; +CustomCommandBlockMorph.prototype.initializeVariables = function () { + var myself = this; + this.variables = new VariableFrame(); + this.definition.variableNames.forEach(function (name) { + myself.variables.addVar(name); + }); +}; + CustomCommandBlockMorph.prototype.refresh = function (silently) { var def = this.definition, newSpec = this.isPrototype ? @@ -447,6 +458,10 @@ CustomCommandBlockMorph.prototype.refresh = function (silently) { inp.setContents(def.inputNames()[idx]); } }); + + // initialize block vars + // to do: preserve values of unchanged variable names + this.initializeVariables(); }; CustomCommandBlockMorph.prototype.restoreInputs = function (oldInputs) { @@ -759,7 +774,47 @@ CustomCommandBlockMorph.prototype.isInUse = function () { // CustomCommandBlockMorph menu: CustomCommandBlockMorph.prototype.userMenu = function () { - var menu; + var hat = this.parentThatIsA(PrototypeHatBlockMorph), + rcvr = this.receiver(), + myself = this, + menu; + + function monitor(vName) { + var stage = rcvr.parentThatIsA(StageMorph), + varFrame = myself.variables; + menu.addItem( + vName + '...', + function () { + var watcher = detect( + stage.children, + function (morph) { + return morph instanceof WatcherMorph + && morph.target === varFrame + && morph.getter === vName; + } + ), + others; + if (watcher !== null) { + watcher.show(); + watcher.fixLayout(); // re-hide hidden parts + return; + } + watcher = new WatcherMorph( + vName + ' ' + localize('(temporary)'), + SpriteMorph.prototype.blockColor.variables, + varFrame, + vName + ); + watcher.setPosition(stage.position().add(10)); + others = stage.watchers(watcher.left()); + if (others.length > 0) { + watcher.setTop(others[others.length - 1].bottom()); + } + stage.add(watcher); + watcher.fixLayout(); + } + ); + } if (this.isPrototype) { menu = new MenuMorph(this); @@ -776,6 +831,23 @@ CustomCommandBlockMorph.prototype.userMenu = function () { }, 'open a new window\nwith a picture of this script' ); + if (hat.inputs().length < 2) { + menu.addItem( + "block variables...", + function () { + hat.enableBlockVars(); + }, + 'experimental -\nunder construction' + ); + } else { + menu.addItem( + "remove block variables...", + function () { + hat.enableBlockVars(false); + }, + 'experimental -\nunder construction' + ); + } } else { menu = this.constructor.uber.userMenu.call(this); if (!menu) { @@ -786,6 +858,10 @@ CustomCommandBlockMorph.prototype.userMenu = function () { // menu.addItem("export definition...", 'exportBlockDefinition'); menu.addItem("delete block definition...", 'deleteBlockDefinition'); + + this.variables.names().forEach(function (vName) { + monitor(vName); + }); } menu.addItem("edit...", 'edit'); // works also for prototypes return menu; @@ -946,16 +1022,19 @@ CustomReporterBlockMorph.prototype.init = function ( ) { this.definition = definition; // mandatory this.isPrototype = isProto || false; // optional - CustomReporterBlockMorph.uber.init.call(this, isPredicate, true); // sil. - this.category = definition.category; + this.variables = new VariableFrame(); + this.initializeVariables(); this.selector = 'evaluateCustomBlock'; if (definition) { // needed for de-serializing this.refresh(); } }; +CustomReporterBlockMorph.prototype.initializeVariables = + CustomCommandBlockMorph.prototype.initializeVariables; + CustomReporterBlockMorph.prototype.refresh = function () { CustomCommandBlockMorph.prototype.refresh.call(this, true); if (!this.isPrototype) { @@ -1740,7 +1819,7 @@ BlockEditorMorph.prototype.init = function (definition, target) { this.setExtent(new Point(375, 300)); // normal initial extent this.fixLayout(); - proto.children[0].fixLayout(); + proto.parts()[0].fixLayout(); scripts.fixMultiArgs(); }; @@ -1872,6 +1951,7 @@ BlockEditorMorph.prototype.updateDefinition = function () { this.definition.receiver = this.target; // only for serialization this.definition.spec = this.prototypeSpec(); this.definition.declarations = this.prototypeSlots(); + this.definition.variableNames = this.variableNames(); this.definition.scripts = []; this.definition.editorDimensions = this.bounds.copy(); @@ -1948,6 +2028,14 @@ BlockEditorMorph.prototype.prototypeSlots = function () { ).parts()[0].declarationsFromFragments(); }; +BlockEditorMorph.prototype.variableNames = function () { + // answer the variable declarations from my prototype hat + return detect( + this.body.contents.children, + function (c) {return c instanceof PrototypeHatBlockMorph; } + ).variableNames(); +}; + // BlockEditorMorph layout BlockEditorMorph.prototype.setInitialDimensions = function () { @@ -2014,7 +2102,8 @@ function PrototypeHatBlockMorph(definition) { } PrototypeHatBlockMorph.prototype.init = function (definition) { - var proto = definition.prototypeInstance(); + var proto = definition.prototypeInstance(), + vars; this.definition = definition; @@ -2027,6 +2116,14 @@ PrototypeHatBlockMorph.prototype.init = function (definition) { this.color = SpriteMorph.prototype.blockColor.control; this.category = 'control'; this.add(proto); + if (definition.variableNames.length) { + vars = this.labelPart('%blockVars'); + this.add(this.labelPart('%br')); + this.add(vars); + definition.variableNames.forEach(function (name) { + vars.addInput(name); + }); + } proto.refreshPrototypeSlotTypes(); // show slot type indicators this.fixLayout(); }; @@ -2040,11 +2137,11 @@ PrototypeHatBlockMorph.prototype.mouseClickLeft = function () { if (this.world().currentKey === 16) { // shift-clicked return this.focus(); } - this.children[0].mouseClickLeft(); + this.parts()[0].mouseClickLeft(); }; PrototypeHatBlockMorph.prototype.userMenu = function () { - return this.children[0].userMenu(); + return this.parts()[0].userMenu(); }; // PrototypeHatBlockMorph zebra coloring @@ -2053,7 +2150,7 @@ PrototypeHatBlockMorph.prototype.fixBlockColor = function ( nearestBlock, isForced ) { - var nearest = this.children[0] || nearestBlock; + var nearest = this.parts()[0] || nearestBlock; if (!this.zebraContrast && !isForced) { return; @@ -2076,6 +2173,25 @@ PrototypeHatBlockMorph.prototype.fixBlockColor = function ( } }; +// PrototypeHatBlockMorph block instance variables + +PrototypeHatBlockMorph.prototype.variableNames = function (choice) { + var parts = this.parts(); + if (parts.length < 3) {return []; } + return parts[2].evaluate(); +}; + +PrototypeHatBlockMorph.prototype.enableBlockVars = function (choice) { + var prot = this.parts()[0]; + if (choice === false) { + this.setSpec('%s', true); + } else { + this.setSpec('%s %br %blockVars', true); + } + this.replaceInput(this.parts()[0], prot); + this.spec = null; +}; + // BlockLabelFragment ////////////////////////////////////////////////// // BlockLabelFragment instance creation: @@ -2314,8 +2430,11 @@ BlockLabelFragmentMorph.prototype.userMenu = function () { tuple[0] = '$' + string; myself.text = tuple.join('-'); myself.fragment.labelString = myself.text; + myself.parent.parent.changed(); myself.drawNew(); myself.changed(); + myself.parent.parent.fixLayout(); + myself.parent.parent.changed(); }, null, this, @@ -2436,21 +2555,38 @@ BlockLabelPlaceHolderMorph.prototype.drawNew = function () { if (this.parent.fixLayout) { this.parent.fixLayout(); } + if (this.parent.parent instanceof PrototypeHatBlockMorph) { + this.parent.parent.fixLayout(); + } } }; // BlockLabelPlaceHolderMorph events: BlockLabelPlaceHolderMorph.prototype.mouseEnter = function () { + var hat = this.parentThatIsA(PrototypeHatBlockMorph); this.isHighlighted = true; - this.drawNew(); - this.changed(); + if (this.plainLabel && hat) { + hat.changed(); + this.drawNew(); + hat.changed(); + } else { + this.drawNew(); + this.changed(); + } }; BlockLabelPlaceHolderMorph.prototype.mouseLeave = function () { + var hat = this.parentThatIsA(PrototypeHatBlockMorph); this.isHighlighted = false; - this.drawNew(); - this.changed(); + if (this.plainLabel && hat) { + hat.changed(); + this.drawNew(); + hat.changed(); + } else { + this.drawNew(); + this.changed(); + } }; BlockLabelPlaceHolderMorph.prototype.mouseClickLeft diff --git a/cloud.js b/cloud.js index 73f9df20..28b2a6c5 100644 --- a/cloud.js +++ b/cloud.js @@ -30,7 +30,7 @@ /*global modules, IDE_Morph, SnapSerializer, hex_sha512, alert, nop, localize*/ -modules.cloud = '2015-December-04'; +modules.cloud = '2015-December-15'; // Global stuff @@ -326,7 +326,9 @@ Cloud.prototype.reconnect = function ( Cloud.prototype.saveProject = function (ide, callBack, errorCall) { var myself = this, pdata, - media; + media, + size, + mediaSize; ide.serializer.isCollectingMedia = true; pdata = ide.serializer.serialize(ide.stage); @@ -335,6 +337,19 @@ Cloud.prototype.saveProject = function (ide, callBack, errorCall) { ide.serializer.isCollectingMedia = false; ide.serializer.flushMedia(); + mediaSize = media ? media.length : 0; + size = pdata.length + mediaSize; + if (mediaSize > 10485760) { + new DialogBoxMorph().inform( + 'Snap!Cloud - Cannot Save Project', + 'The media inside this project exceeds 10 MB.\n' + + 'Please reduce the size of costumes or sounds.\n', + ide.world(), + ide.cloudIcon(null, new Color(180, 0, 0)) + ); + throw new Error('Project media exceeds 10 MB size limit'); + } + // check if serialized data can be parsed back again try { ide.serializer.parse(pdata); @@ -353,6 +368,7 @@ Cloud.prototype.saveProject = function (ide, callBack, errorCall) { ide.serializer.isCollectingMedia = false; ide.serializer.flushMedia(); + ide.showMessage('Uploading ' + Math.round(size / 1024) + ' KB...'); myself.reconnect( function () { myself.callService( @@ -545,7 +561,7 @@ Cloud.prototype.callService = function ( } else { responseList = myself.parseResponse( request.responseText - ) + ); } callBack.call(null, responseList, service.url); } diff --git a/gui.js b/gui.js index cebd2a7d..c3037ee8 100644 --- a/gui.js +++ b/gui.js @@ -71,7 +71,7 @@ BlockRemovalDialogMorph, saveAs*/ // Global stuff //////////////////////////////////////////////////////// -modules.gui = '2015-December-04'; +modules.gui = '2015-December-15'; // Declarations @@ -232,7 +232,7 @@ IDE_Morph.prototype.init = function (isAutoFill) { this.corralBar = null; this.corral = null; - this.isAutoFill = isAutoFill || true; + this.isAutoFill = isAutoFill === undefined ? true : isAutoFill; this.isAppMode = false; this.isSmallStage = false; this.filePicker = null; @@ -623,11 +623,22 @@ IDE_Morph.prototype.createControlBar = function () { this.controlBar.appModeButton = appModeButton; // for refreshing // stopButton - button = new PushButtonMorph( - this, + button = new ToggleButtonMorph( + null, // colors + this, // the IDE is the target 'stopAllScripts', - new SymbolMorph('octagon', 14) + [ + new SymbolMorph('octagon', 14), + new SymbolMorph('square', 14) + ], + function () { // query + return myself.stage ? + myself.stage.enableCustomHatBlocks && + myself.stage.threads.pauseCustomHatBlocks + : true; + } ); + button.corner = 12; button.color = colors[0]; button.highlightColor = colors[1]; @@ -641,13 +652,15 @@ IDE_Morph.prototype.createControlBar = function () { button.drawNew(); // button.hint = 'stop\nevery-\nthing'; button.fixLayout(); + button.refresh(); stopButton = button; this.controlBar.add(stopButton); + this.controlBar.stopButton = stopButton; // for refreshing //pauseButton button = new ToggleButtonMorph( null, //colors, - myself, // the IDE is the target + this, // the IDE is the target 'togglePauseResume', [ new SymbolMorph('pause', 12), @@ -1705,6 +1718,8 @@ IDE_Morph.prototype.pressStart = function () { if (this.world().currentKey === 16) { // shiftClicked this.toggleFastTracking(); } else { + this.stage.threads.pauseCustomHatBlocks = false; + this.controlBar.stopButton.refresh(); this.runScripts(); } }; @@ -1762,6 +1777,13 @@ IDE_Morph.prototype.isPaused = function () { }; IDE_Morph.prototype.stopAllScripts = function () { + if (this.stage.enableCustomHatBlocks) { + this.stage.threads.pauseCustomHatBlocks = + !this.stage.threads.pauseCustomHatBlocks; + } else { + this.stage.threads.pauseCustomHatBlocks = false; + } + this.controlBar.stopButton.refresh(); this.stage.fireStopAllEvent(); }; @@ -2727,7 +2749,7 @@ IDE_Morph.prototype.aboutSnap = function () { module, btn1, btn2, btn3, btn4, licenseBtn, translatorsBtn, world = this.world(); - aboutTxt = 'Snap! 4.0.3\nBuild Your Own Blocks\n\n' + aboutTxt = 'Snap! 4.0.4\nBuild Your Own Blocks\n\n' + 'Copyright \u24B8 2015 Jens M\u00F6nig and ' + 'Brian Harvey\n' + 'jens@moenig.org, bh@cs.berkeley.edu\n\n' @@ -3472,10 +3494,11 @@ IDE_Morph.prototype.rawOpenProjectString = function (str) { IDE_Morph.prototype.openCloudDataString = function (str) { var msg, - myself = this; + myself = this, + size = Math.round(str.length / 1024); this.nextSteps([ function () { - msg = myself.showMessage('Opening project...'); + msg = myself.showMessage('Opening project\n' + size + ' KB...'); }, function () {nop(); }, // yield (bug in Chrome) function () { @@ -5336,6 +5359,12 @@ ProjectDialogMorph.prototype.rawOpenCloudProject = function (proj) { 'getRawProject', function (response) { SnapCloud.disconnect(); + /* + if (myself.world().currentKey === 16) { + myself.ide.download(response); + return; + } + */ myself.ide.source = 'cloud'; myself.ide.droppedText(response); if (proj.Public === 'true') { diff --git a/history.txt b/history.txt index df1ffe66..86a1913e 100755 --- a/history.txt +++ b/history.txt @@ -2695,3 +2695,114 @@ end - bulk of 151116 * Cloud: doubled the number of supported backend slices * Cloud, GUI: support new “raw” cloud project services +++++++++++++++++++++++++++ +new stuff - bulk of 151215 +++++++++++++++++++++++++++ + +151121 +------ +* Threads: Show result bubble when the user clicks on a command script that uses REPORT (You can now click on REPORT and it actually does something) + +151124 +------ +* Blocks: fix a re-rendering glitch when changing block specs in dev mode +* Threads: add optional receiver (environment) to invoke() function + +151125 +------ +* Threads, Objects, GUI, Store: Generic “When” hat block +* BYOB: fixed a rendering bug when using plain prototype labels + +151126 +------ +* Threads, Blocks: Performance optimizations (replace “contains” with chained tests) +* German translation update (for custom hat blocks) + +151127 +------ +* Blocks, BYOB, Store: new experimental block variables feature +* BYOB: more prototype label rendering fixes + +151128 +------ +* BYOB, Store: Fix some bugs related to block vars (zebra coloring etc.) + +151201 +------ +* BYOB, Blocks: Fix BlockMorph.fullCopy() for block vars + +151202 +------ +* Threads: Only support block vars for blocks that actually define any, to avoid race conditions among parallel global blocks with the same definition that also access sprite-local variables + +151207 +------ +* Threads, GUI: Stop button stops / restarts custom hat blocks, green flag starts custom hat blocks + +151208 +------ +* Objects, Blocks, Threads, GUI, Store, Locale: Automatically enable/disable custom hat blocks when they’re used in a project +* BYOB: initialize custom block vars on every definition-refresh + +151209 +------ +* Threads: allow invoke() to operate on both blocks and rings with arguments +* Blocks: cache reporter slot specs for evaluation performance (30% speedup) + +151210 +------ +* Store: persist block (instance) vars +* Threads: only show result bubble on user-clicked scripts if “Report” is in the lexical script (not inside a reporter block definition) +* Morphic: obey grab threshold when dragging inside scroll frames + +151211 +------ +* Threads: extend red LENGTH reporter to also work on Text +* GUI, Objects, Blocks: extend the red stop button to reflect whether custom hat blocks are paused (indicated by a red square instead of the stop sign) +* Blocks: Tweak C-Slots to better fit inside reporters + +151212 +------ +* Locale: change English ‘any’ (in “item of”) to ‘random’ because teachers + +151214 +------ +* Objects: added “fill” primitive to the Pen category +* Updated German translation +* GUI: Directly download projects from cloud by holding shift while opening - commented out +* GUI, Cloud: show size of uploaded / downloaded projects +* GUI, Cloud: upload size limit of 5 MB - commented out + +151215 +------ +* snap.html: switch to animation frame scheduling because Chrome sucks sooooo much!!!! +* GUI: pushed version to 4.0.4 + +++++++++++++++++++++++++++ + +v4.0.4 draft features: +* Show result bubble when the user clicks on a command script that uses REPORT (You can now click on REPORT and it actually does something) +* New generic “When” hat block, enhances red stop button behavior +* New block (instance) variables feature (experimental) +* evaluator performance optimizations +* Morphic grab-threshold fix for scroll frames +* fixed several block rendering glitches +* List category LENGTH reporter now also works on text +* Changed “any” to “random” (in English only) +* new FILL primitive in the Pen category +* switched to animation frame scheduling, please use TURBO for music +* Updated German translation + +++++++++++++++++++++++++++ +end - bulk of 151215 +++++++++++++++++++++++++++ + +151215 - contributions +------ +* Objects, Paint: Automatic Sprite Center Detection, Thanks, Craxic!! +* Morphic: Handling of diacritics, [Alt] + key in input fields (Windows), Thanks, DaDoro!! +* NL translation update + +151215 - more changes +------ +* Cloud: 10 MB cloud upload limit for media per project diff --git a/lang-de.js b/lang-de.js index ea94da56..93a2a86b 100644 --- a/lang-de.js +++ b/lang-de.js @@ -185,7 +185,7 @@ SnapTranslator.dict.de = { 'translator_e-mail': 'jens@moenig.org', // optional 'last_changed': - '2015-10-07', // this, too, will appear in the Translators tab + '2015-12-14', // this, too, will appear in the Translators tab // GUI // control bar: @@ -411,6 +411,8 @@ SnapTranslator.dict.de = { 'setze Stiftdicke auf %n', 'stamp': 'stemple', + 'fill': + 'male aus', // control: 'when %greenflag clicked': @@ -429,6 +431,8 @@ SnapTranslator.dict.de = { 'vom Mauszeiger betreten', 'mouse-departed': 'vom Mauszeiger verlassen', + 'when %b': + 'Wenn %b', 'when I receive %msgHat': 'Wenn ich %msgHat empfange', 'broadcast %msg': diff --git a/lang-nl.js b/lang-nl.js index 630e49fb..7eef046a 100644 --- a/lang-nl.js +++ b/lang-nl.js @@ -179,7 +179,7 @@ SnapTranslator.dict.nl = { 'translator_e-mail': 'sjoerddirk@fromScratchEd.nl, frank.sierens@telenet.be', // optional 'last_changed': - '2013-08-12', // this, too, will appear in the Translators tab + '2015-12-15', // this, too, will appear in the Translators tab // GUI // control bar: @@ -990,7 +990,7 @@ SnapTranslator.dict.nl = { 'Command\n(C-shape)': 'Opdracht\n(C-vorm)', 'Any\n(unevaluated)': - 'Alle\n(onge\u00EBvalueerd)', + 'Willekeurig\n(onge\u00EBvalueerd)', 'Boolean\n(unevaluated)': 'Booleaans\n(onge\u00EBvalueerd)', 'Single input.': @@ -1211,6 +1211,6 @@ SnapTranslator.dict.nl = { 'last': 'laatste', 'any': - 'alle' + 'willekeurig' }; diff --git a/locale.js b/locale.js index 50eb6dd6..59adbdf9 100644 --- a/locale.js +++ b/locale.js @@ -42,7 +42,7 @@ /*global modules, contains*/ -modules.locale = '2015-November-16'; +modules.locale = '2015-December-15'; // Global stuff @@ -123,7 +123,11 @@ SnapTranslator.dict.en = { 'translator_e-mail': 'jens@moenig.org', 'last_changed': - '2012-11-16', + '2015-12-15', + + // rewordings in English avoiding having to adjust all other translations + 'any': + 'random', // long strings look-up only 'file menu import hint': @@ -157,7 +161,7 @@ SnapTranslator.dict.de = { 'translator_e-mail': 'jens@moenig.org', 'last_changed': - '2015-10-07' + '2015-12-15' }; SnapTranslator.dict.it = { @@ -313,7 +317,7 @@ SnapTranslator.dict.nl = { 'translator_e-mail': 'frank.sierens@telenet.be, sjoerddirk@fromScratchEd.nl', 'last_changed': - '2013-08-12' + '2015-12-15' }; SnapTranslator.dict.pl = { diff --git a/morphic.js b/morphic.js index 33fbcb82..ca43cc62 100644 --- a/morphic.js +++ b/morphic.js @@ -1052,7 +1052,7 @@ /*global window, HTMLCanvasElement, getMinimumFontHeight, FileReader, Audio, FileList, getBlurredShadowSupport*/ -var morphicVersion = '2015-November-16'; +var morphicVersion = '2015-December-15'; var modules = {}; // keep track of additional loaded modules var useBlurredShadows = getBlurredShadowSupport(); // check for Chrome-bug @@ -4515,7 +4515,7 @@ CursorMorph.prototype.processKeyPress = function (event) { return null; } if (event.keyCode) { // Opera doesn't support charCode - if (event.ctrlKey) { + if (event.ctrlKey && (!event.altKey)) { this.ctrl(event.keyCode, event.shiftKey); } else if (event.metaKey) { this.cmd(event.keyCode, event.shiftKey); @@ -4526,7 +4526,7 @@ CursorMorph.prototype.processKeyPress = function (event) { ); } } else if (event.charCode) { // all other browsers - if (event.ctrlKey) { + if (event.ctrlKey && (!event.altKey)) { this.ctrl(event.charCode, event.shiftKey); } else if (event.metaKey) { this.cmd(event.charCode, event.shiftKey); @@ -4545,7 +4545,7 @@ CursorMorph.prototype.processKeyDown = function (event) { // this.inspectKeyEvent(event); var shift = event.shiftKey; this.keyDownEventUsed = false; - if (event.ctrlKey) { + if (event.ctrlKey && (!event.altKey)) { this.ctrl(event.keyCode, event.shiftKey); // notify target's parent of key event this.target.escalateEvent('reactToKeystroke', event); @@ -8896,6 +8896,7 @@ ScrollFrameMorph.prototype.mouseDownLeft = function (pos) { return null; } var world = this.root(), + hand = world.hand, oldPos = pos, myself = this, deltaX = 0, @@ -8904,10 +8905,18 @@ ScrollFrameMorph.prototype.mouseDownLeft = function (pos) { this.step = function () { var newPos; - if (world.hand.mouseButton && - (world.hand.children.length === 0) && - (myself.bounds.containsPoint(world.hand.position()))) { - newPos = world.hand.bounds.origin; + if (hand.mouseButton && + (hand.children.length === 0) && + (myself.bounds.containsPoint(hand.bounds.origin))) { + + if (hand.grabPosition && + (hand.grabPosition.distanceTo(hand.position()) <= + MorphicPreferences.grabThreshold)) { + // still within the grab threshold + return null; + } + + newPos = hand.bounds.origin; deltaX = newPos.x - oldPos.x; if (deltaX !== 0) { myself.scrollX(deltaX); @@ -10345,7 +10354,7 @@ WorldMorph.prototype.initEventListeners = function () { } event.preventDefault(); } - if ((event.ctrlKey || event.metaKey) && + if ((event.ctrlKey && (!event.altKey) || event.metaKey) && (event.keyCode !== 86)) { // allow pasting-in event.preventDefault(); } diff --git a/objects.js b/objects.js index 7cbfee3b..f6b97385 100644 --- a/objects.js +++ b/objects.js @@ -125,7 +125,7 @@ PrototypeHatBlockMorph*/ // Global stuff //////////////////////////////////////////////////////// -modules.objects = '2015-November-16'; +modules.objects = '2015-December-15'; var SpriteMorph; var StageMorph; @@ -569,6 +569,12 @@ SpriteMorph.prototype.initBlocks = function () { category: 'pen', spec: 'stamp' }, + floodFill: { + only: SpriteMorph, + type: 'command', + category: 'pen', + spec: 'fill' + }, // Control receiveGo: { @@ -602,6 +608,11 @@ SpriteMorph.prototype.initBlocks = function () { category: 'control', spec: 'when I receive %msgHat' }, + receiveCondition: { + type: 'hat', + category: 'control', + spec: 'when %b' + }, doBroadcast: { type: 'command', category: 'control', @@ -1861,12 +1872,14 @@ SpriteMorph.prototype.blockTemplates = function (category) { blocks.push(block('setSize')); blocks.push('-'); blocks.push(block('doStamp')); + blocks.push(block('floodFill')); } else if (cat === 'control') { blocks.push(block('receiveGo')); blocks.push(block('receiveKey')); blocks.push(block('receiveInteraction')); + blocks.push(block('receiveCondition')); blocks.push(block('receiveMessage')); blocks.push('-'); blocks.push(block('doBroadcast')); @@ -3350,6 +3363,60 @@ SpriteMorph.prototype.drawLine = function (start, dest) { } }; +SpriteMorph.prototype.floodFill = function () { + var layer = this.parent.penTrails(), + width = layer.width, + height = layer.height, + ctx = layer.getContext('2d'), + img = ctx.getImageData(0, 0, width, height), + dta = img.data, + stack = [ + ((height / 2) - Math.round(this.yPosition())) * width + + Math.round(this.xPosition() + (width / 2)) + ], + current, + src; + + function read(p) { + var d = p * 4; + return [dta[d], dta[d + 1], dta[d + 2], dta[d + 3]]; + } + + function check(p) { + return p[0] === src[0] && + p[1] === src[1] && + p[2] === src[2] && + p[3] === src[3]; + } + + src = read(stack[0]); + if (src[0] === Math.round(this.color.r) && + src[1] === Math.round(this.color.g) && + src[2] === Math.round(this.color.b) && + src[3] === Math.round(this.color.a * 255)) { + return; + } + while (stack.length > 0) { + current = stack.pop(); + if (check(read(current))) { + if (current % width > 1) { + stack.push(current + 1); + stack.push(current - 1); + } + if (current > 0 && current < height * width) { + stack.push(current + width); + stack.push(current - width); + } + } + dta[current * 4] = Math.round(this.color.r); + dta[current * 4 + 1] = Math.round(this.color.g); + dta[current * 4 + 2] = Math.round(this.color.b); + dta[current * 4 + 3] = Math.round(this.color.a * 255); + } + ctx.putImageData(img, 0, 0); + this.parent.changed(); +}; + // SpriteMorph motion - adjustments due to nesting SpriteMorph.prototype.moveBy = function (delta, justMe) { @@ -3693,6 +3760,15 @@ SpriteMorph.prototype.allHatBlocksForInteraction = function (interaction) { }); }; +SpriteMorph.prototype.allGenericHatBlocks = function () { + return this.scripts.children.filter(function (morph) { + if (morph.selector) { + return morph.selector === 'receiveCondition'; + } + return false; + }); +}; + // SpriteMorph events SpriteMorph.prototype.mouseClickLeft = function () { @@ -4674,7 +4750,6 @@ StageMorph.prototype.hiddenPrimitives = {}; StageMorph.prototype.codeMappings = {}; StageMorph.prototype.codeHeaders = {}; StageMorph.prototype.enableCodeMapping = false; - StageMorph.prototype.enableInheritance = false; // StageMorph instance creation @@ -4695,6 +4770,7 @@ StageMorph.prototype.init = function (globals) { this.sounds = new List(); this.version = Date.now(); // for observers this.isFastTracked = false; + this.enableCustomHatBlocks = true; this.cloneCount = 0; this.timerStart = Date.now(); @@ -5036,6 +5112,9 @@ StageMorph.prototype.step = function () { } // manage threads + if (this.enableCustomHatBlocks) { + this.stepGenericConditions(); + } if (this.isFastTracked && this.threads.processes.length) { this.children.forEach(function (morph) { if (morph instanceof SpriteMorph) { @@ -5072,6 +5151,28 @@ StageMorph.prototype.step = function () { } }; +StageMorph.prototype.stepGenericConditions = function (stopAll) { + var hats = [], + myself = this, + ide; + this.children.concat(this).forEach(function (morph) { + if (morph instanceof SpriteMorph || morph instanceof StageMorph) { + hats = hats.concat(morph.allGenericHatBlocks()); + } + }); + if (!hats.length) { + this.enableCustomHatBlocks = false; + ide = this.parentThatIsA(IDE_Morph); + if (ide) { + ide.controlBar.stopButton.refresh(); + } + return; + } + hats.forEach(function (block) { + myself.threads.doWhen(block, stopAll); + }); +}; + StageMorph.prototype.developersMenu = function () { var myself = this, menu = StageMorph.uber.developersMenu.call(this); @@ -5430,6 +5531,7 @@ StageMorph.prototype.blockTemplates = function (category) { blocks.push(block('receiveGo')); blocks.push(block('receiveKey')); blocks.push(block('receiveInteraction')); + blocks.push(block('receiveCondition')); blocks.push(block('receiveMessage')); blocks.push('-'); blocks.push(block('doBroadcast')); @@ -5988,6 +6090,9 @@ StageMorph.prototype.allHatBlocksForKey StageMorph.prototype.allHatBlocksForInteraction = SpriteMorph.prototype.allHatBlocksForInteraction; +StageMorph.prototype.allGenericHatBlocks + = SpriteMorph.prototype.allGenericHatBlocks; + // StageMorph events StageMorph.prototype.mouseClickLeft @@ -6334,11 +6439,10 @@ Costume.prototype.shrinkWrap = function () { this.version = Date.now(); }; -Costume.prototype.boundingBox = function () { +Costume.prototype.canvasBoundingBox = function (pic) { // answer the rectangle surrounding my contents' non-transparent pixels var row, col, - pic = this.contents, w = pic.width, h = pic.height, ctx = pic.getContext('2d'), @@ -6395,6 +6499,10 @@ Costume.prototype.boundingBox = function () { return new Rectangle(getLeft(), getTop(), getRight(), getBottom()); }; +Costume.prototype.boundingBox = function () { + return this.canvasBoundingBox(this.contents); +}; + // Costume duplication Costume.prototype.copy = function () { diff --git a/paint.js b/paint.js index 61c244e3..f3464792 100644 --- a/paint.js +++ b/paint.js @@ -59,6 +59,7 @@ Sep 29 - tweaks (Jens) Sep 28 [of the following year :)] - Try to prevent antialiasing (Kartik) Oct 02 - revert disable smoothing (Jens) + Dec 15 - center rotation point on costume creating (Craxic) */ /*global Point, Rectangle, DialogBoxMorph, fontHeight, AlignmentMorph, @@ -71,7 +72,7 @@ // Global stuff //////////////////////////////////////////////////////// -modules.paint = '2015-October-02'; +modules.paint = '2015-December-15'; // Declarations @@ -307,6 +308,7 @@ PaintEditorMorph.prototype.refreshToolButtons = function () { }; PaintEditorMorph.prototype.ok = function () { + this.paper.updateAutomaticCenter(); this.callback( this.paper.paper, this.paper.rotationCenter @@ -585,10 +587,35 @@ PaintCanvasMorph.prototype.init = function (shift) { var key = this.world().currentKey; return (key === 16); }; + // should we calculate the center of the image ourselves, + // or use the user position + this.automaticCrosshairs = true; this.buildContents(); }; +// Calculate the center of all the non-transparent pixels on the canvas. +PaintCanvasMorph.prototype.calculateCanvasCenter = function(canvas) { + var canvasBounds = Costume.prototype.canvasBoundingBox(canvas); + if (canvasBounds === null) { + return null; + } + // Can't use canvasBounds.center(), it rounds down. + return new Point((canvasBounds.origin.x + canvasBounds.corner.x) / 2, (canvasBounds.origin.y + canvasBounds.corner.y) / 2); +}; + +// If we are in automaticCrosshairs mode, recalculate the rotationCenter. +PaintCanvasMorph.prototype.updateAutomaticCenter = function () { + if (this.automaticCrosshairs) { + // Calculate this.rotationCenter from this.paper + var rotationCenter = this.calculateCanvasCenter(this.paper); + if (rotationCenter !== null) { + this.rotationCenter = rotationCenter; + } + } +}; + PaintCanvasMorph.prototype.scale = function (x, y) { + this.updateAutomaticCenter(); this.mask = newCanvas(this.extent()); var c = newCanvas(this.extent()); c.getContext("2d").save(); @@ -645,6 +672,7 @@ PaintCanvasMorph.prototype.clearCanvas = function () { PaintCanvasMorph.prototype.toolChanged = function (tool) { this.mask = newCanvas(this.extent()); if (tool === "crosshairs") { + this.updateAutomaticCenter(); this.drawcrosshair(); } this.drawNew(); @@ -908,6 +936,8 @@ PaintCanvasMorph.prototype.mouseMove = function (pos) { } break; case "crosshairs": + // Disable automatic crosshairs: user has now chosen where they should be. + this.automaticCrosshairs = false; this.rotationCenter = relpos.copy(); this.drawcrosshair(mctx); break; diff --git a/snap.html b/snap.html index 618cd4c6..0a6ce8c0 100755 --- a/snap.html +++ b/snap.html @@ -25,9 +25,10 @@ world = new WorldMorph(document.getElementById('world')); world.worldCanvas.focus(); new IDE_Morph().openIn(world); - setInterval(loop, 1); + loop(); }; function loop() { + requestAnimationFrame(loop); world.doOneCycle(); } diff --git a/snap_fast.html b/snap_fast.html new file mode 100755 index 00000000..976bcd63 --- /dev/null +++ b/snap_fast.html @@ -0,0 +1,37 @@ + + + + + Snap! Build Your Own Blocks + + + + + + + + + + + + + + + + + + + + + diff --git a/store.js b/store.js index 1d38e9ad..e2640c13 100644 --- a/store.js +++ b/store.js @@ -61,7 +61,7 @@ SyntaxElementMorph, Variable*/ // Global stuff //////////////////////////////////////////////////////// -modules.store = '2015-October-07'; +modules.store = '2015-December-15'; // XML_Serializer /////////////////////////////////////////////////////// @@ -792,7 +792,7 @@ SnapSerializer.prototype.loadCustomBlocks = function ( // private var myself = this; element.children.forEach(function (child) { - var definition, names, inputs, header, code, comment, i; + var definition, names, inputs, vars, header, code, comment, i; if (child.tag !== 'block-definition') { return; } @@ -836,6 +836,13 @@ SnapSerializer.prototype.loadCustomBlocks = function ( }); } + vars = child.childNamed('variables'); + if (vars) { + definition.variableNames = myself.loadValue( + vars.require('list') + ).asArray(); + } + header = child.childNamed('header'); if (header) { definition.codeHeader = header.contents; @@ -1047,7 +1054,9 @@ SnapSerializer.prototype.loadBlock = function (model, isReporter) { block.isDraggable = true; inputs = block.inputs(); model.children.forEach(function (child, i) { - if (child.tag === 'comment') { + if (child.tag === 'variables') { + this.loadVariables(block.variables, child); + } else if (child.tag === 'comment') { block.comment = this.loadComment(child); block.comment.block = block; } else if (child.tag === 'receiver') { @@ -1728,11 +1737,16 @@ CustomCommandBlockMorph.prototype.toBlockXML = function (serializer) { var scope = this.definition.isGlobal ? undefined : this.definition.receiver.name; return serializer.format( - '%%%', + '%%%%', this.blockSpec, this.definition.isGlobal ? '' : serializer.format(' scope="@"', scope), serializer.store(this.inputs()), + this.definition.variableNames.length ? + '' + + this.variables.toXML(serializer) + + '' + : '', this.comment ? this.comment.toXML(serializer) : '', scope && !this.definition.receiver[serializer.idProperty] ? '' + @@ -1748,6 +1762,7 @@ CustomReporterBlockMorph.prototype.toBlockXML CustomBlockDefinition.prototype.toXML = function (serializer) { var myself = this; + function encodeScripts(array) { return array.reduce(function (xml, element) { if (element instanceof BlockMorph) { @@ -1763,6 +1778,7 @@ CustomBlockDefinition.prototype.toXML = function (serializer) { return serializer.format( '' + '%' + + (this.variableNames.length ? '%' : '@') + '
@
' + '@' + '%%%' + @@ -1771,6 +1787,8 @@ CustomBlockDefinition.prototype.toXML = function (serializer) { this.type, this.category || 'other', this.comment ? this.comment.toXML(serializer) : '', + (this.variableNames.length ? + serializer.store(new List(this.variableNames)) : ''), this.codeHeader || '', this.codeMapping || '', Object.keys(this.declarations).reduce(function (xml, decl) { diff --git a/threads.js b/threads.js index 114b3a8e..76c7e0a0 100644 --- a/threads.js +++ b/threads.js @@ -83,7 +83,7 @@ ArgLabelMorph, localize, XML_Element, hex_sha512*/ // Global stuff //////////////////////////////////////////////////////// -modules.threads = '2015-November-16'; +modules.threads = '2015-December-15'; var ThreadManager; var Process; @@ -128,24 +128,67 @@ function snapEquals(a, b) { return x === y; } -function invoke(block, timeout) { - // exectue the given block synchronously, i.e. without yielding. - // if a timeout (in milliseconds) is specified, abort execution +function invoke( + action, // a BlockMorph or a Context, a reified ("ringified") block + contextArgs, // optional List of arguments for the context, or null + receiver, // optional sprite or environment + timeout, // msecs + timeoutErrorMsg, // string + suppressErrors // bool +) { + // execute the given block or context synchronously without yielding. + // Apply context (not a block) to a list of optional arguments. + // Receiver (sprite, stage or environment), timeout etc. are optional. + // If a timeout (in milliseconds) is specified, abort execution // after the timeout has been reached and throw an error. - // For debugging purposes only. + // SuppressErrors (bool) if non-timeout errors occurring in the + // block are handled elsewhere. + // This is highly experimental. // Caution: Kids, do not try this at home! - // use ThreadManager::startProcess instead - var proc = new Process(block), - startTime = Date.now(); - while (proc.isRunning()) { - if (timeout && ((Date.now() - startTime) > timeout)) { - throw (new Error("a synchronous Snap! script has timed out")); + // Use ThreadManager::startProcess with a callback instead + + var proc = new Process(), + deadline = (timeout ? Date.now() + timeout : null), + rcvr; + + if (action instanceof Context) { + if (receiver) { + action = proc.reportContextFor(receiver); } - proc.runStep(); + proc.initializeFor(action, contextArgs || new List()); + } else if (action instanceof BlockMorph) { + proc.topBlock = action; + rcvr = receiver || action.receiver(); + if (rcvr) { + proc.homeContext = new Context(); + proc.homeContext.receiver = rcvr; + if (rcvr.variables) { + proc.homeContext.variables.parentFrame = rcvr.variables; + } + } + proc.context = new Context( + null, + action.blockSequence(), + proc.homeContext + ); + } else { + throw new Error('expecting a block or ring but getting ' + action); } - if (block instanceof ReporterBlockMorph) { - return proc.homeContext.inputs[0]; + if (suppressErrors) { + proc.isCatchingErrors = false; } + while (proc.isRunning()) { + if (deadline && (Date.now() > deadline)) { + throw (new Error( + localize( + timeoutErrorMsg || + "a synchronous Snap! script has timed out") + ) + ); + } + proc.runStep(deadline); + } + return proc.homeContext.inputs[0]; } // ThreadManager /////////////////////////////////////////////////////// @@ -154,12 +197,14 @@ function ThreadManager() { this.processes = []; } +ThreadManager.prototype.pauseCustomHatBlocks = false; + ThreadManager.prototype.toggleProcess = function (block) { var active = this.findProcess(block); if (active) { active.stop(); } else { - return this.startProcess(block); + return this.startProcess(block, null, null, null, true); } }; @@ -167,7 +212,8 @@ ThreadManager.prototype.startProcess = function ( block, isThreadSafe, exportResult, - callback + callback, + isClicked ) { var active = this.findProcess(block), top = block.topBlock(), @@ -181,6 +227,7 @@ ThreadManager.prototype.startProcess = function ( } newProc = new Process(block.topBlock(), callback); newProc.exportResult = exportResult; + newProc.isClicked = isClicked || false; if (!newProc.homeContext.receiver.isClone) { top.addHighlight(); } @@ -267,7 +314,7 @@ ThreadManager.prototype.removeTerminatedProcesses = function () { } } - if (proc.topBlock instanceof ReporterBlockMorph) { + if (proc.topBlock instanceof ReporterBlockMorph || proc.isShowingResult) { if (proc.onComplete instanceof Function) { proc.onComplete(proc.homeContext.inputs[0]); } else { @@ -303,6 +350,38 @@ ThreadManager.prototype.findProcess = function (block) { ); }; +ThreadManager.prototype.doWhen = function (block, stopIt) { + if (this.pauseCustomHatBlocks) {return; } + var pred = block.inputs()[0]; + if (block.removeHighlight()) { + block.world().hand.destroyTemporaries(); + } + if (stopIt) {return; } + if ((!block) || + !(pred instanceof ReporterBlockMorph) || + this.findProcess(block) + ) {return; } + try { + if (invoke( + pred, + null, + null, + 20, + 'the predicate takes\ntoo long for a\ncustom hat block', + true // suppress errors => handle them right here instead + ) === true) { + this.startProcess(block); + } + } catch (error) { + block.addErrorHighlight(); + block.showBubble( + error.name + + '\n' + + error.message + ); + } +}; + // Process ///////////////////////////////////////////////////////////// /* @@ -349,6 +428,10 @@ ThreadManager.prototype.findProcess = function (block) { httpRequest active instance of an HttpRequest or null pauseOffset msecs between the start of an interpolated operation and when the process was paused + isClicked boolean flag indicating whether the process was + initiated by a user-click on a block + isShowingResult boolean flag indicating whether a "report" command + has been executed in a user-clicked process exportResult boolean flag indicating whether a picture of the top block along with the result bubble shoud be exported onComplete an optional callback function to be executed when @@ -369,6 +452,8 @@ function Process(topBlock, onComplete) { this.readyToYield = false; this.readyToTerminate = false; this.isDead = false; + this.isClicked = false; + this.isShowingResult = false; this.errorFlag = false; this.context = null; this.homeContext = new Context(); @@ -404,7 +489,7 @@ Process.prototype.isRunning = function () { // Process entry points -Process.prototype.runStep = function () { +Process.prototype.runStep = function (deadline) { // a step is an an uninterruptable 'atom', it can consist // of several contexts, even of several blocks @@ -422,6 +507,14 @@ Process.prototype.runStep = function () { if (this.isPaused) { return this.pauseStep(); } + if (deadline && (Date.now() > deadline)) { + if (this.isAtomic && + this.homeContext.receiver && + this.homeContext.receiver.endWarp) { + this.homeContext.receiver.endWarp(); + } + return; + } this.evaluateContext(); } this.lastYield = Date.now(); @@ -505,9 +598,12 @@ Process.prototype.evaluateContext = function () { }; Process.prototype.evaluateBlock = function (block, argCount) { + var selector = block.selector; // check for special forms - if (contains(['reportOr', 'reportAnd', 'doReport'], block.selector)) { - return this[block.selector](block); + if (selector === 'reportOr' || + selector === 'reportAnd' || + selector === 'doReport') { + return this[selector](block); } // first evaluate all inputs, then apply the primitive @@ -517,13 +613,13 @@ Process.prototype.evaluateBlock = function (block, argCount) { if (argCount > inputs.length) { this.evaluateNextInput(block); } else { - if (this[block.selector]) { + if (this[selector]) { rcvr = this; } if (this.isCatchingErrors) { try { this.returnValueToParentContext( - rcvr[block.selector].apply(rcvr, inputs) + rcvr[selector].apply(rcvr, inputs) ); this.popContext(); } catch (error) { @@ -531,7 +627,7 @@ Process.prototype.evaluateBlock = function (block, argCount) { } } else { this.returnValueToParentContext( - rcvr[block.selector].apply(rcvr, inputs) + rcvr[selector].apply(rcvr, inputs) ); this.popContext(); } @@ -574,6 +670,9 @@ Process.prototype.reportAnd = function (block) { Process.prototype.doReport = function (block) { var outer = this.context.outerContext; + if (this.isClicked && (block.topBlock() === this.topBlock)) { + this.isShowingResult = true; + } if (this.context.expression.partOfCustomCommand) { this.doStopCustomBlock(); this.popContext(); @@ -655,10 +754,9 @@ Process.prototype.evaluateInput = function (input) { } else { ans = input.evaluate(); if (ans) { - if (contains( - [CommandSlotMorph, ReporterSlotMorph], - input.constructor - ) || (input instanceof CSlotMorph && + if (input.constructor === CommandSlotMorph || + input.constructor === ReporterSlotMorph || + (input instanceof CSlotMorph && (!input.isStatic || input.isLambda))) { // I know, this still needs yet to be done right.... ans = this.reify(ans, new List()); @@ -718,6 +816,7 @@ Process.prototype.evaluateNextInput = function (element) { var nxt = this.context.inputs.length, args = element.inputs(), exp = args[nxt], + sel = this.context.expression.selector, outer = this.context.outerContext; // for tail call elimination if (exp.isUnevaluated) { @@ -729,8 +828,7 @@ Process.prototype.evaluateNextInput = function (element) { THE SCRIPT), because those allow for additional explicit parameter bindings. */ - if (contains(['reify', 'reportScript'], - this.context.expression.selector)) { + if (sel === 'reify' || sel === 'reportScript') { this.context.addInput(exp); } else { this.context.addInput(this.reify(exp, new List())); @@ -944,6 +1042,15 @@ Process.prototype.evaluate = function ( }; Process.prototype.fork = function (context, args) { + var proc = new Process(), + stage = this.homeContext.receiver.parentThatIsA(StageMorph); + proc.initializeFor(context, args); + proc.pushContext('doYield'); + stage.threads.processes.push(proc); +}; + +Process.prototype.initializeFor = function (context, args) { + // used by Process.fork() and global invoke() if (context.isContinuation) { throw new Error( 'continuations cannot be forked' @@ -961,8 +1068,7 @@ Process.prototype.fork = function (context, args) { parms = args.asArray(), i, value, - stage = this.homeContext.receiver.parentThatIsA(StageMorph), - proc = new Process(); + exit; // assign parameters if any were passed if (parms.length > 0) { @@ -1008,13 +1114,24 @@ Process.prototype.fork = function (context, args) { if (runnable.expression instanceof CommandBlockMorph) { runnable.expression = runnable.expression.blockSequence(); + + // insert a tagged exit context + // which "report" can catch later + // needed for invoke() situations + exit = new Context( + runnable.parentContext, + 'expectReport', + outer, + outer.receiver + ); + exit.tag = 'exit'; + runnable.parentContext = exit; } - proc.homeContext = context.outerContext; - proc.topBlock = context.expression; - proc.context = runnable; - proc.pushContext('doYield'); - stage.threads.processes.push(proc); + this.homeContext = new Context(); // context.outerContext; + this.homeContext.receiver = context.outerContext.receiver; + this.topBlock = context.expression; + this.context = runnable; }; // Process stopping blocks primitives @@ -1091,8 +1208,21 @@ Process.prototype.evaluateCustomBlock = function () { this.procedureCount += 1; outer = new Context(); outer.receiver = this.context.receiver; - outer.variables.parentFrame = outer.receiver ? - outer.receiver.variables : null; + + outer.variables.parentFrame = this.context.expression.variables; + + // block (instance) var support, experimental: + // only splice in block vars if any are defined, because block vars + // can cause race conditions in global block definitions that + // access sprite-local variables at the same time. + if (this.context.expression.definition.variableNames.length) { + this.context.expression.variables.parentFrame = outer.receiver ? + outer.receiver.variables : null; + } else { + // original code without block variables: + outer.variables.parentFrame = outer.receiver ? + outer.receiver.variables : null; + } runnable = new Context( this.context.parentContext, @@ -1420,7 +1550,10 @@ Process.prototype.reportListItem = function (index, list) { }; Process.prototype.reportListLength = function (list) { - return list.length(); + if (list instanceof List) { + return list.length(); + } + return list.length; // catch a common student error }; Process.prototype.reportListContainsItem = function (list, element) { @@ -2110,10 +2243,12 @@ Process.prototype.reportIsIdentical = function (a, b) { Process.prototype.isImmutable = function (obj) { // private - return contains( - ['nothing', 'Boolean', 'text', 'number', 'undefined'], - this.reportTypeOf(obj) - ); + var type = this.reportTypeOf(obj); + return type === 'nothing' || + type === 'Boolean' || + type === 'text' || + type === 'number' || + type === 'undefined'; }; Process.prototype.reportTrue = function () {