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 @@ + + +
+ +@
' +
'