summaryrefslogtreecommitdiff
path: root/gui.js
diff options
context:
space:
mode:
authorjmoenig <jens@moenig.org>2013-03-16 09:02:16 +0100
committerjmoenig <jens@moenig.org>2013-03-16 09:02:16 +0100
commit0b510366d230399a38f18a8db92dd396a0dad42e (patch)
treeffb841d64bca04799ebc470701b71e59e726017e /gui.js
downloadsnap-0b510366d230399a38f18a8db92dd396a0dad42e.tar.gz
snap-0b510366d230399a38f18a8db92dd396a0dad42e.zip
Creation
Diffstat (limited to 'gui.js')
-rw-r--r--gui.js4991
1 files changed, 4991 insertions, 0 deletions
diff --git a/gui.js b/gui.js
new file mode 100644
index 0000000..6056277
--- /dev/null
+++ b/gui.js
@@ -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