diff --git a/blocks.js b/blocks.js index 8e33ecaa..8a307d8e 100644 --- a/blocks.js +++ b/blocks.js @@ -148,7 +148,7 @@ CustomCommandBlockMorph, SymbolMorph*/ // Global stuff //////////////////////////////////////////////////////// -modules.blocks = '2017-September-01'; +modules.blocks = '2017-September-06'; var SyntaxElementMorph; var BlockMorph; @@ -1025,7 +1025,8 @@ SyntaxElementMorph.prototype.labelPart = function (spec) { 'whitespace' : ['whitespace'], 'line' : ['line'], 'tab' : ['tab'], - 'cr' : ['cr'] + 'cr' : ['cr'], + 'csv' : ['csv'] }, false // read-only ); @@ -1972,7 +1973,7 @@ SyntaxElementMorph.prototype.exportPictureWithResult = function (aBubble) { // request to open pic in new window. ide.saveCanvasAs( pic, - ide.projetName || localize('Untitled') + ' ' + localize('script pic') + (ide.projetName || localize('untitled')) + ' ' + localize('script pic') ); }; @@ -2550,7 +2551,7 @@ BlockMorph.prototype.userMenu = function () { ); ide.saveCanvasAs( myself.topBlock().scriptPic(), - ide.projetName || localize('Untitled') + ' ' + + (ide.projetName || localize('untitled')) + ' ' + localize('script pic') ); }, @@ -6230,7 +6231,7 @@ ScriptsMorph.prototype.userMenu = function () { ); if (ide) { menu.addLine(); - if (obj.exemplar) { + if (!blockEditor && obj.exemplar) { addOption( 'inherited', function () { @@ -6304,7 +6305,7 @@ ScriptsMorph.prototype.exportScriptsPicture = function () { if (pic) { ide.saveCanvasAs( pic, - ide.projetName || localize('Untitled') + ' ' + + (ide.projetName || localize('untitled')) + ' ' + localize('script pic') ); } @@ -8193,7 +8194,7 @@ InputSlotMorph.prototype.distancesMenu = function () { allNames = []; stage.children.forEach(function (morph) { - if (morph instanceof SpriteMorph) { + if (morph instanceof SpriteMorph && !morph.isTemporary) { if (morph.name !== rcvr.name) { allNames = allNames.concat(morph.name); } @@ -8239,7 +8240,7 @@ InputSlotMorph.prototype.objectsMenu = function () { dict[stage.name] = stage.name; stage.children.forEach(function (morph) { - if (morph instanceof SpriteMorph) { + if (morph instanceof SpriteMorph && !morph.isTemporary) { allNames.push(morph.name); } }); @@ -11655,7 +11656,7 @@ CommentMorph.prototype.userMenu = function () { var ide = myself.parentThatIsA(IDE_Morph); ide.saveCanvasAs( myself.fullImageClassic(), - ide.projetName || localize('Untitled') + ' ' + + (ide.projetName || localize('untitled')) + ' ' + localize('comment pic') ); }, diff --git a/byob.js b/byob.js index 6a3d5a71..d6da9a2c 100644 --- a/byob.js +++ b/byob.js @@ -930,7 +930,7 @@ CustomCommandBlockMorph.prototype.userMenu = function () { var ide = this.world().children[0]; ide.saveCanvasAs( this.topBlock().scriptPic(), - ide.projectName || localize('Untitled') + ' ' + + (ide.projectName || localize('untitled')) + ' ' + localize('script pic') ); }, @@ -3705,7 +3705,7 @@ BlockExportDialogMorph.prototype.exportBlocks = function () { + ''; ide.saveXMLAs( str, - ide.projectName || localize('Untitled') + ' ' + localize('blocks') + (ide.projectName || localize('untitled')) + ' ' + localize('blocks') ); } else { new DialogBoxMorph().inform( diff --git a/gui.js b/gui.js index e331c84b..a53cc9cb 100644 --- a/gui.js +++ b/gui.js @@ -43,8 +43,9 @@ TurtleIconMorph CostumeIconMorph WardrobeMorph - StageHandleMorph; - PaletteHandleMorph; + StageHandleMorph + PaletteHandleMorph + CamSnapshotDialogMorph credits @@ -74,7 +75,7 @@ isRetinaSupported, SliderMorph, Animation*/ // Global stuff //////////////////////////////////////////////////////// -modules.gui = '2017-September-01'; +modules.gui = '2017-September-08'; // Declarations @@ -89,6 +90,7 @@ var SoundIconMorph; var JukeboxMorph; var StageHandleMorph; var PaletteHandleMorph; +var CamSnapshotDialogMorph; // IDE_Morph /////////////////////////////////////////////////////////// @@ -1419,6 +1421,7 @@ IDE_Morph.prototype.createCorralBar = function () { var padding = 5, newbutton, paintbutton, + cambutton, colors = [ this.groupColor, this.frameColor.darker(50), @@ -1480,6 +1483,46 @@ IDE_Morph.prototype.createCorralBar = function () { this.corralBar.left() + padding + newbutton.width() + padding ); this.corralBar.add(paintbutton); + + cambutton = new PushButtonMorph( + this, + "newCamSprite", + new SymbolMorph("camera", 15) + ); + + cambutton.corner = 12; + cambutton.color = colors[0]; + cambutton.highlightColor = colors[1]; + cambutton.pressColor = colors[2]; + cambutton.labelMinExtent = new Point(36, 18); + cambutton.padding = 0; + cambutton.labelShadowOffset = new Point(-1, -1); + cambutton.labelShadowColor = colors[1]; + cambutton.labelColor = this.buttonLabelColor; + cambutton.contrast = this.buttonContrast; + cambutton.drawNew(); + cambutton.hint = "take a camera snapshot and\nimport it as a new sprite"; + cambutton.fixLayout(); + cambutton.setCenter(this.corralBar.center()); + cambutton.setLeft( + this.corralBar.left() + + padding + + newbutton.width() + + padding + + paintbutton.width() + + padding + ); + + if (location.protocol === 'http:') { + cambutton.hint = 'Due to browser security policies, you need to\n' + + 'access Snap! through HTTPS to use the camera.\n\n' + + 'Plase replace the "http://" part of the address\n' + + 'in your browser by "https://" and try again.'; + cambutton.disable(); + } + + this.corralBar.add(cambutton); + }; IDE_Morph.prototype.createCorral = function () { @@ -2140,6 +2183,30 @@ IDE_Morph.prototype.paintNewSprite = function () { ); }; +IDE_Morph.prototype.newCamSprite = function () { + var sprite = new SpriteMorph(this.globalVariables), + camDialog, + myself = this; + + sprite.name = this.newSpriteName(sprite.name); + sprite.setCenter(this.stage.center()); + this.stage.add(sprite); + this.sprites.add(sprite); + this.corral.addSprite(sprite); + this.selectSprite(sprite); + + camDialog = new CamSnapshotDialogMorph( + this, + sprite, + function () { myself.removeSprite(sprite); }, + function (costume) { + sprite.addCostume(costume); + sprite.wearCostume(costume); + }); + + camDialog.popUp(this.world()); +}; + IDE_Morph.prototype.duplicateSprite = function (sprite) { var duplicate = sprite.fullCopy(); duplicate.setPosition(this.world().hand.position()); @@ -6617,7 +6684,7 @@ SpriteIconMorph.prototype.init = function (aSprite, aTemplate) { hover = function () { if (!aSprite.exemplar) {return null; } - return (localize('parent' + ':\n' + aSprite.exemplar.name)); + return (localize('parent') + ':\n' + aSprite.exemplar.name); }; // additional properties: @@ -7187,7 +7254,7 @@ CostumeIconMorph.prototype.removeCostume = function () { var wardrobe = this.parentThatIsA(WardrobeMorph), idx = this.parent.children.indexOf(this), ide = this.parentThatIsA(IDE_Morph); - wardrobe.removeCostumeAt(idx - 2); + wardrobe.removeCostumeAt(idx - 3); // ignore the paintbrush and camera buttons if (ide.currentSprite.costume === this.object) { ide.currentSprite.wearCostume(null); } @@ -7455,12 +7522,14 @@ WardrobeMorph.prototype.updateList = function () { x = this.left() + 5, y = this.top() + 5, padding = 4, + toolsPadding = 5, oldFlag = Morph.prototype.trackChanges, oldPos = this.contents.position(), icon, template, txt, - paintbutton; + paintbutton, + cambutton; this.changed(); oldFlag = Morph.prototype.trackChanges; @@ -7501,9 +7570,40 @@ WardrobeMorph.prototype.updateList = function () { paintbutton.setCenter(icon.center()); paintbutton.setLeft(icon.right() + padding * 4); - this.addContents(paintbutton); + cambutton = new PushButtonMorph( + this, + "newFromCam", + new SymbolMorph("camera", 15) + ); + cambutton.padding = 0; + cambutton.corner = 12; + cambutton.color = IDE_Morph.prototype.groupColor; + cambutton.highlightColor = IDE_Morph.prototype.frameColor.darker(50); + cambutton.pressColor = paintbutton.highlightColor; + cambutton.labelMinExtent = new Point(36, 18); + cambutton.labelShadowOffset = new Point(-1, -1); + cambutton.labelShadowColor = paintbutton.highlightColor; + cambutton.labelColor = TurtleIconMorph.prototype.labelColor; + cambutton.contrast = this.buttonContrast; + cambutton.drawNew(); + cambutton.hint = "Import a new costume from your webcam"; + cambutton.setPosition(new Point(x, y)); + cambutton.fixLayout(); + cambutton.setCenter(paintbutton.center()); + cambutton.setLeft(paintbutton.right() + toolsPadding); + + if (location.protocol === 'http:') { + cambutton.hint = 'Due to browser security policies, you need to\n' + + 'access Snap! through HTTPS to use the camera.\n\n' + + 'Plase replace the "http://" part of the address\n' + + 'in your browser by "https://" and try again.'; + cambutton.disable(); + } + + this.addContents(cambutton); + txt = new TextMorph(localize( "costumes tab help" // look up long string in translator )); @@ -7514,7 +7614,6 @@ WardrobeMorph.prototype.updateList = function () { this.addContents(txt); y = txt.bottom() + padding; - this.sprite.costumes.asArray().forEach(function (costume) { template = icon = new CostumeIconMorph(costume, template); icon.setPosition(new Point(x, y)); @@ -7574,6 +7673,25 @@ WardrobeMorph.prototype.paintNew = function () { }); }; +WardrobeMorph.prototype.newFromCam = function () { + var camDialog, + ide = this.parentThatIsA(IDE_Morph), + myself = this, + sprite = this.sprite; + + camDialog = new CamSnapshotDialogMorph( + ide, + sprite, + nop, + function (costume) { + sprite.addCostume(costume); + sprite.wearCostume(costume); + myself.updateList(); + }); + + camDialog.popUp(this.world()); +}; + // Wardrobe drag & drop WardrobeMorph.prototype.wantsDropOf = function (morph) { @@ -8178,3 +8296,133 @@ PaletteHandleMorph.prototype.mouseDoubleClick = function () { this.target.parentThatIsA(IDE_Morph).setPaletteWidth(200); }; +// CamSnapshotDialogMorph //////////////////////////////////////////////////// + +/* + I am a dialog morph that lets users take a snapshot using their webcam + and use it as a costume for their sprites or a background for the Stage. +*/ + +// CamSnapshotDialogMorph inherits from DialogBoxMorph: + + +CamSnapshotDialogMorph.prototype = new DialogBoxMorph(); +CamSnapshotDialogMorph.prototype.constructor = CamSnapshotDialogMorph; +CamSnapshotDialogMorph.uber = DialogBoxMorph.prototype; + +// CamSnapshotDialogMorph instance creation + +function CamSnapshotDialogMorph(ide, sprite, onCancel, onAccept) { + this.init(ide, sprite, onCancel, onAccept); +} + +CamSnapshotDialogMorph.prototype.init = function ( + ide, + sprite, + onCancel, +onAccept) { + this.ide = ide; + this.sprite = sprite; + this.padding = 10; + + this.oncancel = onCancel; + this.accept = onAccept; + + this.videoElement = null; // an HTML5 video element + this.videoView = new Morph(); // a morph where we'll copy the video contents + + CamSnapshotDialogMorph.uber.init.call(this); + + this.labelString = 'Camera'; + this.createLabel(); + + this.buildContents(); +}; + +CamSnapshotDialogMorph.prototype.buildContents = function () { + var myself = this; + + this.videoElement = document.createElement('video'); + this.videoElement.hidden = true; + document.body.appendChild(this.videoElement); + + if (navigator.mediaDevices && navigator.mediaDevices.getUserMedia) { + navigator.mediaDevices.getUserMedia({ video: true }) + .then(function (stream) { + myself.videoElement.src = window.URL.createObjectURL(stream); + myself.videoElement.play(); + myself.videoElement.stream = stream; + }); + } + + this.videoView.setExtent(this.ide.stage.dimensions); + this.videoView.image = newCanvas(this.videoView.extent()); + this.videoView.drawOn = function (aCanvas) { + var context = aCanvas.getContext('2d'), + w = this.width(), + h = this.height(); + + context.save(); + + // Flip the image so it looks like a mirror + context.translate(w, 0); + context.scale(-1, 1); + + context.drawImage( + myself.videoElement, + this.left() * -1, + this.top(), + w, + h + ); + + context.restore(); + }; + + this.videoView.step = function () { + this.changed(); + }; + + this.addBody(new AlignmentMorph('column', this.padding / 2)); + this.body.add(this.videoView); + this.body.fixLayout(); + + this.addButton('ok', 'Save'); + this.addButton('cancel', 'Cancel'); + + this.fixLayout(); + this.drawNew(); +}; + +CamSnapshotDialogMorph.prototype.ok = function () { + var stage = this.ide.stage, + canvas = newCanvas(stage.dimensions), + context = canvas.getContext('2d'); + + context.translate(stage.dimensions.x, 0); + context.scale(-1, 1); + + context.drawImage( + this.videoElement, + 0, + 0, + stage.dimensions.x, + stage.dimensions.y + ); + + this.accept(new Costume(canvas), this.sprite.newCostumeName('camera')); + this.close(); +}; + +CamSnapshotDialogMorph.prototype.destroy = function () { + this.oncancel.call(this); + this.close(); +}; + +CamSnapshotDialogMorph.prototype.close = function () { + if (this.videoElement) { + this.videoElement.stream.getTracks()[0].stop(); + this.videoElement.remove(); + } + CamSnapshotDialogMorph.uber.destroy.call(this); +}; diff --git a/history.txt b/history.txt index 71f34906..d31da3f3 100755 --- a/history.txt +++ b/history.txt @@ -3601,12 +3601,31 @@ Fixes: * Morphic, Objects: fixed #1843 * Croation translation update, thanks, Zeljko Hrvoj! +170904 +------ +* Blocks, Objects: fixed #1339 + +170905 +------ +* German translation update +* Threads, Objects: Renamed “http” block to “url”, use location.protocol (support https) + +170906 +------ +* Blocks, Threads: added “csv” option to the SPLIT primitive +* Threads: allow https query from locally loaded sources (thanks, Michael, for the hint!) + +170908 +------ +* GUI, Objects, Widgets, Symbols: Camera Snapshot Dialog. Thank you, Bernat!! + v4.1 Features: * polymorphic sprite-local custom blocks * inheritance of sprite-local custom blocks * inheritance of sprite attributes (x, y, direction, size, costumes, costume #, sounds, scripts) * first-class costumes and sounds +* camera snapshots for costumes and new sprites * localization support when typing expressions * support for user-forced line-breaks in custom block labels * ternary Boolean slot setting: support to limit Boolean input slots to “true/false” outside of rings and in palette @@ -3625,6 +3644,8 @@ v4.1 Features: * two-item lists as x-y coordinate arguments for "point towards”, "go to" and “distance to” primitives * Piano keyboard as drop-down menu for entering musical notes, Thanks, Michael! * Basic “instruments” support: sine, square, sawtooth and triangle waves +* Support https in “url” reporter +* splitting csv-text Fixes: * changed keyboard shortcut indicator for “find blocks” to “^” @@ -3635,4 +3656,5 @@ Fixes: * fixed rotation-bug when flipping costumes in "only turn left/right" mode" * fixed variable renaming (“refactoring”) bugs, thanks, Bernat! * fixed “fill” block crash when applying the same color twice +* fixed occasional empty drop-down menu items named “close” * fixed some typos diff --git a/lang-de.js b/lang-de.js index 00286bfd..99bbeb46 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': - '2017-06-24', // this, too, will appear in the Translators tab + '2017-09-05', // this, too, will appear in the Translators tab // GUI // control bar: @@ -381,8 +381,10 @@ SnapTranslator.dict.de = { 'stoppe alle Kl\u00e4nge', 'rest for %n beats': 'spiele Pause f\u00fcr %n Schl\u00e4ge', - 'play note %n for %n beats': - 'spiele Note %n f\u00fcr %n Schl\u00e4ge', + 'play note %note for %n beats': + 'spiele Note %note f\u00fcr %n Schl\u00e4ge', + 'set instrument to %inst': + 'setze Instrument auf %inst', 'change tempo by %n': '\u00e4ndere Tempo um %n', 'set tempo to %n bpm': @@ -390,6 +392,16 @@ SnapTranslator.dict.de = { 'tempo': 'Tempo', + // "instruments", i.e. wave forms + '(1) sine': + '(1) Sinus', + '(2) square': + '(2) Quadrat', + '(3) sawtooth': + '(3) Sägeblatt', + '(4) triangle': + '(4) Dreieck', + // pen: 'clear': 'wische', @@ -495,10 +507,16 @@ SnapTranslator.dict.de = { 'Wenn ich geklont werde', 'create a clone of %cln': 'klone %cln', + 'a new clone of %cln': + 'neuer Klon von %cln', 'myself': - 'mich', + 'selbst', 'delete this clone': 'entferne diesen Klon', + 'tell %spr to %cl': + 'lasse %spr tun %cl', + 'ask %spr for %repRing': + 'frage %spr nach %repRing', // sensing: 'touching %col ?': @@ -537,6 +555,24 @@ SnapTranslator.dict.de = { 'Turbomodus?', 'set turbo mode to %b': 'setze Turbomodus auf %b', + 'current %dates': + 'Kalender %dates', + 'year': + 'Jahr', + 'month': + 'Monat', + 'date': + 'Datum', + 'day of week': + 'Wochentag', + 'hour': + 'Stunde', + 'minute': + 'Minute', + 'second': + 'Sekunde', + 'time in milliseconds': + 'Zeit in Millisekunden', 'filtered for %clr': 'nach %clr gefiltert', @@ -584,6 +620,8 @@ SnapTranslator.dict.de = { 'ist %s ein(e) %typ ?', 'is %s identical to %s ?': 'ist %s identisch mit %s ?', + 'JavaScript function ( %mult%s ) { %code }': + 'JavaScript Funktion ( %mult%s ) { %code }', 'type of %s': 'Typ von %s', @@ -595,6 +633,8 @@ SnapTranslator.dict.de = { 'Variablenname', 'Script variable name': 'Skriptvariablenname', + 'inherit %shd': + 'erbe %shd', 'Delete a variable': 'Variable l\u00f6schen', @@ -797,6 +837,10 @@ SnapTranslator.dict.de = { 'ausschalten um Schieber\nin Eingabefeldern zu verhindern', 'check to enable\ninput sliders for\nentry fields': 'einschalten um Schieber\nin Eingabefeldern zu aktivieren', + 'Retina display support': + 'Retina Bildschirmauflösung', + 'Codification support': + 'Kodifikation', 'Clicking sound': 'Akustisches Klicken', 'uncheck to turn\nblock clicking\nsound off': @@ -909,6 +953,8 @@ SnapTranslator.dict.de = { // sprites: 'edit': 'Bearbeiten', + 'clone': + 'Klonen', 'move': 'Verschieben', 'pivot': @@ -921,6 +967,14 @@ SnapTranslator.dict.de = { 'Alle Teile abtrennen', 'export...': 'Exportieren...', + 'parent...': + 'Vorfahr...', + 'current parent': + 'aktueller Vorfahr', + 'release': + 'Entlassen', + 'make temporary and\nhide in the sprite corral': + 'temporär machen\nund Icon verstecken', // stage: 'show all': @@ -1099,6 +1153,8 @@ SnapTranslator.dict.de = { // block editor 'Block Editor': 'Blockeditor', + 'Method': + 'Methode', 'Apply': 'Anwenden', @@ -1437,4 +1493,18 @@ SnapTranslator.dict.de = { 'Name', 'stage': 'B\u00fchne', + 'costumes': + 'Kostüme', + 'sounds': + 'Klänge', + 'scripts': + 'Skripte', + + // inheritance + 'inherited': + 'geerbt', + 'check to inherit\nfrom': + 'einschalten, um zu erben\nvon', + 'uncheck to\ndisinherit': + 'ausschalten, um \nnicht mehr zu erben' }; diff --git a/locale.js b/locale.js index 71e9f883..7f82e061 100644 --- a/locale.js +++ b/locale.js @@ -42,7 +42,7 @@ /*global modules, contains*/ -modules.locale = '2017-September-01'; +modules.locale = '2017-September-05'; // Global stuff @@ -160,7 +160,7 @@ SnapTranslator.dict.de = { 'translator_e-mail': 'jens@moenig.org', 'last_changed': - '2017-06-24' + '2017-09-05' }; SnapTranslator.dict.it = { diff --git a/objects.js b/objects.js index 693b0078..7f2e2756 100644 --- a/objects.js +++ b/objects.js @@ -82,7 +82,7 @@ SpeechBubbleMorph, RingMorph, isNil, FileReader, TableDialogMorph, BlockEditorMorph, BlockDialogMorph, PrototypeHatBlockMorph, localize, TableMorph, TableFrameMorph, normalizeCanvas, BooleanSlotMorph, HandleMorph*/ -modules.objects = '2017-September-01'; +modules.objects = '2017-September-08'; var SpriteMorph; var StageMorph; @@ -899,7 +899,7 @@ SpriteMorph.prototype.initBlocks = function () { reportURL: { type: 'reporter', category: 'sensing', - spec: 'http:// %s', + spec: 'url %s', defaults: ['snap.berkeley.edu'] }, reportIsFastTracking: { @@ -5291,6 +5291,7 @@ SpriteMorph.prototype.chooseExemplar = function () { myself = this, other = stage.children.filter(function (m) { return m instanceof SpriteMorph && + !m.isTemporary && (!contains(m.allExemplars(), myself)); }), menu; diff --git a/symbols.js b/symbols.js index cea7a7fe..b7d20bf2 100644 --- a/symbols.js +++ b/symbols.js @@ -41,7 +41,7 @@ // Global stuff //////////////////////////////////////////////////////// -modules.symbols = '2017-September-01'; +modules.symbols = '2017-September-08'; var SymbolMorph; @@ -129,7 +129,8 @@ SymbolMorph.prototype.names = [ 'arrowRightOutline', 'robot', 'magnifyingGlass', - 'notes' + 'notes', + 'camera' ]; // SymbolMorph instance creation: @@ -303,6 +304,8 @@ SymbolMorph.prototype.symbolCanvasColored = function (aColor) { return this.drawSymbolMagnifyingGlass(canvas, aColor); case 'notes': return this.drawSymbolNotes(canvas, aColor); + case 'camera': + return this.drawSymbolCamera(canvas, aColor); default: return canvas; } @@ -1516,3 +1519,38 @@ SymbolMorph.prototype.drawSymbolNotes = function (canvas, color) { ctx.stroke(); return canvas; }; + +SymbolMorph.prototype.drawSymbolCamera = function (canvas, color) { + // answer a canvas showing a camera + var ctx = canvas.getContext('2d'), + w = canvas.width, + h = canvas.width, + r = w * 0.16, + l = Math.max(w / 20, 0.5); + + ctx.lineWidth = l * 2; + + // camera body + ctx.fillStyle = color.toString(); + ctx.beginPath(); + ctx.moveTo(l, h * 5 / 6); + ctx.lineTo(w - l, h * 5 / 6); + ctx.lineTo(w - l, h / 4); + ctx.lineTo(w * 3 / 4 , h / 4); + ctx.lineTo(w * 5 / 8 , l); + ctx.lineTo(w * 3 / 8 , l); + ctx.lineTo(w / 4 , h / 4); + ctx.lineTo(l , h / 4); + ctx.closePath(); + ctx.fill(); + + // camera lens + ctx.save(); + ctx.globalCompositeOperation = 'destination-out'; + ctx.beginPath(); + ctx.arc(w / 2, h / 2, r, radians(0), radians(360), false); + ctx.fill(); + ctx.restore(); + + return canvas; +}; diff --git a/threads.js b/threads.js index 14a32229..f9dd896b 100644 --- a/threads.js +++ b/threads.js @@ -61,7 +61,7 @@ StageMorph, SpriteMorph, StagePrompterMorph, Note, modules, isString, copy, isNil, WatcherMorph, List, ListWatcherMorph, alert, console, TableMorph, TableFrameMorph, ColorSlotMorph, isSnapObject*/ -modules.threads = '2017-September-01'; +modules.threads = '2017-September-06'; var ThreadManager; var Process; @@ -2267,8 +2267,17 @@ Process.prototype.reportLastAnswer = function () { Process.prototype.reportURL = function (url) { var response; if (!this.httpRequest) { + // use the location protocol unless the user specifies otherwise + if (url.indexOf('//') < 0) { + if (location.protocol === 'file:') { + // allow requests from locally loaded sources + url = 'https://' + url; + } else { + url = location.protocol + '//' + url; + } + } this.httpRequest = new XMLHttpRequest(); - this.httpRequest.open("GET", 'http://' + url, true); + this.httpRequest.open("GET", url, true); this.httpRequest.send(null); } else if (this.httpRequest.readyState === 4) { response = this.httpRequest.responseText; @@ -2718,12 +2727,50 @@ Process.prototype.reportTextSplit = function (string, delimiter) { case 'letter': del = ''; break; + case 'csv': + return this.parseCSV(string); default: del = isNil(delimiter) ? '' : delimiter.toString(); } return new List(str.split(del)); }; +Process.prototype.parseCSV = function (string) { + // parse a single row of CSV data into a one-dimensional list + // this assumes that the whole csv data has already been split + // by lines. + // taken from: + // https://stackoverflow.com/questions/8493195/how-can-i-parse-a-csv-string-with-javascript-which-contains-comma-in-data + + var re_valid = /^\s*(?:'[^'\\]*(?:\\[\S\s][^'\\]*)*'|"[^"\\]*(?:\\[\S\s][^"\\]*)*"|[^,'"\s\\]*(?:\s+[^,'"\s\\]+)*)\s*(?:,\s*(?:'[^'\\]*(?:\\[\S\s][^'\\]*)*'|"[^"\\]*(?:\\[\S\s][^"\\]*)*"|[^,'"\s\\]*(?:\s+[^,'"\s\\]+)*)\s*)*$/, + re_value = /(?!\s*$)\s*(?:'([^'\\]*(?:\\[\S\s][^'\\]*)*)'|"([^"\\]*(?:\\[\S\s][^"\\]*)*)"|([^,'"\s\\]*(?:\s+[^,'"\s\\]+)*))\s*(?:,|$)/g, + a = []; + + if (!re_valid.test(string)) { + return new List(); + } + string.replace( + re_value, + function(m0, m1, m2, m3) { + if (m1 !== undefined) { + // remove backslash from \' in single quoted values. + a.push(m1.replace(/\\'/g, "'")); + } else if (m2 !== undefined) { + // remove backslash from \" in double quoted values. + a.push(m2.replace(/\\"/g, '"')); + } else if (m3 !== undefined) { + a.push(m3); + } + return ''; + } + ); + // special case: empty last value. + if (/,\s*$/.test(string)) { + a.push(''); + } + return new List(a); +}; + // Process debugging Process.prototype.alert = function (data) { diff --git a/widgets.js b/widgets.js index 1eb784dd..08c3927b 100644 --- a/widgets.js +++ b/widgets.js @@ -85,7 +85,7 @@ HTMLCanvasElement, fontHeight, SymbolMorph, localize, SpeechBubbleMorph, ArrowMorph, MenuMorph, isString, isNil, SliderMorph, MorphicPreferences, ScrollFrameMorph, MenuItemMorph, Note*/ -modules.widgets = '2017-September-01'; +modules.widgets = '2017-September-08'; var PushButtonMorph; var ToggleButtonMorph; @@ -168,6 +168,7 @@ PushButtonMorph.prototype.init = function ( this.hint = hint || null; this.template = template || null; // for pre-computed backbrounds // if a template is specified, its background images are used as cache + this.enabled = true; // initialize inherited properties: TriggerMorph.uber.init.call(this); @@ -204,9 +205,11 @@ PushButtonMorph.prototype.mouseDownLeft = function () { }; PushButtonMorph.prototype.mouseClickLeft = function () { - PushButtonMorph.uber.mouseClickLeft.call(this); - if (this.label) { - this.label.setCenter(this.center()); + if (this.enabled) { + PushButtonMorph.uber.mouseClickLeft.call(this); + if (this.label) { + this.label.setCenter(this.center()); + } } }; @@ -479,6 +482,22 @@ PushButtonMorph.prototype.createLabel = function () { this.add(this.label); }; +// PushButtonMorph states + +PushButtonMorph.prototype.disable = function () { + this.enabled = false; + this.forAllChildren(function (child) { + child.alpha = 0.3; + }); +}; + +PushButtonMorph.prototype.enable = function () { + this.enabled = true; + this.forAllChildren(function (child) { + child.alpha = 1; + }); +}; + // ToggleButtonMorph /////////////////////////////////////////////////////// /*