diff options
Diffstat (limited to 'gui.js')
| -rw-r--r-- | gui.js | 4991 |
1 files changed, 4991 insertions, 0 deletions
@@ -0,0 +1,4991 @@ +/* + + gui.js + + a programming environment + based on morphic.js, blocks.js, threads.js and objects.js + inspired by Scratch + + written by Jens Mšnig + jens@moenig.org + + Copyright (C) 2013 by Jens Mšnig + + This file is part of Snap!. + + Snap! is free software: you can redistribute it and/or modify + it under the terms of the GNU Affero General Public License as + published by the Free Software Foundation, either version 3 of + the License, or (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with this program. If not, see <http://www.gnu.org/licenses/>. + + + prerequisites: + -------------- + needs blocks.js, threads.js, objects.js and morphic.js + + + toc + --- + the following list shows the order in which all constructors are + defined. Use this list to locate code in this document: + + IDE_Morph + ProjectDialogMorph + SpriteIconMorph + TurtleIconMorph + CostumeIconMorph + WardrobeMorph + + + credits + ------- + Nathan Dinsmore contributed saving and loading of projects, + ypr-Snap! project conversion and countless bugfixes + Ian Reynolds contributed handling and visualization of sounds + +*/ + +/*global modules, Morph, SpriteMorph, BoxMorph, SyntaxElementMorph, Color, +ListWatcherMorph, isString, TextMorph, newCanvas, useBlurredShadows, +radians, VariableFrame, StringMorph, Point, SliderMorph, MenuMorph, +morphicVersion, DialogBoxMorph, ToggleButtonMorph, contains, +ScrollFrameMorph, StageMorph, PushButtonMorph, InputFieldMorph, FrameMorph, +Process, nop, SnapSerializer, ListMorph, detect, AlignmentMorph, TabMorph, +Costume, CostumeEditorMorph, MorphicPreferences, touchScreenSettings, +standardSettings, Sound, BlockMorph, ToggleMorph, InputSlotDialogMorph, +ScriptsMorph, isNil, SymbolMorph, BlockExportDialogMorph, +BlockImportDialogMorph, SnapTranslator, localize, List, InputSlotMorph, +SnapCloud, Uint8Array, HandleMorph, SVG_Costume, fontHeight, hex_sha512, +sb*/ + +// Global stuff //////////////////////////////////////////////////////// + +modules.gui = '2013-March-15'; + +// Declarations + +var IDE_Morph; +var ProjectDialogMorph; +var SpriteIconMorph; +var CostumeIconMorph; +var TurtleIconMorph; +var WardrobeMorph; +var SoundIconMorph; +var JukeboxMorph; + +// IDE_Morph /////////////////////////////////////////////////////////// + +// I am SNAP's top-level frame, the Editor window + +// IDE_Morph inherits from Morph: + +IDE_Morph.prototype = new Morph(); +IDE_Morph.prototype.constructor = IDE_Morph; +IDE_Morph.uber = Morph.prototype; + +// IDE_Morph preferences settings + +IDE_Morph.prototype.buttonContrast = 30; +IDE_Morph.prototype.backgroundColor = new Color(40, 40, 40); +IDE_Morph.prototype.frameColor = SpriteMorph.prototype.paletteColor; +IDE_Morph.prototype.groupColor + = SpriteMorph.prototype.paletteColor.lighter(8); +IDE_Morph.prototype.sliderColor = SpriteMorph.prototype.sliderColor; + +// IDE_Morph instance creation: + +function IDE_Morph(isAutoFill) { + this.init(isAutoFill); +} + +IDE_Morph.prototype.init = function (isAutoFill) { + // global font setting + MorphicPreferences.globalFontFamily = 'Helvetica, Arial'; + + // additional properties: + this.source = 'local'; + this.serializer = new SnapSerializer(); + + this.globalVariables = new VariableFrame(); + this.currentSprite = new SpriteMorph(this.globalVariables); + this.sprites = new List([this.currentSprite]); + this.currentCategory = 'motion'; + this.currentTab = 'scripts'; + this.projectName = ''; + this.projectNotes = ''; + + this.logo = null; + this.controlBar = null; + this.categories = null; + this.palette = null; + this.spriteBar = null; + this.spriteEditor = null; + this.stage = null; + this.corralBar = null; + this.corral = null; + + this.isAutoFill = isAutoFill || true; + this.isAppMode = false; + this.isSmallStage = false; + this.filePicker = null; + this.hasChangedMedia = false; + + this.isAnimating = true; + this.stageRatio = 1; // for IDE animations, e.g. when zooming + + // initialize inherited properties: + IDE_Morph.uber.init.call(this); + + // override inherited properites: + this.color = this.backgroundColor; +}; + +IDE_Morph.prototype.openIn = function (world) { + var hash, usr; + + this.buildPanes(); + world.add(this); + world.userMenu = this.userMenu; + + // get persistent user data, if any + usr = localStorage['-snap-user']; + if (usr) { + usr = SnapCloud.parseResponse(usr)[0]; + if (usr) { + SnapCloud.username = usr.username || null; + SnapCloud.password = usr.password || null; + } + } + + // override SnapCloud's user message with Morphic + SnapCloud.message = function (string) { + var m = new MenuMorph(null, string), + intervalHandle; + m.popUpCenteredInWorld(world); + intervalHandle = setInterval(function () { + m.destroy(); + clearInterval(intervalHandle); + }, 2000); + }; + + // prevent non-DialogBoxMorphs from being dropped + // onto the World in user-mode + world.reactToDropOf = function (morph) { + if (!(morph instanceof DialogBoxMorph)) { + if (world.hand.grabOrigin) { + morph.slideBackTo(world.hand.grabOrigin); + } else { + world.hand.grab(morph); + } + } + }; + + this.reactToWorldResize(world.bounds); + + function getURL(url) { + var request = new XMLHttpRequest(); + request.open('GET', url, false); + request.send(); + if (request.status === 200) { + return request.responseText; + } + throw new Error('unable to retrieve ' + url); + } + + if (location.hash.substr(0, 6) === '#open:') { + hash = location.hash.substr(6); + if (hash.charAt(0) === '%' + || hash.search(/\%(?:[0-9a-f]{2})/i) > -1) { + hash = decodeURIComponent(hash); + } + if (contains( + ['project', 'blocks', 'sprites', 'snapdata'].map( + function (each) {return hash.substr(0, 8).indexOf(each); } + ), + 1 + )) { + this.droppedText(hash); + } else { + this.droppedText(getURL(hash)); + } + } else if (location.hash.substr(0, 5) === '#run:') { + hash = location.hash.substr(5); + if (hash.charAt(0) === '%' + || hash.search(/\%(?:[0-9a-f]{2})/i) > -1) { + hash = decodeURIComponent(hash); + } + if (hash.substr(0, 8) === '<project>') { + this.openProjectString(hash); + } else { +// this.openProjectString(getURL(hash)); + this.droppedText(getURL(hash)); + } + this.toggleAppMode(true); + this.runScripts(); + } else if (location.hash.substr(0, 6) === '#lang:') { + this.setLanguage(location.hash.substr(6)); + this.newProject(); + } +}; + +// IDE_Morph construction + +IDE_Morph.prototype.buildPanes = function () { + this.createLogo(); + this.createControlBar(); + this.createCategories(); + this.createPalette(); + this.createStage(); + this.createSpriteBar(); + this.createSpriteEditor(); + this.createCorralBar(); + this.createCorral(); +}; + +IDE_Morph.prototype.createLogo = function () { + var myself = this; + + if (this.logo) { + this.logo.destroy(); + } + + this.logo = new Morph(); + this.logo.texture = 'snap_logo_sm.gif'; + + this.logo.drawNew = function () { + this.image = newCanvas(this.extent()); + var context = this.image.getContext('2d'), + gradient = context.createLinearGradient( + 0, + 0, + this.width(), + 0 + ); + gradient.addColorStop(0, 'black'); + gradient.addColorStop(0.5, myself.frameColor.toString()); + context.fillStyle = gradient; + context.fillRect(0, 0, this.width(), this.height()); + if (this.texture) { + this.drawTexture(this.texture); + } + }; + + this.logo.drawCachedTexture = function () { + var context = this.image.getContext('2d'); + context.drawImage( + this.cachedTexture, + 5, + Math.round((this.height() - this.cachedTexture.height) / 2) + ); + this.changed(); + }; + + this.logo.mouseClickLeft = function () { + myself.snapMenu(); + }; + + this.logo.color = new Color(); + this.logo.setExtent(new Point(200, 28)); // dimensions are fixed + this.add(this.logo); +}; + +IDE_Morph.prototype.createControlBar = function () { + // assumes the logo has already been created + var padding = 5, + button, + stopButton, + pauseButton, + startButton, + projectButton, + settingsButton, + stageSizeButton, + appModeButton, + cloudButton, + x, + colors = [ + this.groupColor, + this.frameColor.darker(50), + this.frameColor.darker(50) + ], + myself = this; + + if (this.controlBar) { + this.controlBar.destroy(); + } + + this.controlBar = new Morph(); + this.controlBar.color = this.frameColor; + this.controlBar.setHeight(this.logo.height()); // height is fixed + this.controlBar.mouseClickLeft = function () { + this.world().fillPage(); + }; + this.add(this.controlBar); + + //smallStageButton + button = new ToggleButtonMorph( + null, //colors, + myself, // the IDE is the target + 'toggleStageSize', + [ + new SymbolMorph('smallStage', 14), + new SymbolMorph('normalStage', 14) + ], + function () { // query + return myself.isSmallStage; + } + ); + + button.corner = 12; + button.color = colors[0]; + button.highlightColor = colors[1]; + button.pressColor = colors[2]; + button.labelMinExtent = new Point(36, 18); + button.padding = 0; + button.labelShadowOffset = new Point(-1, -1); + button.labelShadowColor = colors[1]; + button.labelColor = new Color(255, 255, 255); + button.contrast = this.buttonContrast; + button.drawNew(); + // button.hint = 'stage size\nsmall & normal'; + button.fixLayout(); + button.refresh(); + this.controlBar.add(stageSizeButton = button); + this.controlBar.stageSizeButton = button; // for refreshing + + //appModeButton + button = new ToggleButtonMorph( + null, //colors, + myself, // the IDE is the target + 'toggleAppMode', + [ + new SymbolMorph('fullScreen', 14), + new SymbolMorph('normalScreen', 14) + ], + function () { // query + return myself.isAppMode; + } + ); + + button.corner = 12; + button.color = colors[0]; + button.highlightColor = colors[1]; + button.pressColor = colors[2]; + button.labelMinExtent = new Point(36, 18); + button.padding = 0; + button.labelShadowOffset = new Point(-1, -1); + button.labelShadowColor = colors[1]; + button.labelColor = new Color(255, 255, 255); + button.contrast = this.buttonContrast; + button.drawNew(); + // button.hint = 'app & edit\nmodes'; + button.fixLayout(); + button.refresh(); + this.controlBar.add(appModeButton = button); + this.controlBar.appModeButton = button; // for refreshing + + // stopButton + button = new PushButtonMorph( + this, + 'stopAllScripts', + new SymbolMorph('octagon', 14) + ); + button.corner = 12; + button.color = colors[0]; + button.highlightColor = colors[1]; + button.pressColor = colors[2]; + button.labelMinExtent = new Point(36, 18); + button.padding = 0; + button.labelShadowOffset = new Point(-1, -1); + button.labelShadowColor = colors[1]; + button.labelColor = new Color(200, 0, 0); + button.contrast = this.buttonContrast; + button.drawNew(); + // button.hint = 'stop\nevery-\nthing'; + button.fixLayout(); + this.controlBar.add(stopButton = button); + + //pauseButton + button = new ToggleButtonMorph( + null, //colors, + myself, // the IDE is the target + 'togglePauseResume', + [ + new SymbolMorph('pause', 12), + new SymbolMorph('pointRight', 14) + ], + function () { // query + return myself.isPaused(); + } + ); + + button.corner = 12; + button.color = colors[0]; + button.highlightColor = colors[1]; + button.pressColor = colors[2]; + button.labelMinExtent = new Point(36, 18); + button.padding = 0; + button.labelShadowOffset = new Point(-1, -1); + button.labelShadowColor = colors[1]; + button.labelColor = new Color(255, 220, 0); + button.contrast = this.buttonContrast; + button.drawNew(); + // button.hint = 'pause/resume\nall scripts'; + button.fixLayout(); + button.refresh(); + this.controlBar.add(pauseButton = button); + this.controlBar.pauseButton = button; // for refreshing + + // startButton + button = new PushButtonMorph( + this, + 'pressStart', + new SymbolMorph('flag', 14) + ); + button.corner = 12; + button.color = colors[0]; + button.highlightColor = colors[1]; + button.pressColor = colors[2]; + button.labelMinExtent = new Point(36, 18); + button.padding = 0; + button.labelShadowOffset = new Point(-1, -1); + button.labelShadowColor = colors[1]; + button.labelColor = new Color(0, 200, 0); + button.contrast = this.buttonContrast; + button.drawNew(); + // button.hint = 'start green\nflag scripts'; + button.fixLayout(); + this.controlBar.add(startButton = button); + this.controlBar.startButton = startButton; + + // projectButton + button = new PushButtonMorph( + this, + 'projectMenu', + new SymbolMorph('file', 14) + //'\u270E' + ); + button.corner = 12; + button.color = colors[0]; + button.highlightColor = colors[1]; + button.pressColor = colors[2]; + button.labelMinExtent = new Point(36, 18); + button.padding = 0; + button.labelShadowOffset = new Point(-1, -1); + button.labelShadowColor = colors[1]; + button.labelColor = new Color(255, 255, 255); + button.contrast = this.buttonContrast; + button.drawNew(); + // button.hint = 'open, save, & annotate project'; + button.fixLayout(); + this.controlBar.add(projectButton = button); + this.controlBar.projectButton = projectButton; // for menu positioning + + // settingsButton + button = new PushButtonMorph( + this, + 'settingsMenu', + new SymbolMorph('gears', 14) + //'\u2699' + ); + button.corner = 12; + button.color = colors[0]; + button.highlightColor = colors[1]; + button.pressColor = colors[2]; + button.labelMinExtent = new Point(36, 18); + button.padding = 0; + button.labelShadowOffset = new Point(-1, -1); + button.labelShadowColor = colors[1]; + button.labelColor = new Color(255, 255, 255); + button.contrast = this.buttonContrast; + button.drawNew(); + // button.hint = 'edit settings'; + button.fixLayout(); + this.controlBar.add(settingsButton = button); + this.controlBar.settingsButton = settingsButton; // for menu positioning + + // cloudButton + button = new PushButtonMorph( + this, + 'cloudMenu', + new SymbolMorph('cloud', 11) + ); + button.corner = 12; + button.color = colors[0]; + button.highlightColor = colors[1]; + button.pressColor = colors[2]; + button.labelMinExtent = new Point(36, 18); + button.padding = 0; + button.labelShadowOffset = new Point(-1, -1); + button.labelShadowColor = colors[1]; + button.labelColor = new Color(255, 255, 255); + button.contrast = this.buttonContrast; + button.drawNew(); + // button.hint = 'cloud operations'; + button.fixLayout(); + this.controlBar.add(cloudButton = button); + this.controlBar.cloudButton = cloudButton; // for menu positioning + + this.controlBar.fixLayout = function () { + x = this.right() - padding; + [stopButton, pauseButton, startButton].forEach( + function (button) { + button.setCenter(myself.controlBar.center()); + button.setRight(x); + x -= button.width(); + x -= padding; + } + ); + + x = myself.right() - (StageMorph.prototype.dimensions.x + * (myself.isSmallStage ? myself.stageRatio : 1)); + + [stageSizeButton, appModeButton].forEach( + function (button) { + x += padding; + button.setCenter(myself.controlBar.center()); + button.setLeft(x); + x += button.width(); + } + ); + + settingsButton.setCenter(myself.controlBar.center()); + settingsButton.setLeft(this.left()); + + cloudButton.setCenter(myself.controlBar.center()); + cloudButton.setRight(settingsButton.left() - padding); + + projectButton.setCenter(myself.controlBar.center()); + projectButton.setRight(cloudButton.left() - padding); + + this.updateLabel(); + }; + + this.controlBar.updateLabel = function () { + var suffix = myself.world().isDevMode ? + ' - ' + localize('development mode') : ''; + + if (this.label) { + this.label.destroy(); + } + if (myself.isAppMode) { + return; + } + + this.label = new StringMorph( + (myself.projectName || localize('untitled')) + suffix, + 14, + 'sans-serif', + true, + false, + false, + new Point(2, 1), + myself.frameColor.darker(myself.buttonContrast) + ); + this.label.color = new Color(255, 255, 255); + this.label.drawNew(); + this.add(this.label); + this.label.setCenter(this.center()); + this.label.setLeft(this.settingsButton.right() + padding); + }; +}; + +IDE_Morph.prototype.createCategories = function () { + // assumes the logo has already been created + var myself = this; + + if (this.categories) { + this.categories.destroy(); + } + + this.categories = new Morph(); + this.categories.color = this.groupColor; + this.categories.silentSetWidth(this.logo.width()); // width is fixed + + function addCategoryButton(category) { + var labelWidth = 75, + colors = [ + myself.frameColor, + myself.frameColor.darker(50), + SpriteMorph.prototype.blockColor[category] + ], + button; + + button = new ToggleButtonMorph( + colors, + myself, // the IDE is the target + function () { + myself.currentCategory = category; + myself.categories.children.forEach(function (each) { + each.refresh(); + }); + myself.refreshPalette(true); + }, + category[0].toUpperCase().concat(category.slice(1)), // label + function () { // query + return myself.currentCategory === category; + }, + null, // env + null, // hint + null, // template cache + labelWidth, // minWidth + true // has preview + ); + + button.corner = 8; + button.padding = 0; + button.labelShadowOffset = new Point(-1, -1); + button.labelShadowColor = colors[1]; + button.labelColor = new Color(255, 255, 255); + button.fixLayout(); + button.refresh(); + myself.categories.add(button); + return button; + } + + function fixCategoriesLayout() { + var buttonWidth = myself.categories.children[0].width(), + buttonHeight = myself.categories.children[0].height(), + border = 3, + rows = Math.ceil((myself.categories.children.length) / 2), + xPadding = (myself.categories.width() + - border + - buttonWidth * 2) / 3, + yPadding = 2, + l = myself.categories.left(), + t = myself.categories.top(), + i = 0, + row, + col; + + myself.categories.children.forEach(function (button) { + i += 1; + row = Math.ceil(i / 2); + col = 2 - (i % 2); + button.setPosition(new Point( + l + (col * xPadding + ((col - 1) * buttonWidth)), + t + (row * yPadding + ((row - 1) * buttonHeight) + border) + )); + }); + + myself.categories.setHeight( + (rows + 1) * yPadding + + rows * buttonHeight + + 2 * border + ); + } + + SpriteMorph.prototype.categories.forEach(function (cat) { + if (!contains(['lists', 'other'], cat)) { + addCategoryButton(cat); + } + }); + fixCategoriesLayout(); + this.add(this.categories); +}; + +IDE_Morph.prototype.createPalette = function () { + // assumes that the logo pane has already been created + // needs the categories pane for layout + var myself = this; + + if (this.palette) { + this.palette.destroy(); + } + + this.palette = this.currentSprite.palette(this.currentCategory); + this.palette.isDraggable = false; + this.palette.acceptsDrops = true; + this.palette.contents.acceptsDrops = false; + + this.palette.reactToDropOf = function (droppedMorph) { + if (droppedMorph instanceof DialogBoxMorph) { + myself.world().add(droppedMorph); + } else if (droppedMorph instanceof SpriteMorph) { + myself.removeSprite(droppedMorph); + } else if (droppedMorph instanceof SpriteIconMorph) { + droppedMorph.destroy(); + myself.removeSprite(droppedMorph.object); + } else if (droppedMorph instanceof CostumeIconMorph) { + myself.currentSprite.wearCostume(null); + droppedMorph.destroy(); + } else { + droppedMorph.destroy(); + } + }; + + this.palette.setWidth(this.logo.width()); + this.add(this.palette); + this.palette.scrollX(this.palette.padding); + this.palette.scrollY(this.palette.padding); +}; + +IDE_Morph.prototype.createStage = function () { + // assumes that the logo pane has already been created + if (this.stage) { + this.stage.destroy(); + } + StageMorph.prototype.frameRate = 0; + this.stage = new StageMorph(this.globalVariables); + this.stage.setExtent(this.stage.dimensions); // dimensions are fixed + if (this.currentSprite instanceof SpriteMorph) { + this.currentSprite.setPosition( + this.stage.center().subtract( + this.currentSprite.extent().divideBy(2) + ) + ); + this.stage.add(this.currentSprite); + } + this.add(this.stage); +}; + +IDE_Morph.prototype.createSpriteBar = function () { + // assumes that the categories pane has already been created + var rotationStyleButtons = [], + thumbSize = new Point(45, 45), + nameField, + padlock, + thumbnail, + tabCorner = 15, + tabColors = [ + this.groupColor.darker(40), + this.groupColor.darker(60), + this.groupColor + ], + tabBar = new AlignmentMorph('row', -tabCorner * 2), + tab, + myself = this; + + if (this.spriteBar) { + this.spriteBar.destroy(); + } + + this.spriteBar = new Morph(); + this.spriteBar.color = this.frameColor; + this.add(this.spriteBar); + + function addRotationStyleButton(rotationStyle) { + var colors = tabColors, + button; + + button = new ToggleButtonMorph( + colors, + myself, // the IDE is the target + function () { + if (myself.currentSprite instanceof SpriteMorph) { + myself.currentSprite.rotationStyle = rotationStyle; + myself.currentSprite.changed(); + myself.currentSprite.drawNew(); + myself.currentSprite.changed(); + } + rotationStyleButtons.forEach(function (each) { + each.refresh(); + }); + }, + ['\u2192', '\u21BB', '\u2194'][rotationStyle], // label + function () { // query + return myself.currentSprite instanceof SpriteMorph + && myself.currentSprite.rotationStyle === rotationStyle; + }, + null, // environment + localize( + [ + 'don\'t rotate', 'can rotate', 'only face left/right' + ][rotationStyle] + ) + ); + + button.corner = 8; + button.labelMinExtent = new Point(11, 11); + button.padding = 0; + button.labelShadowOffset = new Point(-1, -1); + button.labelShadowColor = colors[1]; + button.labelColor = new Color(255, 255, 255); + button.fixLayout(); + button.refresh(); + rotationStyleButtons.push(button); + button.setPosition(myself.spriteBar.position().add(2)); + button.setTop(button.top() + + ((rotationStyleButtons.length - 1) * (button.height() + 2)) + ); + myself.spriteBar.add(button); + if (myself.currentSprite instanceof StageMorph) { + button.hide(); + } + return button; + } + + addRotationStyleButton(1); + addRotationStyleButton(2); + addRotationStyleButton(0); + this.rotationStyleButtons = rotationStyleButtons; + + thumbnail = new Morph(); + thumbnail.setExtent(thumbSize); + thumbnail.image = this.currentSprite.thumbnail(thumbSize); + thumbnail.setPosition( + rotationStyleButtons[0].topRight().add(new Point(5, 3)) + ); + this.spriteBar.add(thumbnail); + + thumbnail.fps = 3; + + thumbnail.step = function () { + if (thumbnail.version !== myself.currentSprite.version) { + thumbnail.image = myself.currentSprite.thumbnail(thumbSize); + thumbnail.changed(); + thumbnail.version = myself.currentSprite.version; + } + }; + + nameField = new InputFieldMorph(this.currentSprite.name); + nameField.setWidth(100); // fixed dimensions + nameField.contrast = 90; + nameField.setPosition(thumbnail.topRight().add(new Point(10, 3))); + this.spriteBar.add(nameField); + nameField.drawNew(); + nameField.accept = function () { + myself.currentSprite.setName(nameField.getValue()); + }; + this.spriteBar.reactToEdit = function () { + myself.currentSprite.setName(nameField.getValue()); + }; + + // padlock + padlock = new ToggleMorph( + 'checkbox', + null, + function () { + myself.currentSprite.isDraggable = + !myself.currentSprite.isDraggable; + }, + localize('draggable'), + function () { + return myself.currentSprite.isDraggable; + } + ); + padlock.label.isBold = false; + padlock.label.setColor(new Color(255, 255, 255)); + padlock.color = tabColors[2]; + padlock.highlightColor = tabColors[0]; + padlock.pressColor = tabColors[1]; + + padlock.tick.shadowOffset = new Point(-1, -1); + padlock.tick.shadowColor = new Color(); // black + padlock.tick.color = new Color(255, 255, 255); + padlock.tick.isBold = false; + padlock.tick.drawNew(); + + padlock.setPosition(nameField.bottomLeft().add(2)); + padlock.drawNew(); + this.spriteBar.add(padlock); + if (this.currentSprite instanceof StageMorph) { + padlock.hide(); + } + + // tab bar + tabBar.tabTo = function (tabString) { + var active; + myself.currentTab = tabString; + this.children.forEach(function (each) { + each.refresh(); + if (each.state) {active = each; } + }); + active.refresh(); // needed when programmatically tabbing + myself.createSpriteEditor(); + myself.fixLayout('tabEditor'); + }; + + tab = new TabMorph( + tabColors, + null, // target + function () {tabBar.tabTo('scripts'); }, + localize('Scripts'), // label + function () { // query + return myself.currentTab === 'scripts'; + } + ); + tab.padding = 3; + tab.corner = tabCorner; + tab.edge = 1; + tab.labelShadowOffset = new Point(-1, -1); + tab.labelShadowColor = tabColors[1]; + tab.labelColor = new Color(255, 255, 255); + tab.drawNew(); + tab.fixLayout(); + tabBar.add(tab); + + tab = new TabMorph( + tabColors, + null, // target + function () {tabBar.tabTo('costumes'); }, + localize('Costumes'), // label + function () { // query + return myself.currentTab === 'costumes'; + } + ); + tab.padding = 3; + tab.corner = tabCorner; + tab.edge = 1; + tab.labelShadowOffset = new Point(-1, -1); + tab.labelShadowColor = tabColors[1]; + tab.labelColor = new Color(255, 255, 255); + tab.drawNew(); + tab.fixLayout(); + tabBar.add(tab); + + tab = new TabMorph( + tabColors, + null, // target + function () {tabBar.tabTo('sounds'); }, + localize('Sounds'), // label + function () { // query + return myself.currentTab === 'sounds'; + } + ); + tab.padding = 3; + tab.corner = tabCorner; + tab.edge = 1; + tab.labelShadowOffset = new Point(-1, -1); + tab.labelShadowColor = tabColors[1]; + tab.labelColor = new Color(255, 255, 255); + tab.drawNew(); + tab.fixLayout(); + tabBar.add(tab); + + tabBar.fixLayout(); + tabBar.children.forEach(function (each) { + each.refresh(); + }); + + this.spriteBar.add(this.spriteBar.tabBar = tabBar); + + this.spriteBar.fixLayout = function () { + this.tabBar.setLeft(this.left()); + this.tabBar.setBottom(this.bottom()); + }; +}; + +IDE_Morph.prototype.createSpriteEditor = function () { + // assumes that the logo pane and the stage have already been created + var scripts = this.currentSprite.scripts, + myself = this; + + if (this.spriteEditor) { + this.spriteEditor.destroy(); + } + + if (this.currentTab === 'scripts') { + scripts.isDraggable = false; + scripts.color = this.groupColor; + scripts.texture = 'scriptsPaneTexture.gif'; + + this.spriteEditor = new ScrollFrameMorph( + scripts, + null, + this.sliderColor + ); + this.spriteEditor.padding = 10; + this.spriteEditor.growth = 50; + this.spriteEditor.isDraggable = false; + this.spriteEditor.acceptsDrops = false; + this.spriteEditor.contents.acceptsDrops = true; + + scripts.scrollFrame = this.spriteEditor; + this.add(this.spriteEditor); + this.spriteEditor.scrollX(this.spriteEditor.padding); + this.spriteEditor.scrollY(this.spriteEditor.padding); + } else if (this.currentTab === 'costumes') { + this.spriteEditor = new WardrobeMorph( + this.currentSprite, + this.sliderColor + ); + this.spriteEditor.color = this.groupColor; + this.add(this.spriteEditor); + this.spriteEditor.updateSelection(); + + this.spriteEditor.acceptsDrops = false; + this.spriteEditor.contents.acceptsDrops = false; + } else if (this.currentTab === 'sounds') { + this.spriteEditor = new JukeboxMorph( + this.currentSprite, + this.sliderColor + ); + this.spriteEditor.color = this.groupColor; + this.add(this.spriteEditor); + this.spriteEditor.updateSelection(); + this.spriteEditor.acceptDrops = false; + this.spriteEditor.contents.acceptsDrops = false; + } else { + this.spriteEditor = new Morph(); + this.spriteEditor.color = this.groupColor; + this.spriteEditor.acceptsDrops = true; + this.spriteEditor.reactToDropOf = function (droppedMorph) { + if (droppedMorph instanceof DialogBoxMorph) { + myself.world().add(droppedMorph); + } else if (droppedMorph instanceof SpriteMorph) { + myself.removeSprite(droppedMorph); + } else { + droppedMorph.destroy(); + } + }; + this.add(this.spriteEditor); + } +}; + +IDE_Morph.prototype.createCorralBar = function () { + // assumes the stage has already been created + var padding = 5, + button, + colors = [ + this.groupColor, + this.frameColor.darker(50), + this.frameColor.darker(50) + ]; + + if (this.corralBar) { + this.corralBar.destroy(); + } + + this.corralBar = new Morph(); + this.corralBar.color = this.frameColor; + this.corralBar.setHeight(this.logo.height()); // height is fixed + this.add(this.corralBar); + + // new sprite button + button = new PushButtonMorph( + this, + 'addNewSprite', + new SymbolMorph('turtle', 14) + ); + button.corner = 12; + button.color = colors[0]; + button.highlightColor = colors[1]; + button.pressColor = colors[2]; + button.labelMinExtent = new Point(36, 18); + button.padding = 0; + button.labelShadowOffset = new Point(-1, -1); + button.labelShadowColor = colors[1]; + button.labelColor = new Color(255, 255, 255); + button.contrast = this.buttonContrast; + button.drawNew(); + button.hint = 'add a new Sprite'; + button.fixLayout(); + button.setCenter(this.corralBar.center()); + button.setLeft(this.corralBar.left() + padding); + this.corralBar.add(button); + +}; + +IDE_Morph.prototype.createCorral = function () { + // assumes the corral bar has already been created + var frame, template, padding = 5, myself = this; + + if (this.corral) { + this.corral.destroy(); + } + + this.corral = new Morph(); + this.corral.color = this.groupColor; + this.add(this.corral); + + this.corral.stageIcon = new SpriteIconMorph(this.stage); + this.corral.stageIcon.isDraggable = false; + this.corral.add(this.corral.stageIcon); + + frame = new ScrollFrameMorph(null, null, this.sliderColor); + frame.acceptsDrops = false; + frame.contents.acceptsDrops = false; + + frame.contents.wantsDropOf = function (morph) { + return morph instanceof SpriteIconMorph; + }; + + frame.contents.reactToDropOf = function (spriteIcon) { + myself.corral.reactToDropOf(spriteIcon); + }; + + frame.alpha = 0; + + this.sprites.asArray().forEach(function (morph) { + frame.contents.add( + template = new SpriteIconMorph(morph, template) + ); + }); + + this.corral.frame = frame; + this.corral.add(frame); + + this.corral.fixLayout = function () { + this.stageIcon.setCenter(this.center()); + this.stageIcon.setLeft(this.left() + padding); + this.frame.setLeft(this.stageIcon.right() + padding); + this.frame.setExtent(new Point( + this.right() - this.frame.left(), + this.height() + )); + this.arrangeIcons(); + this.refresh(); + }; + + this.corral.arrangeIcons = function () { + var x = this.frame.left(), + y = this.frame.top(), + max = this.frame.right(), + start = this.frame.left(); + + this.frame.contents.children.forEach(function (icon) { + var w = icon.width(); + + if (x + w > max) { + x = start; + y += icon.height(); // they're all the same + } + icon.setPosition(new Point(x, y)); + x += w; + }); + this.frame.contents.adjustBounds(); + }; + + this.corral.addSprite = function (sprite) { + this.frame.contents.add(new SpriteIconMorph(sprite)); + this.fixLayout(); + }; + + this.corral.refresh = function () { + this.stageIcon.refresh(); + this.frame.contents.children.forEach(function (icon) { + icon.refresh(); + }); + }; + + this.corral.wantsDropOf = function (morph) { + return morph instanceof SpriteIconMorph; + }; + + this.corral.reactToDropOf = function (spriteIcon) { + var idx = 1, + pos = spriteIcon.position(); + spriteIcon.destroy(); + this.frame.contents.children.forEach(function (icon) { + if (pos.gt(icon.position()) || pos.y > icon.bottom()) { + idx += 1; + } + }); + myself.sprites.add(spriteIcon.object, idx); + myself.createCorral(); + myself.fixLayout(); + }; +}; + +// IDE_Morph layout + +IDE_Morph.prototype.fixLayout = function (situation) { + // situation is a string, i.e. + // 'selectSprite' or 'refreshPalette' or 'tabEditor' + var padding = 5; + + Morph.prototype.trackChanges = false; + + if (situation !== 'refreshPalette') { + // controlBar + this.controlBar.setPosition(this.logo.topRight()); + this.controlBar.setWidth(this.right() - this.controlBar.left()); + this.controlBar.fixLayout(); + + // categories + this.categories.setLeft(this.logo.left()); + this.categories.setTop(this.logo.bottom()); + } + + // palette + this.palette.setLeft(this.logo.left()); + this.palette.setTop(this.categories.bottom()); + this.palette.setHeight(this.bottom() - this.palette.top()); + + if (situation !== 'refreshPalette') { + // stage + if (this.isAppMode) { + this.stage.setScale(Math.floor(Math.min( + (this.width() - padding * 2) / this.stage.dimensions.x, + (this.height() - this.controlBar.height() * 2 - padding * 2) + / this.stage.dimensions.y + ) * 10) / 10); + this.stage.setCenter(this.center()); + } else { +// this.stage.setScale(this.isSmallStage ? 0.5 : 1); + this.stage.setScale(this.isSmallStage ? this.stageRatio : 1); + this.stage.setTop(this.logo.bottom() + padding); + this.stage.setRight(this.right()); + } + + // spriteBar + this.spriteBar.setPosition(this.logo.bottomRight().add(padding)); + this.spriteBar.setExtent(new Point( + Math.max(0, this.stage.left() - padding - this.spriteBar.left()), + this.categories.bottom() - this.spriteBar.top() - padding + )); + this.spriteBar.fixLayout(); + + // spriteEditor + if (this.spriteEditor.isVisible) { + this.spriteEditor.setPosition(this.spriteBar.bottomLeft()); + this.spriteEditor.setExtent(new Point( + this.spriteBar.width(), + this.bottom() - this.spriteEditor.top() + )); + } + + // corralBar + this.corralBar.setLeft(this.stage.left()); + this.corralBar.setTop(this.stage.bottom() + padding); + this.corralBar.setWidth(this.stage.width()); + + // corral + if (!contains(['selectSprite', 'tabEditor'], situation)) { + this.corral.setPosition(this.corralBar.bottomLeft()); + this.corral.setWidth(this.stage.width()); + this.corral.setHeight(this.bottom() - this.corral.top()); + this.corral.fixLayout(); + } + } + + Morph.prototype.trackChanges = true; + this.changed(); +}; + +IDE_Morph.prototype.setProjectName = function (string) { + this.projectName = string; + this.controlBar.updateLabel(); +}; + +// IDE_Morph resizing + +IDE_Morph.prototype.setExtent = function (point) { + var minExt, + ext; + + // determine the minimum dimensions making sense for the current mode + if (this.isAppMode) { + minExt = StageMorph.prototype.dimensions.add( + this.controlBar.height() + 10 + ); + } else { + /* // auto-switches to small stage mode, commented out b/c I don't like it + if (point.x < 910) { + this.isSmallStage = true; + this.stageRatio = 0.5; + } + */ + minExt = this.isSmallStage ? + new Point(700, 350) : new Point(910, 490); + } + ext = point.max(minExt); + IDE_Morph.uber.setExtent.call(this, ext); + this.fixLayout(); +}; + +// IDE_Morph events + +IDE_Morph.prototype.reactToWorldResize = function (rect) { + if (this.isAutoFill) { + this.setPosition(rect.origin); + this.setExtent(rect.extent()); + } + if (this.filePicker) { + document.body.removeChild(this.filePicker); + this.filePicker = null; + } +}; + +IDE_Morph.prototype.droppedImage = function (aCanvas, name) { + var costume = new Costume(aCanvas, name.split('.')[0]); // up to period + this.currentSprite.addCostume(costume); + this.currentSprite.wearCostume(costume); + this.spriteBar.tabBar.tabTo('costumes'); + this.hasChangedMedia = true; +}; + +IDE_Morph.prototype.droppedSVG = function (anImage, name) { + var costume = new SVG_Costume(anImage, name.split('.')[0]); + this.currentSprite.addCostume(costume); + this.currentSprite.wearCostume(costume); + this.spriteBar.tabBar.tabTo('costumes'); + this.hasChangedMedia = true; + this.showMessage( + 'SVG costumes are\nnot yet fully supported\nin every browser', + 2 + ); +}; + +IDE_Morph.prototype.droppedAudio = function (anAudio, name) { + this.currentSprite.addSound(anAudio, name.split('.')[0]); // up to period + this.spriteBar.tabBar.tabTo('sounds'); + this.hasChangedMedia = true; +}; + +IDE_Morph.prototype.droppedText = function (aString, name) { + var lbl = name ? name.split('.')[0] : ''; + if (aString.indexOf('<project') === 0) { + return this.openProjectString(aString); + } + if (aString.indexOf('<snapdata') === 0) { + return this.openCloudDataString(aString); + } + if (aString.indexOf('<blocks') === 0) { + return this.openBlocksString(aString, lbl, true); + } + if (aString.indexOf('<sprites') === 0) { + return this.openSpritesString(aString); + } + if (aString.indexOf('<media') === 0) { + return this.openMediaString(aString); + } +}; + +IDE_Morph.prototype.droppedBinary = function (anArrayBuffer, name) { + // dynamically load ypr->Snap! + var ypr = document.getElementById('ypr'), + myself = this, + suffix = name.substring(name.length - 3); + + if (suffix.toLowerCase() !== 'ypr') {return; } + + function loadYPR(buffer, lbl) { + var reader = new sb.Reader(), + pname = lbl.split('.')[0]; // up to period + reader.onload = function (info) { + myself.droppedText(new sb.XMLWriter().write(pname, info)); + }; + reader.readYPR(new Uint8Array(buffer)); + } + + if (!ypr) { + ypr = document.createElement('script'); + ypr.id = 'ypr'; + ypr.onload = function () {loadYPR(anArrayBuffer, name); }; + document.head.appendChild(ypr); + ypr.src = 'ypr.js'; + } else { + loadYPR(anArrayBuffer, name); + } +}; + +// IDE_Morph button actions + +IDE_Morph.prototype.refreshPalette = function (shouldIgnorePosition) { + var oldTop = this.palette.contents.top(); + + this.createPalette(); + this.fixLayout('refreshPalette'); + if (!shouldIgnorePosition) { + this.palette.contents.setTop(oldTop); + } +}; + +IDE_Morph.prototype.pressStart = function () { + if (this.world().currentKey === 16) { // shiftClicked + this.toggleFastTracking(); + } else { + this.runScripts(); + } +}; + +IDE_Morph.prototype.toggleFastTracking = function () { + if (this.stage.isFastTracked) { + this.stopFastTracking(); + } else { + this.startFastTracking(); + } +}; + +IDE_Morph.prototype.toggleVariableFrameRate = function () { + if (StageMorph.prototype.frameRate) { + StageMorph.prototype.frameRate = 0; + this.stage.fps = 0; + } else { + StageMorph.prototype.frameRate = 30; + this.stage.fps = 30; + } +}; + +IDE_Morph.prototype.startFastTracking = function () { + this.stage.isFastTracked = true; + this.stage.fps = 0; + this.controlBar.startButton.labelString = new SymbolMorph('flash', 14); + this.controlBar.startButton.drawNew(); + this.controlBar.startButton.fixLayout(); +}; + +IDE_Morph.prototype.stopFastTracking = function () { + this.stage.isFastTracked = false; + this.stage.fps = this.stage.frameRate; + this.controlBar.startButton.labelString = new SymbolMorph('flag', 14); + this.controlBar.startButton.drawNew(); + this.controlBar.startButton.fixLayout(); +}; + +IDE_Morph.prototype.runScripts = function () { + this.stage.fireGreenFlagEvent(); +}; + +IDE_Morph.prototype.togglePauseResume = function () { + if (this.stage.threads.isPaused()) { + this.stage.threads.resumeAll(this.stage); + } else { + this.stage.threads.pauseAll(this.stage); + } + this.controlBar.pauseButton.refresh(); +}; + +IDE_Morph.prototype.isPaused = function () { + if (!this.stage) {return false; } + return this.stage.threads.isPaused(); +}; + +IDE_Morph.prototype.stopAllScripts = function () { + this.stage.fireStopAllEvent(); +}; + +IDE_Morph.prototype.selectSprite = function (sprite) { + this.currentSprite = sprite; + this.createPalette(); + this.createSpriteBar(); + this.createSpriteEditor(); + this.corral.refresh(); + this.fixLayout('selectSprite'); + this.currentSprite.scripts.fixMultiArgs(); +}; + +// IDE_Morph sprite list access + +IDE_Morph.prototype.addNewSprite = function () { + var sprite = new SpriteMorph(this.globalVariables), + rnd = Process.prototype.reportRandom; + + sprite.name = sprite.name + + (this.corral.frame.contents.children.length + 1); + sprite.setCenter(this.stage.center()); + this.stage.add(sprite); + + // randomize sprite properties + sprite.setHue(rnd.call(this, 0, 100)); + sprite.setBrightness(rnd.call(this, 50, 100)); + sprite.turn(rnd.call(this, 1, 360)); + sprite.setXPosition(rnd.call(this, -220, 220)); + sprite.setYPosition(rnd.call(this, -160, 160)); + + this.sprites.add(sprite); + this.corral.addSprite(sprite); + this.selectSprite(sprite); +}; + +IDE_Morph.prototype.duplicateSprite = function (sprite) { + var duplicate = sprite.fullCopy(); + + duplicate.name = sprite.name + '(2)'; + duplicate.setPosition(this.world().hand.position()); + this.stage.add(duplicate); + duplicate.keepWithin(this.stage); + this.sprites.add(duplicate); + this.corral.addSprite(duplicate); + this.selectSprite(duplicate); +}; + +IDE_Morph.prototype.removeSprite = function (sprite) { + var idx = this.sprites.asArray().indexOf(sprite) + 1; + + sprite.destroy(); + this.stage.watchers().forEach(function (watcher) { + if (watcher.object() === sprite) { + watcher.destroy(); + } + }); + + if (idx < 1) {return; } + + this.currentSprite = detect( + this.stage.children, + function (morph) {return morph instanceof SpriteMorph; } + ) || this.stage; + this.sprites.remove(this.sprites.asArray().indexOf(sprite) + 1); + this.createCorral(); + this.fixLayout(); + this.selectSprite(this.currentSprite); +}; + +// IDE_Morph menus + +IDE_Morph.prototype.userMenu = function () { + var menu = new MenuMorph(this); + menu.addItem('help', 'nop'); + return menu; +}; + +IDE_Morph.prototype.snapMenu = function () { + var menu, + world = this.world(); + + menu = new MenuMorph(this); + menu.addItem('About...', 'aboutSnap'); + menu.addLine(); + menu.addItem( + 'Reference manual', + function () { + window.open('help/SnapManual.pdf', 'SnapReferenceManual'); + } + ); + menu.addItem( + 'Snap! website', + function () { + window.open('http://snap.berkeley.edu/', 'SnapWebsite'); + } + ); + menu.addItem( + 'Download source', + function () { + window.open( + 'http://snap.berkeley.edu/snapsource/snap.zip', + 'SnapSource' + ); + } + ); + if (world.isDevMode) { + menu.addLine(); + menu.addItem( + 'Switch back to user mode', + 'switchToUserMode', + 'disable deep-Morphic\ncontext menus' + + '\nand show user-friendly ones', + new Color(0, 100, 0) + ); + } else if (world.currentKey === 16) { // shift-click + menu.addLine(); + menu.addItem( + 'Switch to dev mode', + 'switchToDevMode', + 'enable Morphic\ncontext menus\nand inspectors,' + + '\nnot user-friendly!', + new Color(100, 0, 0) + ); + } + menu.popup(world, this.logo.bottomLeft()); +}; + +IDE_Morph.prototype.cloudMenu = function () { + var menu, + myself = this, + world = this.world(), + pos = this.controlBar.cloudButton.bottomLeft(), + shiftClicked = (world.currentKey === 16); + + menu = new MenuMorph(this); + if (shiftClicked) { + menu.addItem( + 'url...', + 'setCloudURL', + null, + new Color(100, 0, 0) + ); + menu.addLine(); + } + if (!SnapCloud.username) { + menu.addItem( + 'Login...', + 'initializeCloud' + ); + menu.addItem( + 'Signup...', + 'createCloudAccount' + ); + } else { + menu.addItem( + 'Logout', + 'logout' + ); + menu.addItem( + 'Change Password...', + 'changeCloudPassword' + ); + } + if (shiftClicked) { + menu.addLine(); + menu.addItem( + 'export project media only...', + function () { + if (myself.projectName) { + myself.exportProjectMedia(myself.projectName); + } else { + myself.prompt('Export Project As...', function (name) { + myself.exportProjectMedia(name); + }); + } + }, + null, + this.hasChangedMedia ? new Color(100, 0, 0) : new Color(0, 100, 0) + ); + menu.addItem( + 'export project without media...', + function () { + if (myself.projectName) { + myself.exportProjectNoMedia(myself.projectName); + } else { + myself.prompt('Export Project As...', function (name) { + myself.exportProjectNoMedia(name); + }); + } + }, + null, + new Color(100, 0, 0) + ); + menu.addLine(); + menu.addItem( + 'export project as cloud data...', + function () { + if (myself.projectName) { + myself.exportProjectAsCloudData(myself.projectName); + } else { + myself.prompt('Export Project As...', function (name) { + myself.exportProjectAsCloudData(name); + }); + } + }, + null, + new Color(100, 0, 0) + ); + } + menu.popup(world, pos); +}; + +IDE_Morph.prototype.settingsMenu = function () { + var menu, + stage = this.stage, + world = this.world(), + myself = this, + pos = this.controlBar.settingsButton.bottomLeft(), + shiftClicked = (world.currentKey === 16); + + function addPreference(label, toggle, test, onHint, offHint, hide) { + var on = '\u2611 ', + off = '\u2610 '; + if (!hide || shiftClicked) { + menu.addItem( + (test ? on : off) + localize(label), + toggle, + test ? onHint : offHint, + hide ? new Color(100, 0, 0) : null + ); + } + } + + menu = new MenuMorph(this); + menu.addItem('Language...', 'languageMenu'); + menu.addLine(); + addPreference( + 'Blurred shadows', + 'toggleBlurredShadows', + useBlurredShadows, + 'uncheck to use solid drop\nshadows and highlights', + 'check to use blurred drop\nshadows and highlights', + true + ); + addPreference( + 'Zebra coloring', + 'toggleZebraColoring', + BlockMorph.prototype.zebraContrast, + 'uncheck to disable alternating\ncolors for nested block', + 'check to enable alternating\ncolors for nested blocks', + true + ); + addPreference( + 'Dynamic input labels', + 'toggleDynamicInputLabels', + SyntaxElementMorph.prototype.dynamicInputLabels, + 'uncheck to disable dynamic\nlabels for variadic inputs', + 'check to enable dynamic\nlabels for variadic inputs', + true + ); + addPreference( + 'Prefer empty slot drops', + 'togglePreferEmptySlotDrops', + ScriptsMorph.prototype.isPreferringEmptySlots, + 'uncheck to allow dropped\nreporters to kick out others', + 'settings menu prefer empty slots hint', + true + ); + addPreference( + 'Long form input dialog', + 'toggleLongFormInputDialog', + InputSlotDialogMorph.prototype.isLaunchingExpanded, + 'uncheck to use the input\ndialog in short form', + 'check to always show slot\ntypes in the input dialog' + ); + addPreference( + 'Virtual keyboard', + 'toggleVirtualKeyboard', + MorphicPreferences.useVirtualKeyboard, + 'uncheck to disable\nvirtual keyboard support\nfor mobile devices', + 'check to enable\nvirtual keyboard support\nfor mobile devices', + true + ); + addPreference( + 'Input sliders', + 'toggleInputSliders', + MorphicPreferences.useSliderForInput, + 'uncheck to disable\ninput sliders for\nentry fields', + 'check to enable\ninput sliders for\nentry fields' + ); + if (MorphicPreferences.useSliderForInput) { + addPreference( + 'Execute on slider change', + 'toggleSliderExecute', + InputSlotMorph.prototype.executeOnSliderEdit, + 'uncheck to supress\nrunning scripts\nwhen moving the slider', + 'check to run\nthe edited script\nwhen moving the slider' + ); + } + addPreference( + 'Clicking sound', + function () {BlockMorph.prototype.toggleSnapSound(); }, + BlockMorph.prototype.snapSound, + 'uncheck to turn\nblock clicking\nsound off', + 'check to turn\nblock clicking\nsound on' + ); + addPreference( + 'Animations', + function () {myself.isAnimating = !myself.isAnimating; }, + myself.isAnimating, + 'uncheck to disable\nIDE animations', + 'check to enable\nIDE animations', + true + ); + addPreference( + 'Turbo mode', + 'toggleFastTracking', + this.stage.isFastTracked, + 'uncheck to run scripts\nat normal speed', + 'check to prioritize\nscript execution' + ); + addPreference( + 'Rasterize SVGs', + function () { + MorphicPreferences.rasterizeSVGs = + !MorphicPreferences.rasterizeSVGs; + }, + MorphicPreferences.rasterizeSVGs, + 'uncheck for smooth\nscaling of vector costumes', + 'check to rasterize\nSVGs on import', + true + ); + menu.addLine(); // everything below this line is made persistent + addPreference( + 'Thread safe scripts', + function () {stage.isThreadSafe = !stage.isThreadSafe; }, + this.stage.isThreadSafe, + 'uncheck to allow\nscript reentrance', + 'check to disallow\nscript reentrance' + ); + addPreference( + 'Prefer smooth animations', + 'toggleVariableFrameRate', + StageMorph.prototype.frameRate, + 'uncheck for greater speed\nat variable frame rates', + 'check for smooth, predictable\nanimations across computers' + ); + menu.popup(world, pos); +}; + +IDE_Morph.prototype.projectMenu = function () { + var menu, + myself = this, + world = this.world(), + pos = this.controlBar.projectButton.bottomLeft(), + shiftClicked = (world.currentKey === 16); + + menu = new MenuMorph(this); + menu.addItem('Project Notes...', 'editProjectNotes'); + menu.addLine(); + menu.addItem( + 'New', + function () { + myself.confirm( + 'Replace the current project with a new one?', + 'New Project', + function () { + myself.newProject(); + } + ); + } + ); + menu.addItem('Open...', 'openProjectsBrowser'); + menu.addItem( + 'Save', + function () { + if (myself.projectName) { + if (myself.source === 'local') { // as well as 'examples' + myself.saveProject(myself.projectName); + } else { // 'cloud' + myself.saveProjectToCloud(myself.projectName); + } + } else { + myself.saveProjectsBrowser(); + } + } + ); + if (shiftClicked) { + menu.addItem( + 'Save to disk', + 'saveProjectToDisk', + 'experimental - store this project\nin your downloads folder', + new Color(100, 0, 0) + ); + } + menu.addItem('Save As...', 'saveProjectsBrowser'); + menu.addLine(); + menu.addItem( + 'Import...', + function () { + var inp = document.createElement('input'); + if (myself.filePicker) { + document.body.removeChild(myself.filePicker); + myself.filePicker = null; + } + inp.type = 'file'; + inp.style.color = "transparent"; + inp.style.backgroundColor = "transparent"; + inp.style.border = "none"; + inp.style.outline = "none"; + inp.style.position = "absolute"; + inp.style.top = "0px"; + inp.style.left = "0px"; + inp.style.width = "0px"; + inp.style.height = "0px"; + inp.addEventListener( + "change", + function () { + document.body.removeChild(inp); + myself.filePicker = null; + world.hand.processDrop(inp.files); + }, + false + ); + document.body.appendChild(inp); + myself.filePicker = inp; + inp.click(); + }, + 'file menu import hint' // looks up the actual text in the translator + ); + + menu.addItem( + shiftClicked ? + 'Export project as plain text ...' : 'Export project...', + function () { + if (myself.projectName) { + myself.exportProject(myself.projectName, shiftClicked); + } else { + myself.prompt('Export Project As...', function (name) { + myself.exportProject(name); + }); + } + }, + 'show project data as XML\nin a new browser window', + shiftClicked ? new Color(100, 0, 0) : null + ); + + menu.addItem( + 'Export blocks ...', + function () {myself.exportGlobalBlocks(); }, + 'show global custom block definitions as XML\nin a new browser window' + ); + + menu.addLine(); + menu.addItem( + 'Import tools...', + function () { + + var url = 'http://snap.berkeley.edu/snapsource/tools.xml', + request = new XMLHttpRequest(); + request.open('GET', url, false); + request.send(); + if (request.status === 200) { + return myself.droppedText(request.responseText, 'tools'); + } + throw new Error('unable to retrieve ' + url); + + }, + 'load the official library of\npowerful blocks' + ); + + menu.popup(world, pos); +}; + +// IDE_Morph menu actions + +IDE_Morph.prototype.aboutSnap = function () { + var dlg, aboutTxt, noticeTxt, creditsTxt, versions = '', translations, + module, btn1, btn2, btn3, btn4, licenseBtn, translatorsBtn, + world = this.world(); + + aboutTxt = 'Snap! 4.0\nBuild Your Own Blocks\n\n--- beta ---\n\n' + + 'Copyright \u24B8 2013 Jens M\u00F6nig and ' + + 'Brian Harvey\n' + + 'jens@moenig.org, bh@cs.berkeley.edu\n\n' + + + 'Snap! is developed by the University of California, Berkeley\n' + + ' with support from the National Science Foundation ' + + 'and MioSoft. \n' + + + 'The design of Snap! is influenced and inspired by Scratch,\n' + + 'from the Lifelong Kindergarten group at the MIT Media Lab\n\n' + + + 'for more information see http://snap.berkeley.edu\n' + + 'and http://scratch.mit.edu'; + + noticeTxt = localize('License') + + '\n\n' + + 'Snap! is free software: you can redistribute it and/or modify\n' + + 'it under the terms of the GNU Affero General Public License as\n' + + 'published by the Free Software Foundation, either version 3 of\n' + + 'the License, or (at your option) any later version.\n\n' + + + 'This program is distributed in the hope that it will be useful,\n' + + 'but WITHOUT ANY WARRANTY; without even the implied warranty of\n' + + 'MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n' + + 'GNU Affero General Public License for more details.\n\n' + + + 'You should have received a copy of the\n' + + 'GNU Affero General Public License along with this program.\n' + + 'If not, see http://www.gnu.org/licenses/'; + + creditsTxt = localize('Contributors') + + '\n\nNathan Dinsmore: Saving/Loading, Snap-Logo Design, ' + + 'countless bugfixes' + + '\nIan Reynolds: UI Design, Event Bindings, ' + + 'Sound primitives' + + '\nIvan Motyashov: Initial Squeak Porting' + + '\nDavide Della Casa: Morphic Optimizations' + + '\nAchal Dave: Web Audio' + + '\nJoe Otto: Morphic Testing and Debugging'; + + for (module in modules) { + if (modules.hasOwnProperty(module)) { + versions += ('\n' + module + ' (' + + modules[module] + ')'); + } + } + if (versions !== '') { + versions = localize('current module versions:') + ' \n\n' + + 'morphic (' + morphicVersion + ')' + + versions; + } + translations = localize('Translations') + '\n' + SnapTranslator.credits(); + + dlg = new DialogBoxMorph(); + dlg.inform('About Snap', aboutTxt, world); + btn1 = dlg.buttons.children[0]; + translatorsBtn = dlg.addButton( + function () { + dlg.body.text = translations; + dlg.body.drawNew(); + btn1.show(); + btn2.show(); + btn3.hide(); + btn4.hide(); + licenseBtn.hide(); + translatorsBtn.hide(); + dlg.fixLayout(); + dlg.drawNew(); + dlg.setCenter(world.center()); + }, + 'Translators...' + ); + btn2 = dlg.addButton( + function () { + dlg.body.text = aboutTxt; + dlg.body.drawNew(); + btn1.show(); + btn2.hide(); + btn3.show(); + btn4.show(); + licenseBtn.show(); + translatorsBtn.hide(); + dlg.fixLayout(); + dlg.drawNew(); + dlg.setCenter(world.center()); + }, + 'Back...' + ); + btn2.hide(); + licenseBtn = dlg.addButton( + function () { + dlg.body.text = noticeTxt; + dlg.body.drawNew(); + btn1.show(); + btn2.show(); + btn3.hide(); + btn4.hide(); + licenseBtn.hide(); + translatorsBtn.hide(); + dlg.fixLayout(); + dlg.drawNew(); + dlg.setCenter(world.center()); + }, + 'License...' + ); + btn3 = dlg.addButton( + function () { + dlg.body.text = versions; + dlg.body.drawNew(); + btn1.show(); + btn2.show(); + btn3.hide(); + btn4.hide(); + licenseBtn.hide(); + translatorsBtn.hide(); + dlg.fixLayout(); + dlg.drawNew(); + dlg.setCenter(world.center()); + }, + 'Modules...' + ); + btn4 = dlg.addButton( + function () { + dlg.body.text = creditsTxt; + dlg.body.drawNew(); + btn1.show(); + btn2.show(); + translatorsBtn.show(); + btn3.hide(); + btn4.hide(); + licenseBtn.hide(); + dlg.fixLayout(); + dlg.drawNew(); + dlg.setCenter(world.center()); + }, + 'Credits...' + ); + translatorsBtn.hide(); + dlg.fixLayout(); + dlg.drawNew(); +}; + +IDE_Morph.prototype.editProjectNotes = function () { + var dialog = new DialogBoxMorph(), + frame = new ScrollFrameMorph(), + text = new TextMorph(this.projectNotes || ''), + ok = dialog.ok, + myself = this, + size = 250, + world = this.world(); + + frame.padding = 6; + frame.setWidth(size); + frame.acceptsDrops = false; + frame.contents.acceptsDrops = false; + + text.setWidth(size - frame.padding * 2); + text.setPosition(frame.topLeft().add(frame.padding)); + text.enableSelecting(); + text.isEditable = true; + + frame.setHeight(size); + frame.fixLayout = nop; + frame.edge = InputFieldMorph.prototype.edge; + frame.fontSize = InputFieldMorph.prototype.fontSize; + frame.typeInPadding = InputFieldMorph.prototype.typeInPadding; + frame.contrast = InputFieldMorph.prototype.contrast; + frame.drawNew = InputFieldMorph.prototype.drawNew; + frame.drawRectBorder = InputFieldMorph.prototype.drawRectBorder; + + frame.addContents(text); + text.drawNew(); + + dialog.ok = function () { + myself.projectNotes = text.text; + ok.call(this); + }; + + dialog.justDropped = function () { + text.edit(); + }; + + dialog.labelString = 'Project Notes'; + dialog.createLabel(); + dialog.addBody(frame); + frame.drawNew(); + dialog.addButton('ok', 'OK'); + dialog.addButton('cancel', 'Cancel'); + dialog.fixLayout(); + dialog.drawNew(); + world.add(dialog); + dialog.setCenter(world.center()); + text.edit(); +}; + +IDE_Morph.prototype.newProject = function () { + this.source = SnapCloud.username ? 'cloud' : 'local'; + if (this.stage) { + this.stage.destroy(); + } + location.hash = ''; + this.globalVariables = new VariableFrame(); + this.currentSprite = new SpriteMorph(this.globalVariables); + this.sprites = new List([this.currentSprite]); + this.setProjectName(''); + this.projectNotes = ''; + this.createStage(); + this.add(this.stage); + this.createCorral(); + this.selectSprite(this.stage.children[0]); + this.fixLayout(); +}; + +IDE_Morph.prototype.saveProject = function (name) { + var myself = this; + this.nextSteps([ + function () { + myself.showMessage('Saving...'); + }, + function () { + myself.rawSaveProject(name); + } + ]); +}; + +IDE_Morph.prototype.rawSaveProject = function (name) { + var str; + if (name) { + this.setProjectName(name); + if (Process.prototype.isCatchingErrors) { + try { + localStorage['-snap-project-' + name] + = str = this.serializer.serialize(this.stage); + location.hash = '#open:' + str; + this.showMessage('Saved!', 1); + } catch (err) { + this.showMessage('Save failed: ' + err); + } + } else { + localStorage['-snap-project-' + name] + = str = this.serializer.serialize(this.stage); + location.hash = '#open:' + str; + this.showMessage('Saved!', 1); + } + } +}; + +IDE_Morph.prototype.saveProjectToDisk = function () { + var data, + link = document.createElement('a'); + + if (Process.prototype.isCatchingErrors) { + try { + data = this.serializer.serialize(this.stage); + link.setAttribute('href', 'data:text/xml,' + data); + link.setAttribute('download', this.projectName + '.xml'); + document.body.appendChild(link); + link.click(); + document.body.removeChild(link); + } catch (err) { + this.showMessage('Saving failed: ' + err); + } + } else { + data = this.serializer.serialize(this.stage); + link.setAttribute('href', 'data:text/xml,' + data); + link.setAttribute('download', this.projectName + '.xml'); + document.body.appendChild(link); + link.click(); + document.body.removeChild(link); + } +}; + +IDE_Morph.prototype.exportProject = function (name, plain) { + var menu, str; + if (name) { + this.setProjectName(name); + if (Process.prototype.isCatchingErrors) { + try { + menu = this.showMessage('Exporting'); + str = encodeURIComponent( + this.serializer.serialize(this.stage) + ); + location.hash = '#open:' + str; + window.open('data:text/' + + (plain ? 'plain,' + str : 'xml,' + str)); + menu.destroy(); + this.showMessage('Exported!', 1); + } catch (err) { + this.showMessage('Export failed: ' + err); + } + } else { + menu = this.showMessage('Exporting'); + str = encodeURIComponent( + this.serializer.serialize(this.stage) + ); + location.hash = '#open:' + str; + window.open('data:text/' + + (plain ? 'plain,' + str : 'xml,' + str)); + menu.destroy(); + this.showMessage('Exported!', 1); + } + } +}; + +IDE_Morph.prototype.exportGlobalBlocks = function () { + if (this.stage.globalBlocks.length > 0) { + new BlockExportDialogMorph( + this.serializer, + this.stage.globalBlocks + ).popUp(this.world()); + } else { + this.inform( + 'Export blocks', + 'this project doesn\'t have any\n' + + 'custom global blocks yet' + ); + } +}; + +IDE_Morph.prototype.exportSprite = function (sprite) { + var str = this.serializer.serialize(sprite); + window.open('data:text/xml,<sprites app="' + + this.serializer.app + + '" version="' + + this.serializer.version + + '">' + + str + + '</sprites>'); +}; + +IDE_Morph.prototype.openProjectString = function (str) { + var msg, + myself = this; + this.nextSteps([ + function () { + msg = myself.showMessage('Opening project...'); + }, + function () { + myself.rawOpenProjectString(str); + }, + function () { + msg.destroy(); + } + ]); +}; + +IDE_Morph.prototype.rawOpenProjectString = function (str) { + if (Process.prototype.isCatchingErrors) { + try { + this.serializer.openProject(this.serializer.load(str), this); + } catch (err) { + this.showMessage('Load failed: ' + err); + } + } else { + this.serializer.openProject(this.serializer.load(str), this); + } + this.stopFastTracking(); +}; + +IDE_Morph.prototype.openCloudDataString = function (str) { + var msg, + myself = this; + this.nextSteps([ + function () { + msg = myself.showMessage('Opening project...'); + }, + function () { + myself.rawOpenCloudDataString(str); + }, + function () { + msg.destroy(); + } + ]); +}; + +IDE_Morph.prototype.rawOpenCloudDataString = function (str) { + var model; + if (Process.prototype.isCatchingErrors) { + try { + model = this.serializer.parse(str); + this.serializer.loadMediaModel(model.childNamed('media')); + this.serializer.openProject( + this.serializer.loadProjectModel(model.childNamed('project')), + this + ); + } catch (err) { + this.showMessage('Load failed: ' + err); + } + } else { + model = this.serializer.parse(str); + this.serializer.loadMediaModel(model.childNamed('media')); + this.serializer.openProject( + this.serializer.loadProjectModel(model.childNamed('project')), + this + ); + } + this.stopFastTracking(); +}; + +IDE_Morph.prototype.openBlocksString = function (str, name, silently) { + var msg, + myself = this; + this.nextSteps([ + function () { + msg = myself.showMessage('Opening blocks...'); + }, + function () { + myself.rawOpenBlocksString(str, name, silently); + }, + function () { + msg.destroy(); + } + ]); +}; + +IDE_Morph.prototype.rawOpenBlocksString = function (str, name, silently) { + // name is optional (string), so is silently (bool) + var blocks, + myself = this; + if (Process.prototype.isCatchingErrors) { + try { + blocks = this.serializer.loadBlocks(str, myself.stage); + } catch (err) { + this.showMessage('Load failed: ' + err); + } + } else { + blocks = this.serializer.loadBlocks(str, myself.stage); + } + if (silently) { + blocks.forEach(function (def) { + def.receiver = myself.stage; + myself.stage.globalBlocks.push(def); + }); + this.flushPaletteCache(); + this.refreshPalette(); + this.showMessage( + 'Imported Blocks Module' + (name ? ': ' + name : '') + '.', + 2 + ); + } else { + new BlockImportDialogMorph(blocks, this.stage, name).popUp(); + } +}; + +IDE_Morph.prototype.openSpritesString = function (str) { + var msg, + myself = this; + this.nextSteps([ + function () { + msg = myself.showMessage('Opening sprite...'); + }, + function () { + myself.rawOpenSpritesString(str); + }, + function () { + msg.destroy(); + } + ]); +}; + +IDE_Morph.prototype.rawOpenSpritesString = function (str) { + if (Process.prototype.isCatchingErrors) { + try { + this.serializer.loadSprites(str, this); + } catch (err) { + this.showMessage('Load failed: ' + err); + } + } else { + this.serializer.loadSprites(str, this); + } +}; + +IDE_Morph.prototype.openMediaString = function (str) { + if (Process.prototype.isCatchingErrors) { + try { + this.serializer.loadMedia(str); + } catch (err) { + this.showMessage('Load failed: ' + err); + } + } else { + this.serializer.loadMedia(str); + } + this.showMessage('Imported Media Module.', 2); +}; + +IDE_Morph.prototype.openProject = function (name) { + var str; + if (name) { + this.showMessage('opening project\n' + name); + this.setProjectName(name); + this.openProjectString( + str = localStorage['-snap-project-' + name] + ); + location.hash = '#open:' + str; + } +}; + + +IDE_Morph.prototype.switchToUserMode = function () { + var world = this.world(); + + world.isDevMode = false; + Process.prototype.isCatchingErrors = true; + this.controlBar.updateLabel(); + this.isAutoFill = true; + this.isDraggable = false; + this.reactToWorldResize(world.bounds.copy()); + this.siblings().forEach(function (morph) { + if (morph instanceof DialogBoxMorph) { + world.add(morph); // bring to front + } else { + morph.destroy(); + } + }); + this.flushBlocksCache(); + this.refreshPalette(); + // prevent non-DialogBoxMorphs from being dropped + // onto the World in user-mode + world.reactToDropOf = function (morph) { + if (!(morph instanceof DialogBoxMorph)) { + world.hand.grab(morph); + } + }; + this.showMessage('entering user mode', 1); + +}; + +IDE_Morph.prototype.switchToDevMode = function () { + var world = this.world(); + + world.isDevMode = true; + Process.prototype.isCatchingErrors = false; + this.controlBar.updateLabel(); + this.isAutoFill = false; + this.isDraggable = true; + this.setExtent(world.extent().subtract(100)); + this.setPosition(world.position().add(20)); + this.flushBlocksCache(); + this.refreshPalette(); + // enable non-DialogBoxMorphs to be dropped + // onto the World in dev-mode + delete world.reactToDropOf; + this.showMessage( + 'entering development mode.\n\n' + + 'error catching is turned off,\n' + + 'use the browser\'s web console\n' + + 'to see error messages.' + ); +}; + +IDE_Morph.prototype.flushBlocksCache = function (category) { + // if no category is specified, the whole cache gets flushed + if (category) { + this.stage.blocksCache[category] = null; + this.stage.children.forEach(function (m) { + if (m instanceof SpriteMorph) { + m.blocksCache[category] = null; + } + }); + } else { + this.stage.blocksCache = {}; + this.stage.children.forEach(function (m) { + if (m instanceof SpriteMorph) { + m.blocksCache = {}; + } + }); + } + this.flushPaletteCache(category); +}; + +IDE_Morph.prototype.flushPaletteCache = function (category) { + // if no category is specified, the whole cache gets flushed + if (category) { + this.stage.paletteCache[category] = null; + this.stage.children.forEach(function (m) { + if (m instanceof SpriteMorph) { + m.paletteCache[category] = null; + } + }); + } else { + this.stage.paletteCache = {}; + this.stage.children.forEach(function (m) { + if (m instanceof SpriteMorph) { + m.paletteCache = {}; + } + }); + } +}; + +IDE_Morph.prototype.toggleZebraColoring = function () { + var scripts = []; + + if (!BlockMorph.prototype.zebraContrast) { + BlockMorph.prototype.zebraContrast = 40; + } else { + BlockMorph.prototype.zebraContrast = 0; + } + + // select all scripts: + this.stage.children.concat(this.stage).forEach(function (morph) { + if (morph instanceof SpriteMorph || morph instanceof StageMorph) { + scripts = scripts.concat( + morph.scripts.children.filter(function (morph) { + return morph instanceof BlockMorph; + }) + ); + } + }); + + // force-update all scripts: + scripts.forEach(function (topBlock) { + topBlock.fixBlockColor(null, true); + }); +}; + +IDE_Morph.prototype.toggleDynamicInputLabels = function () { + var projectData; + SyntaxElementMorph.prototype.dynamicInputLabels = + !SyntaxElementMorph.prototype.dynamicInputLabels; + if (Process.prototype.isCatchingErrors) { + try { + projectData = this.serializer.serialize(this.stage); + } catch (err) { + this.showMessage('Serialization failed: ' + err); + } + } else { + projectData = this.serializer.serialize(this.stage); + } + SpriteMorph.prototype.initBlocks(); + this.spriteBar.tabBar.tabTo('scripts'); + this.createCategories(); + this.createCorralBar(); + this.openProjectString(projectData); +}; + +IDE_Morph.prototype.toggleBlurredShadows = function () { + window.useBlurredShadows = !useBlurredShadows; +}; + +IDE_Morph.prototype.toggleLongFormInputDialog = function () { + InputSlotDialogMorph.prototype.isLaunchingExpanded = + !InputSlotDialogMorph.prototype.isLaunchingExpanded; +}; + +IDE_Morph.prototype.togglePreferEmptySlotDrops = function () { + ScriptsMorph.prototype.isPreferringEmptySlots = + !ScriptsMorph.prototype.isPreferringEmptySlots; +}; + +IDE_Morph.prototype.toggleVirtualKeyboard = function () { + MorphicPreferences.useVirtualKeyboard = + !MorphicPreferences.useVirtualKeyboard; +}; + +IDE_Morph.prototype.toggleInputSliders = function () { + MorphicPreferences.useSliderForInput = + !MorphicPreferences.useSliderForInput; +}; + +IDE_Morph.prototype.toggleSliderExecute = function () { + InputSlotMorph.prototype.executeOnSliderEdit = + !InputSlotMorph.prototype.executeOnSliderEdit; +}; + +IDE_Morph.prototype.toggleAppMode = function (appMode) { + var world = this.world(), + elements = [ + this.logo, + this.controlBar.cloudButton, + this.controlBar.projectButton, + this.controlBar.settingsButton, + this.controlBar.stageSizeButton, + this.corral, + this.corralBar, + this.spriteEditor, + this.spriteBar, + this.palette, + this.categories + ]; + + this.isAppMode = isNil(appMode) ? !this.isAppMode : appMode; + + Morph.prototype.trackChanges = false; + if (this.isAppMode) { + this.setColor(new Color()); + this.controlBar.setColor(this.color); + this.controlBar.appModeButton.refresh(); + elements.forEach(function (e) { + e.hide(); + }); + world.children.forEach(function (morph) { + if (morph instanceof DialogBoxMorph) { + morph.hide(); + } + }); + } else { + this.setColor(this.backgroundColor); + this.controlBar.setColor(this.frameColor); + elements.forEach(function (e) { + e.show(); + }); + this.stage.setScale(1); + // show all hidden dialogs + world.children.forEach(function (morph) { + if (morph instanceof DialogBoxMorph) { + morph.show(); + } + }); + // prevent scrollbars from showing when morph appears + world.allChildren().filter(function (c) { + return c instanceof ScrollFrameMorph; + }).forEach(function (s) { + s.adjustScrollBars(); + }); + } + this.setExtent(this.world().extent()); // resume trackChanges +}; + +IDE_Morph.prototype.toggleStageSize = function (isSmall) { + var myself = this, + world = this.world(); + + function zoomIn() { + myself.stageRatio = 1; + myself.step = function () { + myself.stageRatio -= (myself.stageRatio - 0.5) / 2; + myself.setExtent(world.extent()); + if (myself.stageRatio < 0.6) { + myself.stageRatio = 0.5; + myself.setExtent(world.extent()); + delete myself.step; + } + }; + } + + function zoomOut() { + myself.isSmallStage = true; + myself.stageRatio = 0.5; + myself.step = function () { + myself.stageRatio += (1 - myself.stageRatio) / 2; + myself.setExtent(world.extent()); + if (myself.stageRatio > 0.9) { + myself.isSmallStage = false; + myself.setExtent(world.extent()); + myself.controlBar.stageSizeButton.refresh(); + delete myself.step; + } + }; + } + + this.isSmallStage = isNil(isSmall) ? !this.isSmallStage : isSmall; + if (this.isAnimating) { + if (this.isSmallStage) { + zoomIn(); + } else { + zoomOut(); + } + } else { + if (this.isSmallStage) {this.stageRatio = 0.5; } + this.setExtent(world.extent()); + } +}; + +IDE_Morph.prototype.openProjectsBrowser = function () { + new ProjectDialogMorph(this, 'open').popUp(); +}; + +IDE_Morph.prototype.saveProjectsBrowser = function () { + new ProjectDialogMorph(this, 'save').popUp(); +}; + +// IDE_Morph localization + +IDE_Morph.prototype.languageMenu = function () { + var menu = new MenuMorph(this), + world = this.world(), + pos = this.controlBar.settingsButton.bottomLeft(), + myself = this; + SnapTranslator.languages().forEach(function (lang) { + menu.addItem( + (SnapTranslator.language === lang ? '\u2713 ' : ' ') + + SnapTranslator.languageName(lang), + function () {myself.setLanguage(lang); } + ); + }); + menu.popup(world, pos); +}; + +IDE_Morph.prototype.setLanguage = function (lang) { + var translation = document.getElementById('language'), + src = 'lang-' + lang + '.js', + myself = this; + SnapTranslator.unload(); + if (translation) { + document.head.removeChild(translation); + } + if (lang === 'en') { + return this.reflectLanguage('en'); + } + translation = document.createElement('script'); + translation.id = 'language'; + translation.onload = function () {myself.reflectLanguage(lang); }; + document.head.appendChild(translation); + translation.src = src; +}; + +IDE_Morph.prototype.reflectLanguage = function (lang) { + var projectData; + SnapTranslator.language = lang; + if (Process.prototype.isCatchingErrors) { + try { + projectData = this.serializer.serialize(this.stage); + } catch (err) { + this.showMessage('Serialization failed: ' + err); + } + } else { + projectData = this.serializer.serialize(this.stage); + } + SpriteMorph.prototype.initBlocks(); + this.spriteBar.tabBar.tabTo('scripts'); + this.createCategories(); + this.createCorralBar(); + this.fixLayout(); + this.openProjectString(projectData); +}; + +// IDE_Morph cloud interface + +IDE_Morph.prototype.initializeCloud = function () { + var myself = this, + world = this.world(); + new DialogBoxMorph( + null, + function (user) { + var pwh = hex_sha512(user.password), + str; + SnapCloud.login( + user.username, + pwh, + function () { + if (user.choice) { + str = SnapCloud.encodeDict( + { + username: user.username, + password: pwh + } + ); + localStorage['-snap-user'] = str; + } + myself.source = 'cloud'; + myself.showMessage('now connected.', 2); + }, + myself.cloudError() + ); + } + ).promptCredentials( + 'Sign in', + 'login', + null, + null, + null, + null, + 'stay signed in on this computer\nuntil logging out', + world, + myself.cloudIcon(), + myself.cloudMsg + ); +}; + +IDE_Morph.prototype.createCloudAccount = function () { + var myself = this, + world = this.world(); + new DialogBoxMorph( + null, + function (user) { + SnapCloud.signup( + user.username, + user.email, + function (txt, title) { + new DialogBoxMorph().inform( + title, + txt + + '.\n\nAn e-mail with your password\n' + + 'has been sent to the address provided', + world, + myself.cloudIcon(null, new Color(0, 180, 0)) + ); + }, + myself.cloudError() + ); + } + ).promptCredentials( + 'Sign up', + 'signup', + 'http://snap.berkeley.edu/tos.html', + 'Terms of Service...', + 'http://snap.berkeley.edu/privacy.html', + 'Privacy...', + 'I have read and agree\nto the Terms of Service', + world, + myself.cloudIcon(), + myself.cloudMsg + ); +}; + +IDE_Morph.prototype.changeCloudPassword = function () { + var myself = this, + world = this.world(); + new DialogBoxMorph( + null, + function (user) { + SnapCloud.changePassword( + user.oldpassword, + user.password, + function () { + myself.logout(); + myself.showMessage('password has been changed.', 2); + }, + myself.cloudError() + ); + } + ).promptCredentials( + 'Change Password', + 'changePassword', + null, + null, + null, + null, + null, + world, + myself.cloudIcon(), + myself.cloudMsg + ); +}; + +IDE_Morph.prototype.logout = function () { + var myself = this; + delete localStorage['-snap-user']; + SnapCloud.logout( + function () { + SnapCloud.clear(); + myself.showMessage('disconnected.', 2); + }, + function () { + SnapCloud.clear(); + myself.showMessage('disconnected.', 2); + } + ); +}; + +IDE_Morph.prototype.saveProjectToCloud = function (name) { + var myself = this; + if (name) { + this.showMessage('Saving project\nto the cloud...'); + this.setProjectName(name); + SnapCloud.saveProject( + this, + function () {myself.showMessage('saved.', 2); }, + this.cloudError() + ); + } +}; + +IDE_Morph.prototype.exportProjectMedia = function (name) { + var menu, str, media; + this.serializer.isCollectingMedia = true; + if (name) { + this.setProjectName(name); + if (Process.prototype.isCatchingErrors) { + try { + menu = this.showMessage('Exporting'); + str = encodeURIComponent( + this.serializer.serialize(this.stage) + ); + media = encodeURIComponent( + this.serializer.mediaXML(name) + ); + window.open('data:text/xml,' + media); + menu.destroy(); + this.showMessage('Exported!', 1); + } catch (err) { + this.serializer.isCollectingMedia = false; + this.showMessage('Export failed: ' + err); + } + } else { + menu = this.showMessage('Exporting'); + str = encodeURIComponent( + this.serializer.serialize(this.stage) + ); + media = encodeURIComponent( + this.serializer.mediaXML() + ); + window.open('data:text/xml,' + media); + menu.destroy(); + this.showMessage('Exported!', 1); + } + } + this.serializer.isCollectingMedia = false; + this.serializer.flushMedia(); + // this.hasChangedMedia = false; +}; + +IDE_Morph.prototype.exportProjectNoMedia = function (name) { + var menu, str; + this.serializer.isCollectingMedia = true; + if (name) { + this.setProjectName(name); + if (Process.prototype.isCatchingErrors) { + try { + menu = this.showMessage('Exporting'); + str = encodeURIComponent( + this.serializer.serialize(this.stage) + ); + window.open('data:text/xml,' + str); + menu.destroy(); + this.showMessage('Exported!', 1); + } catch (err) { + this.serializer.isCollectingMedia = false; + this.showMessage('Export failed: ' + err); + } + } else { + menu = this.showMessage('Exporting'); + str = encodeURIComponent( + this.serializer.serialize(this.stage) + ); + window.open('data:text/xml,' + str); + menu.destroy(); + this.showMessage('Exported!', 1); + } + } + this.serializer.isCollectingMedia = false; + this.serializer.flushMedia(); +}; + +IDE_Morph.prototype.exportProjectAsCloudData = function (name) { + var menu, str, media, dta; + this.serializer.isCollectingMedia = true; + if (name) { + this.setProjectName(name); + if (Process.prototype.isCatchingErrors) { + try { + menu = this.showMessage('Exporting'); + str = encodeURIComponent( + this.serializer.serialize(this.stage) + ); + media = encodeURIComponent( + this.serializer.mediaXML(name) + ); + dta = encodeURIComponent('<snapdata>') + + str + + media + + encodeURIComponent('</snapdata>'); + window.open('data:text/xml,' + dta); + menu.destroy(); + this.showMessage('Exported!', 1); + } catch (err) { + this.serializer.isCollectingMedia = false; + this.showMessage('Export failed: ' + err); + } + } else { + menu = this.showMessage('Exporting'); + str = encodeURIComponent( + this.serializer.serialize(this.stage) + ); + media = encodeURIComponent( + this.serializer.mediaXML() + ); + dta = encodeURIComponent('<snapdata>') + + str + + media + + encodeURIComponent('</snapdata>'); + window.open('data:text/xml,' + dta); + menu.destroy(); + this.showMessage('Exported!', 1); + } + } + this.serializer.isCollectingMedia = false; + this.serializer.flushMedia(); + // this.hasChangedMedia = false; +}; + +IDE_Morph.prototype.cloudAcknowledge = function () { + var myself = this; + return function (responseText, url) { + nop(responseText); + new DialogBoxMorph().inform( + 'Cloud Connection', + 'Successfully connected to:\n' + + 'http://' + + url, + myself.world(), + myself.cloudIcon(null, new Color(0, 180, 0)) + ); + }; +}; + +IDE_Morph.prototype.cloudResponse = function () { + var myself = this; + return function (responseText, url) { + var response = responseText; + if (response.length > 50) { + response = response.substring(0, 50) + '...'; + } + new DialogBoxMorph().inform( + 'Snap!Cloud', + 'http://' + + url + ':\n\n' + + 'responds:\n' + + response, + myself.world(), + myself.cloudIcon(null, new Color(0, 180, 0)) + ); + }; +}; + +IDE_Morph.prototype.cloudError = function () { + var myself = this; + return function (responseText, url) { + var response = responseText; + if (response.length > 50) { + response = response.substring(0, 50) + '...'; + } + new DialogBoxMorph().inform( + 'Snap!Cloud', + (url ? url + '\n' : '') + + response, + myself.world(), + myself.cloudIcon(null, new Color(180, 0, 0)) + ); + }; +}; + +IDE_Morph.prototype.cloudIcon = function (height, color) { + var clr = color || DialogBoxMorph.prototype.titleBarColor, + icon = new SymbolMorph( + 'cloudGradient', + height || 50, + clr, + new Point(-1, -1), + clr.darker(50) + ); + icon.addShadow(new Point(1, 1), 1, clr.lighter(95)); + return icon; +}; + +IDE_Morph.prototype.setCloudURL = function () { + new DialogBoxMorph( + null, + function (url) { + SnapCloud.url = url; + } + ).prompt( + 'Cloud URL', + SnapCloud.url, + this.world(), + null, + { + 'Snap!Cloud' : + 'https://snapcloud.miosoft.com/miocon/app/' + + 'login?_app=SnapCloud', + 'local network dev' : + '192.168.2.108:8087/miocon/app/login?_app=SnapCloud', + 'localhost dev' : + 'localhost/miocon/app/login?_app=SnapCloud' + } + ); +}; + +IDE_Morph.prototype.cloudMsg = null; +/* + 'Snap!Cloud is in Beta-test status.\n' + + 'Expect hiccups and downtimes and don\'t\n' + + 'forget to save important work locally.'; +*/ + +// IDE_Morph user dialog shortcuts + +IDE_Morph.prototype.showMessage = function (message, secs) { + var m = new MenuMorph(null, message), + intervalHandle; + m.popUpCenteredInWorld(this.world()); + if (secs) { + intervalHandle = setInterval(function () { + m.destroy(); + clearInterval(intervalHandle); + }, secs * 1000); + } + return m; +}; + +IDE_Morph.prototype.inform = function (title, message) { + new DialogBoxMorph().inform( + title, + localize(message), + this.world() + ); +}; + +IDE_Morph.prototype.confirm = function (message, title, action) { + new DialogBoxMorph(null, action).askYesNo( + title, + localize(message), + this.world() + ); +}; + +IDE_Morph.prototype.prompt = function (message, callback, choices) { + (new DialogBoxMorph(null, callback)).prompt( + message, + '', + this.world(), + null, + choices + ); +}; + +// ProjectDialogMorph //////////////////////////////////////////////////// + +// ProjectDialogMorph inherits from DialogBoxMorph: + +ProjectDialogMorph.prototype = new DialogBoxMorph(); +ProjectDialogMorph.prototype.constructor = ProjectDialogMorph; +ProjectDialogMorph.uber = DialogBoxMorph.prototype; + +// ProjectDialogMorph instance creation: + +function ProjectDialogMorph(ide, label) { + this.init(ide, label); +} + +ProjectDialogMorph.prototype.init = function (ide, task) { + var myself = this; + + // additional properties: + this.ide = ide; + this.task = task || 'open'; // String describing what do do (open, save) + this.source = ide.source || 'local'; // or 'cloud' or 'examples' + this.projectList = []; // [{name: , thumb: , notes:}] + + this.handle = null; + this.srcBar = null; + this.nameField = null; + this.listField = null; + this.preview = null; + this.notesText = null; + this.notesField = null; + + // initialize inherited properties: + ProjectDialogMorph.uber.init.call( + this, + this, // target + null, // function + null // environment + ); + + // override inherited properites: + this.labelString = this.task === 'save' ? 'Save Project' : 'Open Project'; + this.createLabel(); + + // build contents + this.buildContents(); + this.onNextStep = function () { // yield to show "updating" message + myself.setSource(myself.source); + }; +}; + +ProjectDialogMorph.prototype.buildContents = function () { + var thumbnail; + + this.addBody(new Morph()); + this.body.color = this.color; + + this.srcBar = new AlignmentMorph('column', this.padding / 2); + this.addSourceButton('cloud', localize('Cloud'), 'cloud'); + this.addSourceButton('local', localize('Browser'), 'storage'); + if (this.task === 'open') { + this.addSourceButton('examples', localize('Examples'), 'poster'); + } + this.srcBar.fixLayout(); + this.body.add(this.srcBar); + + if (this.task === 'save') { + this.nameField = new InputFieldMorph(this.ide.projectName); + this.body.add(this.nameField); + } + + this.listField = new ListMorph([]); + this.fixListFieldItemColors(); + this.listField.fixLayout = nop; + this.listField.edge = InputFieldMorph.prototype.edge; + this.listField.fontSize = InputFieldMorph.prototype.fontSize; + this.listField.typeInPadding = InputFieldMorph.prototype.typeInPadding; + this.listField.contrast = InputFieldMorph.prototype.contrast; + this.listField.drawNew = InputFieldMorph.prototype.drawNew; + this.listField.drawRectBorder = InputFieldMorph.prototype.drawRectBorder; + + this.body.add(this.listField); + + this.preview = new Morph(); + this.preview.fixLayout = nop; + this.preview.edge = InputFieldMorph.prototype.edge; + this.preview.fontSize = InputFieldMorph.prototype.fontSize; + this.preview.typeInPadding = InputFieldMorph.prototype.typeInPadding; + this.preview.contrast = InputFieldMorph.prototype.contrast; + this.preview.drawNew = function () { + InputFieldMorph.prototype.drawNew.call(this); + if (this.texture) { + this.drawTexture(this.texture); + } + }; + this.preview.drawCachedTexture = function () { + var context = this.image.getContext('2d'); + context.drawImage(this.cachedTexture, this.edge, this.edge); + this.changed(); + }; + this.preview.drawRectBorder = InputFieldMorph.prototype.drawRectBorder; + this.preview.setExtent( + this.ide.serializer.thumbnailSize.add(this.preview.edge * 2) + ); + + this.body.add(this.preview); + this.preview.drawNew(); + if (this.task === 'save') { + thumbnail = this.ide.stage.thumbnail( + SnapSerializer.prototype.thumbnailSize + ); + this.preview.texture = null; + this.preview.cachedTexture = thumbnail; + this.preview.drawCachedTexture(); + } + + this.notesField = new ScrollFrameMorph(); + this.notesField.fixLayout = nop; + + this.notesField.edge = InputFieldMorph.prototype.edge; + this.notesField.fontSize = InputFieldMorph.prototype.fontSize; + this.notesField.typeInPadding = InputFieldMorph.prototype.typeInPadding; + this.notesField.contrast = InputFieldMorph.prototype.contrast; + this.notesField.drawNew = InputFieldMorph.prototype.drawNew; + this.notesField.drawRectBorder = InputFieldMorph.prototype.drawRectBorder; + + this.notesField.acceptsDrops = false; + this.notesField.contents.acceptsDrops = false; + + if (this.task === 'open') { + this.notesText = new TextMorph(''); + } else { // 'save' + this.notesText = new TextMorph(this.ide.projectNotes); + this.notesText.isEditable = true; + this.notesText.enableSelecting(); + } + + this.notesField.isTextLineWrapping = true; + this.notesField.padding = 3; + this.notesField.setContents(this.notesText); + this.notesField.setWidth(this.preview.width()); + + this.body.add(this.notesField); + + if (this.task === 'open') { + this.addButton('openProject', 'Open'); + this.addButton('deleteProject', 'Delete'); + this.action = 'openProject'; + } else { // 'save' + this.addButton('saveProject', 'Save'); + this.action = 'saveProject'; + } + this.addButton('cancel', 'Cancel'); + + this.setExtent(new Point(455, 335)); + this.fixLayout(); + +}; + +ProjectDialogMorph.prototype.popUp = function (wrrld) { + var world = wrrld || this.ide.world(); + if (world) { + world.add(this); + world.keyboardReceiver = this; + this.handle = new HandleMorph( + this, + 350, + 300, + this.corner, + this.corner + ); + this.setCenter(world.center()); + this.edit(); + } +}; + +// ProjectDialogMorph source buttons + +ProjectDialogMorph.prototype.addSourceButton = function ( + source, + label, + symbol +) { + var myself = this, + lbl1 = new StringMorph( + label, + 10, + null, + true, + null, + null, + new Point(1, 1), + new Color(255, 255, 255) + ), + lbl2 = new StringMorph( + label, + 10, + null, + true, + null, + null, + new Point(-1, -1), + this.titleBarColor.darker(50), + new Color(255, 255, 255) + ), + l1 = new Morph(), + l2 = new Morph(), + button; + + lbl1.add(new SymbolMorph( + symbol, + 24, + this.titleBarColor.darker(20), + new Point(1, 1), + this.titleBarColor.darker(50) + )); + lbl1.children[0].setCenter(lbl1.center()); + lbl1.children[0].setBottom(lbl1.top() - this.padding / 2); + + l1.image = lbl1.fullImage(); + l1.bounds = lbl1.fullBounds(); + + lbl2.add(new SymbolMorph( + symbol, + 24, + new Color(255, 255, 255), + new Point(-1, -1), + this.titleBarColor.darker(50) + )); + lbl2.children[0].setCenter(lbl2.center()); + lbl2.children[0].setBottom(lbl2.top() - this.padding / 2); + + l2.image = lbl2.fullImage(); + l2.bounds = lbl2.fullBounds(); + + button = new ToggleButtonMorph( + null, //colors, + myself, // the ProjectDialog is the target + function () { // action + myself.setSource(source); + }, + [l1, l2], + function () { // query + return myself.source === source; + } + ); + + button.corner = this.buttonCorner; + button.edge = this.buttonEdge; + button.outline = this.buttonOutline; + button.outlineColor = this.buttonOutlineColor; + button.outlineGradient = this.buttonOutlineGradient; + button.labelMinExtent = new Point(60, 0); + button.padding = this.buttonPadding; + button.contrast = this.buttonContrast; + button.pressColor = this.titleBarColor.darker(20); + + button.drawNew(); + button.fixLayout(); + button.refresh(); + this.srcBar.add(button); +}; + +// ProjectDialogMorph list field control + +ProjectDialogMorph.prototype.fixListFieldItemColors = function () { + // remember to always fixLayout() afterwards for the changes + // to take effect + var myself = this; + this.listField.contents.children[0].alpha = 0; + this.listField.contents.children[0].children.forEach(function (item) { + item.pressColor = myself.titleBarColor.darker(20); + item.color = new Color(0, 0, 0, 0); + item.noticesTransparentClick = true; + }); +}; + +// ProjectDialogMorph ops + +ProjectDialogMorph.prototype.setSource = function (source) { + var myself = this, + msg; + + this.source = source; //this.task === 'save' ? 'local' : source; + this.srcBar.children.forEach(function (button) { + button.refresh(); + }); + switch (this.source) { + case 'cloud': + msg = myself.ide.showMessage('Updating\nproject list...'); + this.projectList = []; + SnapCloud.getProjectList( + function (projectList) { + myself.installCloudProjectList(projectList); + msg.destroy(); + }, + function (err, lbl) { + msg.destroy(); + myself.ide.cloudError().call(null, err, lbl); + } + ); + break; + case 'local': + this.projectList = this.getLocalProjectList(); + break; + case 'examples': + this.projectList = []; + break; + } + this.listField.destroy(); + + this.listField = new ListMorph( + this.projectList, + this.projectList.length > 0 ? + function (element) { + return element.name; + } : null + ); + + this.fixListFieldItemColors(); + this.listField.fixLayout = nop; + this.listField.edge = InputFieldMorph.prototype.edge; + this.listField.fontSize = InputFieldMorph.prototype.fontSize; + this.listField.typeInPadding = InputFieldMorph.prototype.typeInPadding; + this.listField.contrast = InputFieldMorph.prototype.contrast; + this.listField.drawNew = InputFieldMorph.prototype.drawNew; + this.listField.drawRectBorder = InputFieldMorph.prototype.drawRectBorder; + + if (this.source === 'local') { + this.listField.action = function (item) { + var src, xml; + + if (myself.nameField) { + myself.nameField.setContents(item.name || ''); + } + if (myself.task === 'open') { + + src = localStorage['-snap-project-' + item.name]; + xml = myself.ide.serializer.parse(src); + + myself.notesText.text = xml.childNamed('notes').contents + || ''; + myself.notesText.drawNew(); + myself.notesField.contents.adjustBounds(); + myself.preview.texture = xml.childNamed('thumbnail').contents + || null; + myself.preview.cachedTexture = null; + myself.preview.drawNew(); + } + myself.edit(); + }; + } else { // 'examples', 'cloud' is initialized elsewhere + this.listField.action = function (item) { + if (myself.nameField) { + myself.nameField.setContents(item.name || ''); + } + if (myself.task === 'open') { + myself.notesText.text = item.notes || ''; + myself.notesText.drawNew(); + myself.notesField.contents.adjustBounds(); + myself.preview.texture = item.thumb || null; + myself.preview.cachedTexture = null; + myself.preview.drawNew(); + } + myself.edit(); + }; + } + this.body.add(this.listField); + this.fixLayout(); + if (this.task === 'open') { + this.clearDetails(); + } +}; + +ProjectDialogMorph.prototype.getLocalProjectList = function () { + var stored, name, dta, + projects = []; + for (stored in localStorage) { + if (localStorage.hasOwnProperty(stored) + && stored.substr(0, 14) === '-snap-project-') { + name = stored.substr(14); + dta = { + name: name, + thumb: null, + notes: null + }; + projects.push(dta); + } + } + projects.sort(function (x, y) { + return x.name < y.name ? -1 : 1; + }); + return projects; +}; + +ProjectDialogMorph.prototype.installCloudProjectList = function (pl) { + var myself = this; + this.projectList = pl || []; + this.projectList.sort(function (x, y) { + return x.ProjectName < y.ProjectName ? -1 : 1; + }); + + this.listField.destroy(); + this.listField = new ListMorph( + this.projectList, + this.projectList.length > 0 ? + function (element) { + return element.ProjectName; + } : null + ); + + this.fixListFieldItemColors(); + this.listField.fixLayout = nop; + this.listField.edge = InputFieldMorph.prototype.edge; + this.listField.fontSize = InputFieldMorph.prototype.fontSize; + this.listField.typeInPadding = InputFieldMorph.prototype.typeInPadding; + this.listField.contrast = InputFieldMorph.prototype.contrast; + this.listField.drawNew = InputFieldMorph.prototype.drawNew; + this.listField.drawRectBorder = InputFieldMorph.prototype.drawRectBorder; + + this.listField.action = function (item) { + if (myself.nameField) { + myself.nameField.setContents(item.ProjectName || ''); + } + if (myself.task === 'open') { + myself.notesText.text = item.Notes || ''; + myself.notesText.drawNew(); + myself.notesField.contents.adjustBounds(); + myself.preview.texture = item.Thumbnail || null; + myself.preview.cachedTexture = null; + myself.preview.drawNew(); + } + myself.edit(); + }; + this.body.add(this.listField); + this.fixLayout(); + if (this.task === 'open') { + this.clearDetails(); + } +}; + +ProjectDialogMorph.prototype.clearDetails = function () { + this.notesText.text = ''; + this.notesText.drawNew(); + this.notesField.contents.adjustBounds(); + this.preview.texture = null; + this.preview.cachedTexture = null; + this.preview.drawNew(); +}; + +ProjectDialogMorph.prototype.openProject = function () { + var myself = this; + + if (this.source === 'cloud') { + if (this.listField.selected) { + this.openSelectedCloudProject(); + } + } else { // 'local, examples' + if (this.listField.selected) { + myself.ide.source = 'local'; + this.ide.openProject(this.listField.selected.name); + this.destroy(); + } + } +}; + +ProjectDialogMorph.prototype.openSelectedCloudProject = function () { + var myself = this; + this.nextSteps([ + function () { + myself.ide.showMessage('Fetching project\nfrom the cloud...'); + }, + function () { + myself.rawOpenSelectedCloudProject(); + } + ]); +}; + +ProjectDialogMorph.prototype.rawOpenSelectedCloudProject = function () { + var myself = this; + SnapCloud.reconnect( + function () { + SnapCloud.callService( + 'getProject', + function (response) { + SnapCloud.disconnect(); + myself.ide.source = 'cloud'; + myself.ide.droppedText(response[0].SourceCode); + }, + myself.ide.cloudError(), + [myself.listField.selected.ProjectName] + ); + }, + myself.ide.cloudError() + ); + this.destroy(); +}; + +ProjectDialogMorph.prototype.saveProject = function () { + var name = this.nameField.contents().text.text, + notes = this.notesText.text, + myself = this; + + this.ide.projectNotes = notes || this.ide.projectNotes; + if (name) { + this.ide.setProjectName(name); + if (this.source === 'cloud') { + if (detect( + this.projectList, + function (item) {return item.ProjectName === name; } + )) { + this.ide.confirm( + localize( + 'Are you sure you want to replace' + ) + '\n"' + name + '"?', + 'Replace Project', + function () { + myself.saveCloudProject(); + } + + ); + } else { + myself.saveCloudProject(); + } + } else { // 'local' + if (detect( + this.projectList, + function (item) {return item.name === name; } + )) { + this.ide.confirm( + localize( + 'Are you sure you want to replace' + ) + '\n"' + name + '"?', + 'Replace Project', + function () { + myself.ide.source = 'local'; + myself.ide.saveProject(name); + myself.destroy(); + } + ); + } else { + myself.ide.source = 'local'; + this.ide.saveProject(name); + this.destroy(); + } + } + } +}; + +ProjectDialogMorph.prototype.saveCloudProject = function () { + var myself = this; + this.ide.showMessage('Saving project\nto the cloud...'); + SnapCloud.saveProject( + this.ide, + function () { + myself.ide.source = 'cloud'; + myself.ide.showMessage('saved.', 2); + }, + this.ide.cloudError() + ); + this.destroy(); +}; + +ProjectDialogMorph.prototype.deleteProject = function () { + var myself = this, + proj, + idx, + name; + + if (this.source === 'cloud') { + proj = this.listField.selected; + if (proj) { + this.ide.confirm( + localize( + 'Are you sure you want to delete' + ) + '\n"' + proj.ProjectName + '"?', + 'Delete Project', + function () { + SnapCloud.reconnect( + function () { + SnapCloud.callService( + 'deleteProject', + function () { + SnapCloud.disconnect(); + myself.ide.hasChangedMedia = true; + idx = myself.projectList.indexOf(proj); + myself.projectList.splice(idx, 1); + myself.installCloudProjectList( + myself.projectList + ); // refresh list + }, + myself.ide.cloudError(), + [myself.listField.selected.ProjectName] + ); + }, + myself.ide.cloudError() + ); + } + ); + } + } else { // 'local, examples' + if (this.listField.selected) { + name = this.listField.selected.name; + this.ide.confirm( + localize( + 'Are you sure you want to delete' + ) + '\n"' + name + '"?', + 'Delete Project', + function () { + delete localStorage['-snap-project-' + name]; + myself.setSource(myself.source); // refresh list + } + ); + } + } +}; + +ProjectDialogMorph.prototype.edit = function () { + if (this.nameField) { + this.nameField.edit(); + } +}; + +// ProjectDialogMorph layout + +ProjectDialogMorph.prototype.fixLayout = function () { + var th = fontHeight(this.titleFontSize) + this.titlePadding * 2, + thin = this.padding / 2, + oldFlag = Morph.prototype.trackChanges; + + Morph.prototype.trackChanges = false; + + if (this.buttons && (this.buttons.children.length > 0)) { + this.buttons.fixLayout(); + } + + if (this.body) { + this.body.setPosition(this.position().add(new Point( + this.padding, + th + this.padding + ))); + this.body.setExtent(new Point( + this.width() - this.padding * 2, + this.height() - this.padding * 3 - th - this.buttons.height() + )); + this.srcBar.setPosition(this.body.position()); + if (this.nameField) { + this.nameField.setWidth( + this.body.width() - this.srcBar.width() - this.padding * 6 + ); + this.nameField.setLeft(this.srcBar.right() + this.padding * 3); + this.nameField.setTop(this.srcBar.top()); + this.nameField.drawNew(); + } + + this.listField.setLeft(this.srcBar.right() + this.padding); + this.listField.setWidth( + this.body.width() + - this.srcBar.width() + - this.preview.width() + - this.padding + - thin + ); + this.listField.contents.children[0].adjustWidths(); + + if (this.nameField) { + this.listField.setTop(this.nameField.bottom() + this.padding); + this.listField.setHeight( + this.body.height() - this.nameField.height() - this.padding + ); + } else { + this.listField.setTop(this.body.top()); + this.listField.setHeight(this.body.height()); + } + + this.preview.setRight(this.body.right()); + if (this.nameField) { + this.preview.setTop(this.nameField.bottom() + this.padding); + } else { + this.preview.setTop(this.body.top()); + } + + this.notesField.setTop(this.preview.bottom() + thin); + this.notesField.setLeft(this.preview.left()); + this.notesField.setHeight( + this.body.bottom() - this.preview.bottom() - thin + ); + } + + if (this.label) { + this.label.setCenter(this.center()); + this.label.setTop(this.top() + (th - this.label.height()) / 2); + } + + if (this.buttons && (this.buttons.children.length > 0)) { + this.buttons.setCenter(this.center()); + this.buttons.setBottom(this.bottom() - this.padding); + } + + Morph.prototype.trackChanges = oldFlag; + this.changed(); +}; + +// SpriteIconMorph //////////////////////////////////////////////////// + +/* + I am a selectable element in the Sprite corral, keeping a self-updating + thumbnail of the sprite I'm respresenting, and a self-updating label + of the sprite's name (in case it is changed elsewhere) +*/ + +// SpriteIconMorph inherits from ToggleButtonMorph (Widgets) + +SpriteIconMorph.prototype = new ToggleButtonMorph(); +SpriteIconMorph.prototype.constructor = SpriteIconMorph; +SpriteIconMorph.uber = ToggleButtonMorph.prototype; + +// SpriteIconMorph settings + +SpriteIconMorph.prototype.thumbSize = new Point(40, 40); +SpriteIconMorph.prototype.labelShadowOffset = null; +SpriteIconMorph.prototype.labelShadowColor = null; +SpriteIconMorph.prototype.labelColor = new Color(255, 255, 255); +SpriteIconMorph.prototype.fontSize = 9; + +// SpriteIconMorph instance creation: + +function SpriteIconMorph(aSprite, aTemplate) { + this.init(aSprite, aTemplate); +} + +SpriteIconMorph.prototype.init = function (aSprite, aTemplate) { + var colors, action, query, myself = this; + + if (!aTemplate) { + colors = [ + IDE_Morph.prototype.groupColor, + IDE_Morph.prototype.frameColor, + IDE_Morph.prototype.frameColor + ]; + + } + + action = function () { + // make my sprite the current one + var ide = myself.parentThatIsA(IDE_Morph); + + if (ide) { + ide.selectSprite(myself.object); + } + }; + + query = function () { + // answer true if my sprite is the current one + var ide = myself.parentThatIsA(IDE_Morph); + + if (ide) { + return ide.currentSprite === myself.object; + } + return false; + }; + + // additional properties: + this.object = aSprite || new SpriteMorph(); // mandatory, actually + this.version = this.object.version; + this.thumbnail = null; + + // initialize inherited properties: + SpriteIconMorph.uber.init.call( + this, + colors, // color overrides, <array>: [normal, highlight, pressed] + null, // target - not needed here + action, // a toggle function + this.object.name, // label string + query, // predicate/selector + null, // environment + null, // hint + aTemplate // optional, for cached background images + ); + + // override defaults and build additional components + this.isDraggable = true; + this.createThumbnail(); + this.padding = 2; + this.corner = 8; + this.fixLayout(); + this.fps = 1; +}; + +SpriteIconMorph.prototype.createThumbnail = function () { + if (this.thumbnail) { + this.thumbnail.destroy(); + } + + this.thumbnail = new Morph(); + this.thumbnail.setExtent(this.thumbSize); + this.thumbnail.image = this.object.thumbnail(this.thumbSize); + this.add(this.thumbnail); +}; + +SpriteIconMorph.prototype.createLabel = function () { + var txt; + + if (this.label) { + this.label.destroy(); + } + txt = new StringMorph( + this.object.name, + this.fontSize, + this.fontStyle, + true, + false, + false, + this.labelShadowOffset, + this.labelShadowColor, + this.labelColor + ); + + this.label = new FrameMorph(); + this.label.acceptsDrops = false; + this.label.alpha = 0; + this.label.setExtent(txt.extent()); + txt.setPosition(this.label.position()); + this.label.add(txt); + this.add(this.label); +}; + +// SpriteIconMorph stepping + +SpriteIconMorph.prototype.step = function () { + if (this.version !== this.object.version) { + this.createThumbnail(); + this.createLabel(); + this.fixLayout(); + this.version = this.object.version; + this.refresh(); + } +}; + +// SpriteIconMorph layout + +SpriteIconMorph.prototype.fixLayout = function () { + if (!this.thumbnail) {return null; } + + this.setWidth( + this.thumbnail.width() + + this.outline * 2 + + this.edge * 2 + + this.padding * 2 + ); + + this.setHeight( + this.thumbnail.height() + + this.outline * 2 + + this.edge * 2 + + this.padding * 3 + + this.label.height() + ); + + this.thumbnail.setCenter(this.center()); + this.thumbnail.setTop( + this.top() + this.outline + this.edge + this.padding + ); + + this.label.setWidth( + Math.min( + this.label.children[0].width(), // the actual text + this.thumbnail.width() + ) + ); + this.label.setCenter(this.center()); + this.label.setTop( + this.thumbnail.bottom() + this.padding + ); +}; + +// SpriteIconMorph menu + +SpriteIconMorph.prototype.userMenu = function () { + var menu = new MenuMorph(this); + if (!(this.object instanceof SpriteMorph)) {return null; } + menu.addItem("show", 'showSpriteOnStage'); + menu.addLine(); + menu.addItem("duplicate", 'duplicateSprite'); + menu.addItem("delete", 'removeSprite'); + menu.addLine(); + menu.addItem("export...", 'exportSprite'); + return menu; +}; + +SpriteIconMorph.prototype.duplicateSprite = function () { + var ide = this.parentThatIsA(IDE_Morph); + if (ide) { + ide.duplicateSprite(this.object); + } +}; + +SpriteIconMorph.prototype.removeSprite = function () { + var ide = this.parentThatIsA(IDE_Morph); + if (ide) { + ide.removeSprite(this.object); + } +}; + +SpriteIconMorph.prototype.exportSprite = function () { + this.object.exportSprite(); +}; + +SpriteIconMorph.prototype.showSpriteOnStage = function () { + this.object.showOnStage(); +}; + +// SpriteIconMorph drawing + +SpriteIconMorph.prototype.createBackgrounds = function () { +// only draw the edges if I am selected + var context, + ext = this.extent(); + + if (this.template) { // take the backgrounds images from the template + this.image = this.template.image; + this.normalImage = this.template.normalImage; + this.highlightImage = this.template.highlightImage; + this.pressImage = this.template.pressImage; + return null; + } + + this.normalImage = newCanvas(ext); + context = this.normalImage.getContext('2d'); + this.drawBackground(context, this.color); + + this.highlightImage = newCanvas(ext); + context = this.highlightImage.getContext('2d'); + this.drawBackground(context, this.highlightColor); + + this.pressImage = newCanvas(ext); + context = this.pressImage.getContext('2d'); + this.drawOutline(context); + this.drawBackground(context, this.pressColor); + this.drawEdges( + context, + this.pressColor, + this.pressColor.lighter(this.contrast), + this.pressColor.darker(this.contrast) + ); + + this.image = this.normalImage; +}; + +// SpriteIconMorph drag & drop + +SpriteIconMorph.prototype.prepareToBeGrabbed = function () { + var ide = this.parentThatIsA(IDE_Morph), + idx; + this.mouseClickLeft(); // select me + if (ide) { + idx = ide.sprites.asArray().indexOf(this.object); + ide.sprites.remove(idx + 1); + ide.createCorral(); + ide.fixLayout(); + } +}; + +SpriteIconMorph.prototype.wantsDropOf = function (morph) { + // allow scripts & media to be copied from one sprite to another + // by drag & drop + return morph instanceof BlockMorph + || (morph instanceof CostumeIconMorph) + || (morph instanceof SoundIconMorph); +}; + +SpriteIconMorph.prototype.reactToDropOf = function (morph, hand) { + if (morph instanceof BlockMorph) { + this.copyStack(morph); + } else if (morph instanceof CostumeIconMorph) { + this.copyCostume(morph.object); + } else if (morph instanceof SoundIconMorph) { + this.copySound(morph.object); + } + this.world().add(morph); + morph.slideBackTo(hand.grabOrigin); +}; + +SpriteIconMorph.prototype.copyStack = function (block) { + var dup = block.fullCopy(), + y = Math.max(this.object.scripts.children.map(function (stack) { + return stack.fullBounds().bottom(); + }).concat([this.object.scripts.top()])); + + dup.setPosition(new Point(this.object.scripts.left() + 20, y + 20)); + this.object.scripts.add(dup); + this.object.scripts.adjustBounds(); + + // delete all custom blocks pointing to local definitions + // under construction... + dup.allChildren().forEach(function (morph) { + if (morph.definition && !morph.definition.isGlobal) { + morph.deleteBlock(); + } + }); +}; + +SpriteIconMorph.prototype.copyCostume = function (costume) { + var dup = costume.copy(); + this.object.addCostume(dup); + this.object.wearCostume(dup); +}; + +SpriteIconMorph.prototype.copySound = function (sound) { + var dup = sound.copy(); + this.object.addSound(dup.audio, dup.name); +}; + +// CostumeIconMorph //////////////////////////////////////////////////// + +/* + I am a selectable element in the SpriteEditor's "Costumes" tab, keeping + a self-updating thumbnail of the costume I'm respresenting, and a + self-updating label of the costume's name (in case it is changed + elsewhere) +*/ + +// CostumeIconMorph inherits from ToggleButtonMorph (Widgets) +// ... and copies methods from SpriteIconMorph + +CostumeIconMorph.prototype = new ToggleButtonMorph(); +CostumeIconMorph.prototype.constructor = CostumeIconMorph; +CostumeIconMorph.uber = ToggleButtonMorph.prototype; + +// CostumeIconMorph settings + +CostumeIconMorph.prototype.thumbSize = new Point(80, 60); +CostumeIconMorph.prototype.labelShadowOffset = null; +CostumeIconMorph.prototype.labelShadowColor = null; +CostumeIconMorph.prototype.labelColor = new Color(255, 255, 255); +CostumeIconMorph.prototype.fontSize = 9; + +// CostumeIconMorph instance creation: + +function CostumeIconMorph(aCostume, aTemplate) { + this.init(aCostume, aTemplate); +} + +CostumeIconMorph.prototype.init = function (aCostume, aTemplate) { + var colors, action, query, myself = this; + + if (!aTemplate) { + colors = [ + IDE_Morph.prototype.groupColor, + IDE_Morph.prototype.frameColor, + IDE_Morph.prototype.frameColor + ]; + + } + + action = function () { + // make my costume the current one + var ide = myself.parentThatIsA(IDE_Morph), + wardrobe = myself.parentThatIsA(WardrobeMorph); + + if (ide) { + ide.currentSprite.wearCostume(myself.object); + } + if (wardrobe) { + wardrobe.updateSelection(); + } + }; + + query = function () { + // answer true if my costume is the current one + var ide = myself.parentThatIsA(IDE_Morph); + + if (ide) { + return ide.currentSprite.costume === myself.object; + } + return false; + }; + + // additional properties: + this.object = aCostume || new Costume(); // mandatory, actually + this.version = this.object.version; + this.thumbnail = null; + + // initialize inherited properties: + CostumeIconMorph.uber.init.call( + this, + colors, // color overrides, <array>: [normal, highlight, pressed] + null, // target - not needed here + action, // a toggle function + this.object.name, // label string + query, // predicate/selector + null, // environment + null, // hint + aTemplate // optional, for cached background images + ); + + // override defaults and build additional components + this.isDraggable = true; + this.createThumbnail(); + this.padding = 2; + this.corner = 8; + this.fixLayout(); + this.fps = 1; +}; + +CostumeIconMorph.prototype.createThumbnail + = SpriteIconMorph.prototype.createThumbnail; + +CostumeIconMorph.prototype.createLabel + = SpriteIconMorph.prototype.createLabel; + +// CostumeIconMorph stepping + +CostumeIconMorph.prototype.step + = SpriteIconMorph.prototype.step; + +// CostumeIconMorph layout + +CostumeIconMorph.prototype.fixLayout + = SpriteIconMorph.prototype.fixLayout; + +// CostumeIconMorph menu + +CostumeIconMorph.prototype.userMenu = function () { + var menu = new MenuMorph(this); + if (!(this.object instanceof Costume)) {return null; } + menu.addItem("edit", 'editCostume'); + menu.addItem("rename", 'renameCostume'); + menu.addItem("delete", 'removeCostume'); + menu.addItem("export", 'exportCostume'); + return menu; +}; + +CostumeIconMorph.prototype.editCostume = function () { + var ide = this.parentThatIsA(IDE_Morph); + this.object.edit(this.world()); + ide.hasChangedMedia = true; +}; + +CostumeIconMorph.prototype.renameCostume = function () { + var costume = this.object, + ide = this.parentThatIsA(IDE_Morph); + (new DialogBoxMorph( + null, + function (answer) { + if (answer && (answer !== costume.name)) { + costume.name = answer; + costume.version = Date.now(); + ide.hasChangedMedia = true; + } + } + )).prompt( + 'rename costume', + costume.name, + this.world() + ); +}; + +CostumeIconMorph.prototype.removeCostume = function () { + var wardrobe = this.parentThatIsA(WardrobeMorph), + idx = this.parent.children.indexOf(this), + ide = this.parentThatIsA(IDE_Morph); + wardrobe.removeCostumeAt(idx - 1); + if (ide.currentSprite.costume === this.object) { + ide.currentSprite.wearCostume(null); + } +}; + +CostumeIconMorph.prototype.exportCostume = function () { + if (this.object instanceof SVG_Costume) { + window.open(this.object.contents.src); + } else { // rastered Costume + window.open(this.object.contents.toDataURL()); + } +}; + +// CostumeIconMorph drawing + +CostumeIconMorph.prototype.createBackgrounds + = SpriteIconMorph.prototype.createBackgrounds; + +// CostumeIconMorph drag & drop + +CostumeIconMorph.prototype.prepareToBeGrabbed = function () { + this.mouseClickLeft(); // select me + this.removeCostume(); +}; + +// TurtleIconMorph //////////////////////////////////////////////////// + +/* + I am a selectable element in the SpriteEditor's "Costumes" tab, keeping + a thumbnail of the sprite's or stage's default "Turtle" costume. +*/ + +// TurtleIconMorph inherits from ToggleButtonMorph (Widgets) +// ... and copies methods from SpriteIconMorph + +TurtleIconMorph.prototype = new ToggleButtonMorph(); +TurtleIconMorph.prototype.constructor = TurtleIconMorph; +TurtleIconMorph.uber = ToggleButtonMorph.prototype; + +// TurtleIconMorph settings + +TurtleIconMorph.prototype.thumbSize = new Point(80, 60); +TurtleIconMorph.prototype.labelShadowOffset = null; +TurtleIconMorph.prototype.labelShadowColor = null; +TurtleIconMorph.prototype.labelColor = new Color(255, 255, 255); +TurtleIconMorph.prototype.fontSize = 9; + +// TurtleIconMorph instance creation: + +function TurtleIconMorph(aSpriteOrStage, aTemplate) { + this.init(aSpriteOrStage, aTemplate); +} + +TurtleIconMorph.prototype.init = function (aSpriteOrStage, aTemplate) { + var colors, action, query, myself = this; + + if (!aTemplate) { + colors = [ + IDE_Morph.prototype.groupColor, + IDE_Morph.prototype.frameColor, + IDE_Morph.prototype.frameColor + ]; + + } + + action = function () { + // make my costume the current one + var ide = myself.parentThatIsA(IDE_Morph), + wardrobe = myself.parentThatIsA(WardrobeMorph); + + if (ide) { + ide.currentSprite.wearCostume(null); + } + if (wardrobe) { + wardrobe.updateSelection(); + } + }; + + query = function () { + // answer true if my costume is the current one + var ide = myself.parentThatIsA(IDE_Morph); + + if (ide) { + return ide.currentSprite.costume === null; + } + return false; + }; + + // additional properties: + this.object = aSpriteOrStage; // mandatory, actually + this.version = this.object.version; + this.thumbnail = null; + + // initialize inherited properties: + TurtleIconMorph.uber.init.call( + this, + colors, // color overrides, <array>: [normal, highlight, pressed] + null, // target - not needed here + action, // a toggle function + 'default', // label string + query, // predicate/selector + null, // environment + null, // hint + aTemplate // optional, for cached background images + ); + + // override defaults and build additional components + this.isDraggable = false; + this.createThumbnail(); + this.padding = 2; + this.corner = 8; + this.fixLayout(); +}; + +TurtleIconMorph.prototype.createThumbnail = function () { + if (this.thumbnail) { + this.thumbnail.destroy(); + } + if (this.object instanceof SpriteMorph) { + this.thumbnail = new SymbolMorph( + 'turtle', + this.thumbSize.y, + this.labelColor, + new Point(-1, -1), + new Color(0, 0, 0) + ); + } else { + this.thumbnail = new SymbolMorph( + 'stage', + this.thumbSize.y, + this.labelColor, + new Point(-1, -1), + new Color(0, 0, 0) + ); + } + this.add(this.thumbnail); +}; + +TurtleIconMorph.prototype.createLabel = function () { + var txt; + + if (this.label) { + this.label.destroy(); + } + txt = new StringMorph( + localize( + this.object instanceof SpriteMorph ? 'Turtle' : 'Empty' + ), + this.fontSize, + this.fontStyle, + true, + false, + false, + this.labelShadowOffset, + this.labelShadowColor, + this.labelColor + ); + + this.label = new FrameMorph(); + this.label.acceptsDrops = false; + this.label.alpha = 0; + this.label.setExtent(txt.extent()); + txt.setPosition(this.label.position()); + this.label.add(txt); + this.add(this.label); +}; + +// TurtleIconMorph layout + +TurtleIconMorph.prototype.fixLayout + = SpriteIconMorph.prototype.fixLayout; + +// TurtleIconMorph drawing + +TurtleIconMorph.prototype.createBackgrounds + = SpriteIconMorph.prototype.createBackgrounds; + +// TurtleIconMorph user menu + +TurtleIconMorph.prototype.userMenu = function () { + var myself = this, + menu = new MenuMorph(this, 'pen'), + on = '\u25CF', + off = '\u25CB'; + if (this.object instanceof StageMorph) { + return null; + } + menu.addItem( + (this.object.penPoint === 'tip' ? on : off) + ' ' + localize('tip'), + function () { + myself.object.penPoint = 'tip'; + myself.object.changed(); + myself.object.drawNew(); + myself.object.changed(); + } + ); + menu.addItem( + (this.object.penPoint === 'middle' ? on : off) + ' ' + localize( + 'middle' + ), + function () { + myself.object.penPoint = 'middle'; + myself.object.changed(); + myself.object.drawNew(); + myself.object.changed(); + } + ); + return menu; +}; + +// WardrobeMorph /////////////////////////////////////////////////////// + +// I am a watcher on a sprite's costume list + +// WardrobeMorph inherits from ScrollFrameMorph + +WardrobeMorph.prototype = new ScrollFrameMorph(); +WardrobeMorph.prototype.constructor = WardrobeMorph; +WardrobeMorph.uber = ScrollFrameMorph.prototype; + +// WardrobeMorph settings + +// ... to follow ... + +// WardrobeMorph instance creation: + +function WardrobeMorph(aSprite, sliderColor) { + this.init(aSprite, sliderColor); +} + +WardrobeMorph.prototype.init = function (aSprite, sliderColor) { + // additional properties + this.sprite = aSprite || new SpriteMorph(); + this.costumesVersion = null; + this.spriteVersion = null; + + // initialize inherited properties + WardrobeMorph.uber.init.call(this, null, null, sliderColor); + + // configure inherited properties + this.fps = 2; + this.updateList(); +}; + +// Wardrobe updating + +WardrobeMorph.prototype.updateList = function () { + var myself = this, + x = this.left() + 5, + y = this.top() + 5, + padding = 4, + oldFlag = Morph.prototype.trackChanges, + oldPos = this.contents.position(), + icon, + template, + txt; + + this.changed(); + oldFlag = Morph.prototype.trackChanges; + Morph.prototype.trackChanges = false; + + this.contents.destroy(); + this.contents = new FrameMorph(this); + this.contents.acceptsDrops = false; + this.contents.reactToDropOf = function (icon) { + myself.reactToDropOf(icon); + }; + this.addBack(this.contents); + + icon = new TurtleIconMorph(this.sprite); + icon.setPosition(new Point(x, y)); + myself.addContents(icon); + y = icon.bottom() + padding; + + txt = new TextMorph(localize( + 'costumes tab help' // look up long string in translator + )); + txt.fontSize = 9; + txt.setColor(new Color(230, 230, 230)); + txt.setPosition(new Point(x, y)); + 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)); + myself.addContents(icon); + y = icon.bottom() + padding; + }); + this.costumesVersion = this.sprite.costumes.lastChanged; + + this.contents.setPosition(oldPos); + this.adjustScrollBars(); + Morph.prototype.trackChanges = oldFlag; + this.changed(); + + this.updateSelection(); +}; + +WardrobeMorph.prototype.updateSelection = function () { + this.contents.children.forEach(function (morph) { + if (morph.refresh) {morph.refresh(); } + }); + this.spriteVersion = this.sprite.version; +}; + +// Wardrobe stepping + +WardrobeMorph.prototype.step = function () { + if (this.costumesVersion !== this.sprite.costumes.lastChanged) { + this.updateList(); + } + if (this.spriteVersion !== this.sprite.version) { + this.updateSelection(); + } +}; + +// Wardrobe ops + +WardrobeMorph.prototype.removeCostumeAt = function (idx) { + this.sprite.costumes.remove(idx); + this.updateList(); +}; + +// Wardrobe drag & drop + +WardrobeMorph.prototype.wantsDropOf = function (morph) { + return morph instanceof CostumeIconMorph; +}; + +WardrobeMorph.prototype.reactToDropOf = function (icon) { + var idx = 0, + costume = icon.object, + top = icon.top(); + + icon.destroy(); + this.contents.children.forEach(function (item) { + if (item instanceof CostumeIconMorph && item.top() < top - 4) { + idx += 1; + } + }); + this.sprite.costumes.add(costume, idx + 1); + this.updateList(); +}; + +// SoundIconMorph /////////////////////////////////////////////////////// + +/* + I am an element in the SpriteEditor's "Sounds" tab. +*/ + +// SoundIconMorph inherits from ToggleButtonMorph (Widgets) +// ... and copies methods from SpriteIconMorph + +SoundIconMorph.prototype = new ToggleButtonMorph(); +SoundIconMorph.prototype.constructor = SoundIconMorph; +SoundIconMorph.uber = ToggleButtonMorph.prototype; + +// SoundIconMorph settings + +SoundIconMorph.prototype.thumbSize = new Point(80, 60); +SoundIconMorph.prototype.labelShadowOffset = null; +SoundIconMorph.prototype.labelShadowColor = null; +SoundIconMorph.prototype.labelColor = new Color(255, 255, 255); +SoundIconMorph.prototype.fontSize = 9; + +// SoundIconMorph instance creation: + +function SoundIconMorph(aSound, aTemplate) { + this.init(aSound, aTemplate); +} + +SoundIconMorph.prototype.init = function (aSound, aTemplate) { + var colors, action, query; + + if (!aTemplate) { + colors = [ + IDE_Morph.prototype.groupColor, + IDE_Morph.prototype.frameColor, + IDE_Morph.prototype.frameColor + ]; + + } + + action = function () { + nop(); // When I am selected (which is never the case for sounds) + }; + + query = function () { + return false; + }; + + // additional properties: + this.object = aSound; // mandatory, actually + this.version = this.object.version; + this.thumbnail = null; + + // initialize inherited properties: + SoundIconMorph.uber.init.call( + this, + colors, // color overrides, <array>: [normal, highlight, pressed] + null, // target - not needed here + action, // a toggle function + this.object.name, // label string + query, // predicate/selector + null, // environment + null, // hint + aTemplate // optional, for cached background images + ); + + // override defaults and build additional components + this.isDraggable = true; + this.createThumbnail(); + this.padding = 2; + this.corner = 8; + this.fixLayout(); + this.fps = 1; +}; + +SoundIconMorph.prototype.createThumbnail = function () { + var label, btnColor, btnLabelColor; + if (this.thumbnail) { + this.thumbnail.destroy(); + } + this.thumbnail = new Morph(); + this.thumbnail.setExtent(this.thumbSize); + this.add(this.thumbnail); + label = new StringMorph( + this.createInfo(), + '16', + '', + true, + false, + false, + this.labelShadowOffset, + this.labelShadowColor, + new Color(200, 200, 200) + ); + this.thumbnail.add(label); + label.setCenter(new Point(40, 15)); + + this.button = new PushButtonMorph( + this, + 'toggleAudioPlaying', + (this.object.previewAudio ? 'Stop' : 'Play') + ); + btnLabelColor = new Color(110, 100, 110); + btnColor = new Color(220, 220, 220); + this.button.drawNew(); + this.button.hint = 'Play sound'; + this.button.fixLayout(); + this.thumbnail.add(this.button); + this.button.setCenter(new Point(40, 40)); +}; + +SoundIconMorph.prototype.createInfo = function () { + var dur = Math.round(this.object.audio.duration || 0), + mod = dur % 60; + return Math.floor(dur / 60).toString() + + ":" + + (mod < 10 ? "0" : "") + + mod.toString(); +}; + +SoundIconMorph.prototype.toggleAudioPlaying = function () { + var myself = this; + if (!this.object.previewAudio) { + //Audio is not playing + this.button.labelString = 'Stop'; + this.button.hint = 'Stop sound'; + this.object.previewAudio = this.object.play(); + this.object.previewAudio.addEventListener('ended', function () { + myself.audioHasEnded(); + }, false); + } else { + //Audio is currently playing + this.button.labelString = 'Play'; + this.button.hint = 'Play sound'; + this.object.previewAudio.pause(); + this.object.previewAudio.terminated = true; + this.object.previewAudio = null; + } + this.button.createLabel(); +}; + +SoundIconMorph.prototype.audioHasEnded = function () { + this.button.trigger(); + this.button.mouseLeave(); +}; + +SoundIconMorph.prototype.createLabel + = SpriteIconMorph.prototype.createLabel; + +// SoundIconMorph stepping + +/* +SoundIconMorph.prototype.step + = SpriteIconMorph.prototype.step; +*/ + +// SoundIconMorph layout + +SoundIconMorph.prototype.fixLayout + = SpriteIconMorph.prototype.fixLayout; + +// SoundIconMorph menu + +SoundIconMorph.prototype.userMenu = function () { + var menu = new MenuMorph(this); + if (!(this.object instanceof Sound)) { return null; } + menu.addItem('rename', 'renameSound'); + menu.addItem('delete', 'removeSound'); + return menu; +}; + + +SoundIconMorph.prototype.renameSound = function () { + var sound = this.object, + ide = this.parentThatIsA(IDE_Morph), + myself = this; + (new DialogBoxMorph( + null, + function (answer) { + if (answer && (answer !== sound.name)) { + sound.name = answer; + sound.version = Date.now(); + myself.createLabel(); // can be omitted once I'm stepping + myself.fixLayout(); // can be omitted once I'm stepping + ide.hasChangedMedia = true; + } + } + )).prompt( + 'rename sound', + sound.name, + this.world() + ); +}; + +SoundIconMorph.prototype.removeSound = function () { + var jukebox = this.parentThatIsA(JukeboxMorph), + idx = this.parent.children.indexOf(this); + jukebox.removeSound(idx); +}; + +SoundIconMorph.prototype.createBackgrounds + = SpriteIconMorph.prototype.createBackgrounds; + +SoundIconMorph.prototype.createLabel + = SpriteIconMorph.prototype.createLabel; + +// SoundIconMorph drag & drop + +SoundIconMorph.prototype.prepareToBeGrabbed = function () { + this.removeSound(); +}; + +// JukeboxMorph ///////////////////////////////////////////////////// + +/* + I am JukeboxMorph, like WardrobeMorph, but for sounds +*/ + +// JukeboxMorph instance creation + +JukeboxMorph.prototype = new ScrollFrameMorph(); +JukeboxMorph.prototype.constructor = JukeboxMorph; +JukeboxMorph.uber = ScrollFrameMorph.prototype; + +function JukeboxMorph(aSprite, sliderColor) { + this.init(aSprite, sliderColor); +} + +JukeboxMorph.prototype.init = function (aSprite, sliderColor) { + // additional properties + this.sprite = aSprite || new SpriteMorph(); + this.costumesVersion = null; + this.spriteVersion = null; + + // initialize inherited properties + JukeboxMorph.uber.init.call(this, null, null, sliderColor); + + // configure inherited properties + this.acceptsDrops = false; + this.fps = 2; + this.updateList(); +}; + +// Jukebox updating + +JukeboxMorph.prototype.updateList = function () { + var myself = this, + x = this.left() + 5, + y = this.top() + 5, + padding = 4, + oldFlag = Morph.prototype.trackChanges, + icon, + template, + txt; + + this.changed(); + oldFlag = Morph.prototype.trackChanges; + Morph.prototype.trackChanges = false; + + this.contents.destroy(); + this.contents = new FrameMorph(this); + this.contents.acceptsDrops = false; + this.contents.reactToDropOf = function (icon) { + myself.reactToDropOf(icon); + }; + this.addBack(this.contents); + + txt = new TextMorph(localize( + 'import a sound from your computer\nby dragging it into here' + )); + txt.fontSize = 9; + txt.setColor(new Color(230, 230, 230)); + txt.setPosition(new Point(x, y)); + this.addContents(txt); + y = txt.bottom() + padding; + + this.sprite.sounds.asArray().forEach(function (sound) { + template = icon = new SoundIconMorph(sound, template); + icon.setPosition(new Point(x, y)); + myself.addContents(icon); + y = icon.bottom() + padding; + }); + + Morph.prototype.trackChanges = oldFlag; + this.changed(); + + this.updateSelection(); +}; + +JukeboxMorph.prototype.updateSelection = function () { + this.contents.children.forEach(function (morph) { + if (morph.refresh) {morph.refresh(); } + }); + this.spriteVersion = this.sprite.version; +}; + +// Jukebox stepping + +/* +JukeboxMorph.prototype.step = function () { + if (this.spriteVersion !== this.sprite.version) { + this.updateSelection(); + } +}; +*/ + +// Jukebox ops + +JukeboxMorph.prototype.removeSound = function (idx) { + this.sprite.sounds.remove(idx); + this.updateList(); +}; + +// Jukebox drag & drop + +JukeboxMorph.prototype.wantsDropOf = function (morph) { + return morph instanceof SoundIconMorph; +}; + +JukeboxMorph.prototype.reactToDropOf = function (icon) { + var idx = 0, + costume = icon.object, + top = icon.top(); + + icon.destroy(); + this.contents.children.forEach(function (item) { + if (item.top() < top - 4) { + idx += 1; + } + }); + this.sprite.sounds.add(costume, idx); + this.updateList(); +};
\ No newline at end of file |
