/*
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) 2022 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 .
prerequisites:
--------------
needs blocks.js, threads.js, objects.js, cloud.jus 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
LibraryImportDialogMorph
SpriteIconMorph
TurtleIconMorph
CostumeIconMorph
WardrobeMorph
SoundIconMorph
JukeboxMorph
SceneIconMorph
SceneAlbumMorph
StageHandleMorph
PaletteHandleMorph
CamSnapshotDialogMorph
SoundRecorderDialogMorph
credits
-------
Nathan Dinsmore contributed saving and loading of projects,
ypr-Snap! project conversion and countless bugfixes
Ian Reynolds contributed handling and visualization of sounds
Michael Ball contributed the LibraryImportDialogMorph and countless
utilities to load libraries from relative urls
Bernat Romagosa contributed more things than I can mention,
including interfacing to the camera and microphone
*/
/*global modules, Morph, SpriteMorph, SyntaxElementMorph, Color, Cloud, Audio,
ListWatcherMorph, TextMorph, newCanvas, useBlurredShadows, Sound, Scene, Note,
StringMorph, Point, MenuMorph, morphicVersion, DialogBoxMorph, BlockEditorMorph,
ToggleButtonMorph, contains, ScrollFrameMorph, StageMorph, PushButtonMorph, sb,
InputFieldMorph, FrameMorph, Process, nop, SnapSerializer, ListMorph, detect,
AlignmentMorph, TabMorph, Costume, MorphicPreferences,BlockMorph, ToggleMorph,
InputSlotDialogMorph, ScriptsMorph, isNil, SymbolMorph, fontHeight, localize,
BlockExportDialogMorph, BlockImportDialogMorph, SnapTranslator, List, ArgMorph,
Uint8Array, HandleMorph, SVG_Costume, TableDialogMorph, CommentMorph, saveAs,
CommandBlockMorph, BooleanSlotMorph, RingReporterSlotMorph, ScriptFocusMorph,
BlockLabelPlaceHolderMorph, SpeechBubbleMorph, XML_Element, WatcherMorph, WHITE,
BlockRemovalDialogMorph,TableMorph, isSnapObject, isRetinaEnabled, SliderMorph,
disableRetinaSupport, enableRetinaSupport, isRetinaSupported, MediaRecorder,
Animation, BoxMorph, BlockDialogMorph, RingMorph, Project, ZERO, BLACK,
BlockVisibilityDialogMorph, ThreadManager*/
/*jshint esversion: 6*/
// Global stuff ////////////////////////////////////////////////////////
modules.gui = '2022-February-08';
// Declarations
var IDE_Morph;
var ProjectDialogMorph;
var LibraryImportDialogMorph;
var SpriteIconMorph;
var CostumeIconMorph;
var TurtleIconMorph;
var WardrobeMorph;
var SoundIconMorph;
var JukeboxMorph;
var SceneIconMorph;
var SceneAlbumMorph;
var StageHandleMorph;
var PaletteHandleMorph;
var CamSnapshotDialogMorph;
var SoundRecorderDialogMorph;
// 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 and skins
IDE_Morph.prototype.setDefaultDesign = function () {
MorphicPreferences.isFlat = false;
SpriteMorph.prototype.paletteColor = new Color(30, 30, 30);
SpriteMorph.prototype.paletteTextColor = new Color(230, 230, 230);
StageMorph.prototype.paletteTextColor
= SpriteMorph.prototype.paletteTextColor;
StageMorph.prototype.paletteColor = SpriteMorph.prototype.paletteColor;
SpriteMorph.prototype.sliderColor
= SpriteMorph.prototype.paletteColor.lighter(30);
IDE_Morph.prototype.buttonContrast = 30;
IDE_Morph.prototype.backgroundColor = new Color(10, 10, 10);
IDE_Morph.prototype.frameColor = SpriteMorph.prototype.paletteColor;
IDE_Morph.prototype.groupColor
= SpriteMorph.prototype.paletteColor.lighter(5);
IDE_Morph.prototype.sliderColor = SpriteMorph.prototype.sliderColor;
IDE_Morph.prototype.buttonLabelColor = WHITE;
IDE_Morph.prototype.tabColors = [
IDE_Morph.prototype.groupColor.darker(50),
IDE_Morph.prototype.groupColor.darker(25),
IDE_Morph.prototype.groupColor
];
IDE_Morph.prototype.rotationStyleColors = IDE_Morph.prototype.tabColors;
IDE_Morph.prototype.appModeColor = BLACK;
IDE_Morph.prototype.scriptsPaneTexture = this.scriptsTexture();
IDE_Morph.prototype.padding = 1;
SpriteIconMorph.prototype.labelColor
= IDE_Morph.prototype.buttonLabelColor;
CostumeIconMorph.prototype.labelColor
= IDE_Morph.prototype.buttonLabelColor;
SoundIconMorph.prototype.labelColor
= IDE_Morph.prototype.buttonLabelColor;
TurtleIconMorph.prototype.labelColor
= IDE_Morph.prototype.buttonLabelColor;
SceneIconMorph.prototype.labelColor
= IDE_Morph.prototype.buttonLabelColor;
SyntaxElementMorph.prototype.contrast = 65;
ScriptsMorph.prototype.feedbackColor = WHITE;
};
IDE_Morph.prototype.setFlatDesign = function () {
MorphicPreferences.isFlat = true;
SpriteMorph.prototype.paletteColor = WHITE;
SpriteMorph.prototype.paletteTextColor = new Color(70, 70, 70);
StageMorph.prototype.paletteTextColor
= SpriteMorph.prototype.paletteTextColor;
StageMorph.prototype.paletteColor = SpriteMorph.prototype.paletteColor;
SpriteMorph.prototype.sliderColor = SpriteMorph.prototype.paletteColor;
IDE_Morph.prototype.buttonContrast = 30;
IDE_Morph.prototype.backgroundColor = new Color(220, 220, 230);
IDE_Morph.prototype.frameColor = new Color(240, 240, 245);
IDE_Morph.prototype.groupColor = WHITE;
IDE_Morph.prototype.sliderColor = SpriteMorph.prototype.sliderColor;
IDE_Morph.prototype.buttonLabelColor = new Color(70, 70, 70);
IDE_Morph.prototype.tabColors = [
IDE_Morph.prototype.frameColor,
IDE_Morph.prototype.frameColor.lighter(50),
IDE_Morph.prototype.groupColor
];
IDE_Morph.prototype.rotationStyleColors = IDE_Morph.prototype.tabColors;
IDE_Morph.prototype.appModeColor = IDE_Morph.prototype.frameColor;
IDE_Morph.prototype.scriptsPaneTexture = null;
IDE_Morph.prototype.padding = 1;
SpriteIconMorph.prototype.labelColor
= IDE_Morph.prototype.buttonLabelColor;
CostumeIconMorph.prototype.labelColor
= IDE_Morph.prototype.buttonLabelColor;
SoundIconMorph.prototype.labelColor
= IDE_Morph.prototype.buttonLabelColor;
TurtleIconMorph.prototype.labelColor
= IDE_Morph.prototype.buttonLabelColor;
SceneIconMorph.prototype.labelColor
= IDE_Morph.prototype.buttonLabelColor;
SyntaxElementMorph.prototype.contrast = 25;
ScriptsMorph.prototype.feedbackColor = new Color(153, 255, 213);
};
IDE_Morph.prototype.scriptsTexture = function () {
var pic = newCanvas(new Point(100, 100)), // bigger scales faster
ctx = pic.getContext('2d'),
i;
for (i = 0; i < 100; i += 4) {
ctx.fillStyle = this.frameColor.toString();
ctx.fillRect(i, 0, 1, 100);
ctx.fillStyle = this.groupColor.lighter(2).toString();
ctx.fillRect(i + 1, 0, 1, 100);
ctx.fillRect(i + 3, 0, 1, 100);
ctx.fillStyle = this.groupColor.darker(2).toString();
ctx.fillRect(i + 2, 0, 1, 100);
}
return pic;
};
IDE_Morph.prototype.setDefaultDesign();
// IDE_Morph instance creation:
function IDE_Morph(isAutoFill) {
this.init(isAutoFill);
}
IDE_Morph.prototype.init = function (isAutoFill) {
// global font setting
MorphicPreferences.globalFontFamily = 'Helvetica, Arial';
// restore saved user preferences
this.userLanguage = null; // user language preference for startup
this.applySavedSettings();
// additional properties:
this.cloud = new Cloud();
this.cloudMsg = null;
this.source = null;
this.serializer = new SnapSerializer();
// scenes
this.scenes = new List([new Scene()]);
this.scene = this.scenes.at(1);
this.isAddingScenes = false;
this.isAddingNextScene = false;
// editor
this.globalVariables = this.scene.globalVariables;
this.currentSprite = this.scene.addDefaultSprite();
this.sprites = this.scene.sprites;
this.currentCategory = this.scene.unifiedPalette ? 'unified' : 'motion';
this.currentTab = 'scripts';
// logoURL is disabled because the image data is hard-copied
// to avoid tainting the world canvas
// this.logoURL = this.resourceURL('src', 'snap_logo_sm.png');
this.logo = null;
this.controlBar = null;
this.categories = null;
this.palette = null;
this.paletteHandle = null;
this.spriteBar = null;
this.spriteEditor = null;
this.stage = null;
this.stageHandle = null;
this.corralBar = null;
this.corral = null;
this.embedPlayButton = null;
this.embedOverlay = null;
this.isEmbedMode = false;
this.isAutoFill = isAutoFill === undefined ? true : isAutoFill;
this.isAppMode = false;
this.isSmallStage = false;
this.filePicker = null;
// incrementally saving projects to the cloud is currently unused
// and needs to be extended to work with scenes before reactivation
this.hasChangedMedia = false;
this.isAnimating = true;
this.paletteWidth = 200; // initially same as logo width
this.stageRatio = 1; // for IDE animations, e.g. when zooming
this.wasSingleStepping = false; // for toggling to and from app mode
this.loadNewProject = false; // flag when starting up translated
this.shield = null;
this.savingPreferences = true; // for bh's infamous "Eisenbergification"
this.bulkDropInProgress = false; // for handling multiple file-drops
this.cachedSceneFlag = null; // for importing multiple scenes at once
// initialize inherited properties:
IDE_Morph.uber.init.call(this);
// override inherited properites:
this.color = this.backgroundColor;
};
IDE_Morph.prototype.openIn = function (world) {
var hash, myself = this;
function initUser(username) {
sessionStorage.username = username;
myself.controlBar.cloudButton.refresh();
if (username) {
myself.source = 'cloud';
if (!myself.cloud.verified) {
new DialogBoxMorph().inform(
'Unverified account',
'Your account is still unverified.\n' +
'Please use the verification link that\n' +
'was sent to your email address when you\n' +
'signed up.\n\n' +
'If you cannot find that email, please\n' +
'check your spam folder. If you still\n' +
'cannot find it, please use the "Resend\n' +
'Verification Email..." option in the cloud\n' +
'menu.',
world,
myself.cloudIcon(null, new Color(0, 180, 0))
);
}
}
}
this.buildPanes();
world.add(this);
world.userMenu = this.userMenu;
// override SnapCloud's user message with Morphic
this.cloud.message = (string) => {
var m = new MenuMorph(null, string),
intervalHandle;
m.popUpCenteredInWorld(world);
intervalHandle = setInterval(() => {
m.destroy();
clearInterval(intervalHandle);
}, 2000);
};
// prevent non-DialogBoxMorphs from being dropped
// onto the World in user-mode
world.reactToDropOf = (morph) => {
if (!(morph instanceof DialogBoxMorph ||
(morph instanceof MenuMorph))) {
if (world.hand.grabOrigin) {
morph.slideBackTo(world.hand.grabOrigin);
} else {
world.hand.grab(morph);
}
}
};
this.reactToWorldResize(world.bounds);
function applyFlags(dict) {
if (dict.noCloud) {
myself.cloud.disable();
}
if (dict.embedMode) {
myself.setEmbedMode();
}
if (dict.editMode) {
myself.toggleAppMode(false);
} else {
myself.toggleAppMode(true);
}
if (!dict.noRun) {
autoRun();
}
if (dict.hideControls) {
myself.controlBar.hide();
window.onbeforeunload = nop;
}
if (dict.noExitWarning) {
window.onbeforeunload = nop;
}
if (dict.blocksZoom) {
myself.savingPreferences = false;
myself.setBlocksScale(Math.max(1,Math.min(dict.blocksZoom, 12)));
myself.savingPreferences = true;
}
// only force my world to get focus if I'm not in embed mode
// to prevent the iFrame from involuntarily scrolling into view
if (!myself.isEmbedMode) {
world.worldCanvas.focus();
}
}
function autoRun () {
// wait until all costumes and sounds are loaded
if (isLoadingAssets()) {
myself.world().animations.push(
new Animation(nop, nop, 0, 200, nop, autoRun)
);
} else {
myself.runScripts();
}
}
function isLoadingAssets() {
return myself.sprites.asArray().concat([myself.stage]).some(any =>
(any.costume ? any.costume.loaded !== true : false) ||
any.costumes.asArray().some(each => each.loaded !== true) ||
any.sounds.asArray().some(each => each.loaded !== true)
);
}
// dynamic notifications from non-source text files
// has some issues, commented out for now
/*
this.cloudMsg = getURL('https://snap.berkeley.edu/cloudmsg.txt');
motd = getURL('https://snap.berkeley.edu/motd.txt');
if (motd) {
this.inform('Snap!', motd);
}
*/
function interpretUrlAnchors() {
var dict, idx;
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(each =>
hash.substr(0, 8).indexOf(each)
),
1
)) {
this.droppedText(hash);
} else {
idx = hash.indexOf("&");
if (idx > 0) {
dict = myself.cloud.parseDict(hash.substr(idx));
dict.editMode = true;
hash = hash.slice(0, idx);
applyFlags(dict);
}
this.shield = new Morph();
this.shield.alpha = 0;
this.shield.setExtent(this.parent.extent());
this.parent.add(this.shield);
this.showMessage('Fetching project...');
this.getURL(
hash,
projectData => {
var msg;
this.nextSteps([
() => msg = this.showMessage('Opening project...'),
() => {
if (projectData.indexOf(' {
this.shield.destroy();
this.shield = null;
msg.destroy();
this.toggleAppMode(false);
}
]);
}
);
}
} else if (location.hash.substr(0, 5) === '#run:') {
dict = '';
hash = location.hash.substr(5);
//decoding if hash is an encoded URI
if (hash.charAt(0) === '%'
|| hash.search(/\%(?:[0-9a-f]{2})/i) > -1) {
hash = decodeURIComponent(hash);
}
idx = hash.indexOf("&");
// supporting three URL cases
// xml project
if (hash.substr(0, 8) === '') + 10)
);
applyFlags(
myself.cloud.parseDict(
hash.substr(hash.indexOf('') + 10)
)
);
// no project, only flags
} else if (idx == 0){
applyFlags(myself.cloud.parseDict(hash));
// xml file path
// three path types allowed:
// (1) absolute (http...),
// (2) relative to site ("/path") or
// (3) relative to folder ("path")
} else {
this.shield = new Morph();
this.shield.alpha = 0;
this.shield.setExtent(this.parent.extent());
this.parent.add(this.shield);
this.showMessage('Fetching project...');
if (idx > 0) {
dict = myself.cloud.parseDict(hash.substr(idx));
hash = hash.slice(0,idx);
}
this.getURL(
hash,
projectData => {
var msg;
this.nextSteps([
() => msg = this.showMessage('Opening project...'),
() => {
if (projectData.indexOf(' {
this.shield.destroy();
this.shield = null;
msg.destroy();
// this.toggleAppMode(true);
applyFlags(dict);
}
]);
}
);
}
} else if (location.hash.substr(0, 9) === '#present:') {
this.shield = new Morph();
this.shield.color = this.color;
this.shield.setExtent(this.parent.extent());
this.parent.add(this.shield);
myself.showMessage('Fetching project\nfrom the cloud...');
// make sure to lowercase the username
dict = myself.cloud.parseDict(location.hash.substr(9));
dict.Username = dict.Username.toLowerCase();
myself.cloud.getPublicProject(
dict.ProjectName,
dict.Username,
projectData => {
var msg;
myself.nextSteps([
() => msg = myself.showMessage('Opening project...'),
() => {
if (projectData.indexOf(' {
myself.shield.destroy();
myself.shield = null;
msg.destroy();
applyFlags(dict);
}
]);
},
this.cloudError()
);
} else if (location.hash.substr(0, 7) === '#cloud:') {
this.shield = new Morph();
this.shield.alpha = 0;
this.shield.setExtent(this.parent.extent());
this.parent.add(this.shield);
myself.showMessage('Fetching project\nfrom the cloud...');
// make sure to lowercase the username
dict = myself.cloud.parseDict(location.hash.substr(7));
myself.cloud.getPublicProject(
dict.ProjectName,
dict.Username,
projectData => {
var msg;
myself.nextSteps([
() => msg = myself.showMessage('Opening project...'),
() => {
if (projectData.indexOf(' {
myself.shield.destroy();
myself.shield = null;
msg.destroy();
myself.toggleAppMode(false);
}
]);
},
this.cloudError()
);
} else if (location.hash.substr(0, 4) === '#dl:') {
myself.showMessage('Fetching project\nfrom the cloud...');
// make sure to lowercase the username
dict = myself.cloud.parseDict(location.hash.substr(4));
dict.Username = dict.Username.toLowerCase();
myself.cloud.getPublicProject(
dict.ProjectName,
dict.Username,
projectData => {
myself.saveXMLAs(projectData, dict.ProjectName);
myself.showMessage(
'Saved project\n' + dict.ProjectName,
2
);
},
this.cloudError()
);
} else if (location.hash.substr(0, 6) === '#lang:') {
dict = myself.cloud.parseDict(location.hash.substr(6));
applyFlags(dict);
} else if (location.hash.substr(0, 7) === '#signup') {
this.createCloudAccount();
}
this.loadNewProject = false;
}
function launcherLangSetting() {
var langSetting = null;
if (location.hash.substr(0, 6) === '#lang:') {
if (location.hash.charAt(8) === '_') {
langSetting = location.hash.slice(6,11);
} else {
langSetting = location.hash.slice(6,8);
}
}
// lang-flag wins lang-anchor setting
langSetting = myself.cloud.parseDict(location.hash).lang || langSetting;
return langSetting;
}
if (launcherLangSetting()) {
// launch with this non-persisten lang setting
this.loadNewProject = true;
this.setLanguage(launcherLangSetting(), interpretUrlAnchors, true);
} else if (this.userLanguage) {
this.loadNewProject = true;
this.setLanguage(this.userLanguage, interpretUrlAnchors);
} else {
interpretUrlAnchors.call(this);
}
if (location.protocol !== 'file:') {
if (!sessionStorage.username) {
// check whether login should persist across browser sessions
this.cloud.initSession(initUser);
} else {
// login only persistent during a single browser session
this.cloud.checkCredentials(initUser);
}
}
world.keyboardFocus = this.stage;
this.warnAboutIE();
};
// 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();
// the logo texture is not loaded dynamically as an image, but instead
// hard-copied here to avoid tainting the world canvas. This lets us
// use Snap's (and Morphic's) color pickers to sense any pixel which
// otherwise would be compromised by annoying browser security.
// this.logo.texture = this.logoURL; // original code, commented out
this.logo.texture = "data:image/png;base64," +
"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" +
"gg==";
this.logo.render = function (ctx) {
var gradient = ctx.createLinearGradient(
0,
0,
this.width(),
0
);
gradient.addColorStop(0, 'black');
gradient.addColorStop(0.5, myself.frameColor.toString());
ctx.fillStyle = MorphicPreferences.isFlat ?
myself.frameColor.toString() : gradient;
ctx.fillRect(0, 0, this.width(), this.height());
if (this.cachedTexture) {
this.renderCachedTexture(ctx);
} else if (this.texture) {
this.renderTexture(this.texture, ctx);
}
};
this.logo.renderCachedTexture = function (ctx) {
ctx.drawImage(
this.cachedTexture,
5,
Math.round((this.height() - this.cachedTexture.height) / 2)
);
this.changed();
};
this.logo.mouseClickLeft = function () {
myself.snapMenu();
};
this.logo.color = BLACK;
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,
slider,
stopButton,
pauseButton,
startButton,
projectButton,
settingsButton,
stageSizeButton,
appModeButton,
steppingButton,
cloudButton,
x,
colors = MorphicPreferences.isFlat ? this.tabColors
: [
this.groupColor,
this.frameColor.darker(50),
this.frameColor.darker(50)
],
activeColor = new Color(153, 255, 213),
activeColors = [
activeColor,
activeColor.lighter(40),
activeColor.lighter(40)
],
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
// let users manually enforce re-layout when changing orientation
// on mobile devices
this.controlBar.mouseClickLeft = function () {
this.world().fillPage();
};
this.add(this.controlBar);
//smallStageButton
button = new ToggleButtonMorph(
null, //colors,
this, // the IDE is the target
'toggleStageSize',
[
new SymbolMorph('smallStage', 14),
new SymbolMorph('normalStage', 14)
],
() => this.isSmallStage // query
);
button.hasNeutralBackground = true;
button.corner = 12;
button.color = colors[0];
button.highlightColor = colors[1];
button.pressColor = colors[0];
button.labelMinExtent = new Point(36, 18);
button.padding = 0;
button.labelShadowOffset = new Point(-1, -1);
button.labelShadowColor = colors[1];
button.labelColor = MorphicPreferences.isFlat ?
WHITE
: this.buttonLabelColor;
button.contrast = this.buttonContrast;
// button.hint = 'stage size\nsmall & normal';
button.fixLayout();
button.refresh();
stageSizeButton = button;
this.controlBar.add(stageSizeButton);
this.controlBar.stageSizeButton = button; // for refreshing
//appModeButton
button = new ToggleButtonMorph(
null, //colors,
this, // the IDE is the target
'toggleAppMode',
[
new SymbolMorph('fullScreen', 14),
new SymbolMorph('normalScreen', 14)
],
() => this.isAppMode // query
);
button.hasNeutralBackground = true;
button.corner = 12;
button.color = colors[0];
button.highlightColor = colors[1];
button.pressColor = colors[0];
button.labelMinExtent = new Point(36, 18);
button.padding = 0;
button.labelShadowOffset = new Point(-1, -1);
button.labelShadowColor = colors[1];
button.labelColor = this.buttonLabelColor;
button.contrast = this.buttonContrast;
// button.hint = 'app & edit\nmodes';
button.fixLayout();
button.refresh();
appModeButton = button;
this.controlBar.add(appModeButton);
this.controlBar.appModeButton = appModeButton; // for refreshing
//steppingButton
button = new ToggleButtonMorph(
null, //colors,
this, // the IDE is the target
'toggleSingleStepping',
[
new SymbolMorph('footprints', 16),
new SymbolMorph('footprints', 16)
],
() => Process.prototype.enableSingleStepping // query
);
button.corner = 12;
button.color = colors[0];
button.highlightColor = colors[1];
button.pressColor = activeColor;
button.labelMinExtent = new Point(36, 18);
button.padding = 0;
button.labelShadowOffset = new Point(-1, -1);
button.labelShadowColor = colors[1];
button.labelColor = this.buttonLabelColor;
button.contrast = this.buttonContrast;
button.hint = 'Visible stepping';
button.fixLayout();
button.refresh();
steppingButton = button;
this.controlBar.add(steppingButton);
this.controlBar.steppingButton = steppingButton; // for refreshing
// stopButton
button = new ToggleButtonMorph(
null, // colors
this, // the IDE is the target
'stopAllScripts',
[
new SymbolMorph('octagon', 14),
new SymbolMorph('square', 14)
],
() => this.stage ? // query
myself.stage.enableCustomHatBlocks &&
myself.stage.threads.pauseCustomHatBlocks
: true
);
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(
MorphicPreferences.isFlat ? 128 : 200,
0,
0
);
button.contrast = this.buttonContrast;
// button.hint = 'stop\nevery-\nthing';
button.fixLayout();
button.refresh();
stopButton = button;
this.controlBar.add(stopButton);
this.controlBar.stopButton = stopButton; // for refreshing
//pauseButton
button = new ToggleButtonMorph(
null, //colors,
this, // the IDE is the target
'togglePauseResume',
[
new SymbolMorph('pause', 12),
new SymbolMorph('pointRight', 14)
],
() => this.isPaused() // query
);
button.hasNeutralBackground = true;
button.corner = 12;
button.color = colors[0];
button.highlightColor = colors[1];
button.pressColor = colors[0];
button.labelMinExtent = new Point(36, 18);
button.padding = 0;
button.labelShadowOffset = new Point(-1, -1);
button.labelShadowColor = colors[1];
button.labelColor = MorphicPreferences.isFlat ?
new Color(220, 185, 0)
: new Color(255, 220, 0);
button.contrast = this.buttonContrast;
// button.hint = 'pause/resume\nall scripts';
button.fixLayout();
button.refresh();
pauseButton = button;
this.controlBar.add(pauseButton);
this.controlBar.pauseButton = pauseButton; // 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.fps = 4;
button.isActive = false;
button.step = function () {
var isRunning;
if (!myself.stage) {
return;
}
isRunning = !!myself.stage.threads.processes.length;
if (isRunning === this.isActive) {
return;
}
this.isActive = isRunning;
if (isRunning) {
this.color = activeColors[0];
this.highlightColor = activeColors[1];
this.pressColor = activeColors[2];
} else {
this.color = colors[0];
this.highlightColor = colors[1];
this.pressColor = colors[2];
}
this.rerender();
};
button.labelColor = new Color(
0,
MorphicPreferences.isFlat ? 100 : 200,
0
);
button.contrast = this.buttonContrast;
// button.hint = 'start green\nflag scripts';
button.fixLayout();
startButton = button;
this.controlBar.add(startButton);
this.controlBar.startButton = startButton;
// steppingSlider
slider = new SliderMorph(
61,
1,
Process.prototype.flashTime * 100 + 1,
6,
'horizontal'
);
slider.action = (num) => {
Process.prototype.flashTime = (num - 1) / 100;
this.controlBar.refreshResumeSymbol();
};
// slider.alpha = MorphicPreferences.isFlat ? 0.1 : 0.3;
slider.color = activeColor;
slider.alpha = 0.3;
slider.setExtent(new Point(50, 14));
this.controlBar.add(slider);
this.controlBar.steppingSlider = slider;
// 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 = this.buttonLabelColor;
button.contrast = this.buttonContrast;
// button.hint = 'open, save, & annotate project';
button.fixLayout();
projectButton = button;
this.controlBar.add(projectButton);
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 = this.buttonLabelColor;
button.contrast = this.buttonContrast;
// button.hint = 'edit settings';
button.fixLayout();
settingsButton = button;
this.controlBar.add(settingsButton);
this.controlBar.settingsButton = settingsButton; // for menu positioning
// cloudButton
button = new ToggleButtonMorph(
null, //colors,
this, // the IDE is the target
'cloudMenu',
[
new SymbolMorph('cloudOutline', 11),
new SymbolMorph('cloud', 11)
],
() => !isNil(this.cloud.username) // query
);
button.hasNeutralBackground = true;
button.corner = 12;
button.color = colors[0];
button.highlightColor = colors[1];
button.pressColor = colors[0];
button.labelMinExtent = new Point(36, 18);
button.padding = 0;
button.labelShadowOffset = new Point(-1, -1);
button.labelShadowColor = colors[1];
button.labelColor = this.buttonLabelColor;
button.contrast = this.buttonContrast;
// button.hint = 'cloud operations';
button.fixLayout();
button.refresh();
cloudButton = button;
this.controlBar.add(cloudButton);
this.controlBar.cloudButton = cloudButton; // for menu positioning & refresh
this.controlBar.fixLayout = function () {
x = this.right() - padding;
[stopButton, pauseButton, startButton].forEach(button => {
button.setCenter(myself.controlBar.center());
button.setRight(x);
x -= button.width();
x -= padding;
}
);
x = Math.min(
startButton.left() - (3 * padding + 2 * stageSizeButton.width()),
myself.right() - myself.stage.dimensions.x *
(myself.isSmallStage ? myself.stageRatio : 1)
);
[stageSizeButton, appModeButton].forEach(button => {
x += padding;
button.setCenter(myself.controlBar.center());
button.setLeft(x);
x += button.width();
}
);
slider.setCenter(myself.controlBar.center());
slider.setRight(stageSizeButton.left() - padding);
steppingButton.setCenter(myself.controlBar.center());
steppingButton.setRight(slider.left() - padding);
settingsButton.setCenter(myself.controlBar.center());
settingsButton.setLeft(this.left());
projectButton.setCenter(myself.controlBar.center());
if (myself.cloud.disabled) {
cloudButton.hide();
projectButton.setRight(settingsButton.left() - padding);
} else {
cloudButton.setCenter(myself.controlBar.center());
cloudButton.setRight(settingsButton.left() - padding);
projectButton.setRight(cloudButton.left() - padding);
}
this.refreshSlider();
this.updateLabel();
};
this.controlBar.refreshSlider = function () {
if (Process.prototype.enableSingleStepping && !myself.isAppMode) {
slider.fixLayout();
slider.rerender();
slider.show();
} else {
slider.hide();
}
this.refreshResumeSymbol();
};
this.controlBar.refreshResumeSymbol = function () {
var pauseSymbols;
if (Process.prototype.enableSingleStepping &&
Process.prototype.flashTime > 0.5) {
myself.stage.threads.pauseAll(myself.stage);
pauseSymbols = [
new SymbolMorph('pause', 12),
new SymbolMorph('stepForward', 14)
];
} else {
pauseSymbols = [
new SymbolMorph('pause', 12),
new SymbolMorph('pointRight', 14)
];
}
pauseButton.labelString = pauseSymbols;
pauseButton.createLabel();
pauseButton.fixLayout();
pauseButton.refresh();
};
this.controlBar.updateLabel = function () {
var prefix = myself.hasUnsavedEdits() ? '\u270E ' : '',
suffix = myself.world().isDevMode ?
' - ' + localize('development mode') : '',
name, scene, txt;
if (this.label) {
this.label.destroy();
}
if (myself.isAppMode) {
return;
}
scene = myself.scenes.at(1) !== myself.scene ?
' (' + myself.scene.name + ')' : '';
name = (myself.getProjectName() || localize('untitled'));
txt = new StringMorph(
prefix + name + scene + suffix,
14,
'sans-serif',
true,
false,
false,
MorphicPreferences.isFlat ? null : new Point(2, 1),
myself.frameColor.darker(myself.buttonContrast)
);
txt.color = myself.buttonLabelColor;
this.label = new FrameMorph();
this.label.acceptsDrops = false;
this.label.alpha = 0;
txt.setPosition(this.label.position());
this.label.add(txt);
this.label.setExtent(
new Point(
steppingButton.left() - settingsButton.right() - padding * 2,
txt.height()
)
);
this.label.setCenter(this.center());
this.label.setLeft(this.settingsButton.right() + padding);
this.add(this.label);
};
};
IDE_Morph.prototype.createCategories = function () {
var myself = this,
categorySelectionAction = this.scene.unifiedPalette ? scrollToCategory
: changePalette,
categoryQueryAction = this.scene.unifiedPalette ? queryTopCategory
: queryCurrentCategory;
if (this.categories) {
this.categories.destroy();
}
this.categories = new Morph();
this.categories.color = this.groupColor;
this.categories.bounds.setWidth(this.paletteWidth);
this.categories.buttons = [];
this.categories.refresh = function () {
this.buttons.forEach(cat => {
cat.refresh();
if (cat.state) {
cat.scrollIntoView();
}
});
};
this.categories.refreshEmpty = function () {
var dict = myself.currentSprite.emptyCategories();
dict.variables = dict.variables || dict.lists || dict.other;
this.buttons.forEach(cat => {
if (dict[cat.category]) {
cat.enable();
} else {
cat.disable();
}
});
};
function changePalette(category) {
return () => {
myself.currentCategory = category;
myself.categories.buttons.forEach(each =>
each.refresh()
);
myself.refreshPalette(true);
};
}
function scrollToCategory(category) {
return () => myself.scrollPaletteToCategory(category);
}
function queryCurrentCategory(category) {
return () => myself.currentCategory === category;
}
function queryTopCategory(category) {
return () => myself.topVisibleCategoryInPalette() === category;
}
function addCategoryButton(category) {
var labelWidth = 75,
colors = [
myself.frameColor,
myself.frameColor.darker(MorphicPreferences.isFlat ? 5 : 50),
SpriteMorph.prototype.blockColor[category]
],
button;
button = new ToggleButtonMorph(
colors,
myself, // the IDE is the target
categorySelectionAction(category),
category[0].toUpperCase().concat(category.slice(1)), // label
categoryQueryAction(category), // query
null, // env
null, // hint
labelWidth, // minWidth
true // has preview
);
button.category = category;
button.corner = 8;
button.padding = 0;
button.labelShadowOffset = new Point(-1, -1);
button.labelShadowColor = colors[1];
button.labelColor = myself.buttonLabelColor;
if (MorphicPreferences.isFlat) {
button.labelPressColor = WHITE;
}
button.fixLayout();
button.refresh();
myself.categories.add(button);
myself.categories.buttons.push(button);
return button;
}
function addCustomCategoryButton(category, color) {
var labelWidth = 168,
colors = [
myself.frameColor,
myself.frameColor.darker(MorphicPreferences.isFlat ? 5 : 50),
color
],
button;
button = new ToggleButtonMorph(
colors,
myself, // the IDE is the target
categorySelectionAction(category),
category, // label
categoryQueryAction(category), // query
null, // env
null, // hint
labelWidth, // minWidth
true // has preview
);
button.category = category;
button.corner = 8;
button.padding = 0;
button.labelShadowOffset = new Point(-1, -1);
button.labelShadowColor = colors[1];
button.labelColor = myself.buttonLabelColor;
if (MorphicPreferences.isFlat) {
button.labelPressColor = WHITE;
}
button.fixLayout();
button.refresh();
myself.categories.add(button);
myself.categories.buttons.push(button);
return button;
}
function fixCategoriesLayout() {
var buttonWidth = myself.categories.children[0].width(),
buttonHeight = myself.categories.children[0].height(),
more = SpriteMorph.prototype.customCategories.size,
border = 3,
xPadding = (200 // myself.logo.width()
- border
- buttonWidth * 2) / 3,
yPadding = 2,
l = myself.categories.left(),
t = myself.categories.top(),
scroller,
row,
col,
i;
myself.categories.children.forEach((button, i) => {
row = i < 8 ? i % 4 : i - 4;
col = (i < 4 || i > 7) ? 1 : 2;
button.setPosition(new Point(
l + (col * xPadding + ((col - 1) * buttonWidth)),
t + ((row + 1) * yPadding + (row * buttonHeight) + border) +
(i > 7 ? border + 2 : 0)
));
});
if (more > 6) {
scroller = new ScrollFrameMorph(null, null, myself.sliderColor);
scroller.setColor(myself.groupColor);
scroller.acceptsDrops = false;
scroller.contents.acceptsDrops = false;
scroller.setPosition(
new Point(0, myself.categories.children[8].top())
);
scroller.setWidth(myself.paletteWidth);
scroller.setHeight(buttonHeight * 6 + yPadding * 5);
for (i = 0; i < more; i += 1) {
scroller.addContents(myself.categories.children[8]);
}
myself.categories.add(scroller);
myself.categories.scroller = scroller;
myself.categories.setHeight(
(4 + 1) * yPadding
+ 4 * buttonHeight
+ 6 * (yPadding + buttonHeight) + border + 2
+ 2 * border
);
} else {
myself.categories.setHeight(
(4 + 1) * yPadding
+ 4 * buttonHeight
+ (more ?
(more * (yPadding + buttonHeight) + border + 2)
: 0)
+ 2 * border
);
}
}
SpriteMorph.prototype.categories.forEach(cat => {
if (!contains(['lists', 'other'], cat)) {
addCategoryButton(cat);
}
});
// sort alphabetically
Array.from(
SpriteMorph.prototype.customCategories.keys()
).sort().forEach(name =>
addCustomCategoryButton(
name,
SpriteMorph.prototype.customCategories.get(name)
)
);
fixCategoriesLayout();
this.add(this.categories);
};
IDE_Morph.prototype.createPalette = function (forSearching) {
// assumes that the logo pane has already been created
// needs the categories pane for layout
var myself = this,
vScrollAction;
if (this.palette) {
this.palette.destroy();
}
if (forSearching) {
this.palette = new ScrollFrameMorph(
null,
null,
this.currentSprite.sliderColor
);
this.palette.isForSearching = true;
// search toolbar (floating cancel button):
/* commented out for now
this.palette.toolBar = new PushButtonMorph(
this,
() => {
this.refreshPalette();
this.palette.adjustScrollBars();
},
new SymbolMorph("magnifierOutline", 16)
);
this.palette.toolBar.alpha = 0.2;
this.palette.toolBar.padding = 1;
// this.palette.toolBar.hint = 'Cancel';
this.palette.toolBar.labelShadowColor = new Color(140, 140, 140);
this.palette.toolBar.fixLayout();
this.palette.add(this.palette.toolBar);
*/
} else {
this.palette = this.currentSprite.palette(this.currentCategory);
}
this.palette.isDraggable = false;
this.palette.acceptsDrops = true;
this.palette.enableAutoScrolling = false;
this.palette.contents.acceptsDrops = false;
if (this.scene.unifiedPalette) {
this.palette.adjustScrollBars = function () {
ScrollFrameMorph.prototype.adjustScrollBars.call(this);
myself.categories.refresh();
};
vScrollAction = this.palette.vBar.action;
this.palette.vBar.action = function (num) {
vScrollAction(num);
myself.categories.buttons.forEach(each => each.refresh());
};
}
this.palette.reactToDropOf = (droppedMorph, hand) => {
if (droppedMorph instanceof DialogBoxMorph) {
this.world().add(droppedMorph);
} else if (droppedMorph instanceof SpriteMorph) {
this.removeSprite(droppedMorph);
} else if (droppedMorph instanceof SpriteIconMorph) {
droppedMorph.destroy();
this.removeSprite(droppedMorph.object);
} else if (droppedMorph instanceof CostumeIconMorph) {
// this.currentSprite.wearCostume(null); // do we need this?
droppedMorph.perish(myself.isAnimating ? 200 : 0);
} else if (droppedMorph instanceof BlockMorph) {
this.stage.threads.stopAllForBlock(droppedMorph);
if (hand && hand.grabOrigin.origin instanceof ScriptsMorph) {
hand.grabOrigin.origin.clearDropInfo();
hand.grabOrigin.origin.lastDroppedBlock = droppedMorph;
hand.grabOrigin.origin.recordDrop(hand.grabOrigin);
}
droppedMorph.perish(myself.isAnimating ? 200 : 0);
} else {
droppedMorph.perish(myself.isAnimating ? 200 : 0);
}
};
this.palette.contents.reactToDropOf = (droppedMorph) => {
// for "undrop" operation
if (droppedMorph instanceof BlockMorph) {
droppedMorph.destroy();
}
};
this.palette.setWidth(this.logo.width());
this.add(this.palette);
return this.palette;
};
IDE_Morph.prototype.createPaletteHandle = function () {
// assumes that the palette has already been created
if (this.paletteHandle) {this.paletteHandle.destroy(); }
this.paletteHandle = new PaletteHandleMorph(this.categories);
this.add(this.paletteHandle);
};
IDE_Morph.prototype.createStage = function () {
if (this.stage) {
this.stage.destroy();
}
this.add(this.scene.stage);
this.stage = this.scene.stage;
};
IDE_Morph.prototype.createStageHandle = function () {
// assumes that the stage has already been created
if (this.stageHandle) {this.stageHandle.destroy(); }
this.stageHandle = new StageHandleMorph(this.stage);
this.add(this.stageHandle);
};
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.tabColors,
tabBar = new AlignmentMorph('row', -tabCorner * 2),
tab,
symbols = [
new SymbolMorph('arrowRightThin', 10),
new SymbolMorph('turnAround', 10),
new SymbolMorph('arrowLeftRightThin', 10),
],
labels = ['don\'t rotate', 'can rotate', 'only face left/right'],
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 = myself.rotationStyleColors,
button;
button = new ToggleButtonMorph(
colors,
myself, // the IDE is the target
() => {
if (myself.currentSprite instanceof SpriteMorph) {
myself.currentSprite.rotationStyle = rotationStyle;
myself.currentSprite.changed();
myself.currentSprite.fixLayout();
myself.currentSprite.rerender();
myself.recordUnsavedChanges();
}
rotationStyleButtons.forEach(each =>
each.refresh()
);
},
symbols[rotationStyle], // label
() => myself.currentSprite instanceof SpriteMorph // query
&& myself.currentSprite.rotationStyle === rotationStyle,
null, // environment
localize(labels[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 = myself.buttonLabelColor;
button.fixLayout();
button.refresh();
rotationStyleButtons.push(button);
button.setPosition(myself.spriteBar.position().add(new Point(2, 4)));
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.isCachingImage = true;
thumbnail.bounds.setExtent(thumbSize);
thumbnail.cachedImage = 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.cachedImage = myself.currentSprite.thumbnail(
thumbSize,
thumbnail.cachedImage
);
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);
this.spriteBar.nameField = nameField;
nameField.fixLayout();
nameField.accept = function () {
var newName = nameField.getValue();
myself.currentSprite.setName(
myself.newSpriteName(newName, myself.currentSprite)
);
nameField.setContents(myself.currentSprite.name);
myself.recordUnsavedChanges();
};
this.spriteBar.reactToEdit = nameField.accept;
// padlock
padlock = new ToggleMorph(
'checkbox',
null,
() => {
this.currentSprite.isDraggable = !this.currentSprite.isDraggable;
this.recordUnsavedChanges();
},
localize('draggable'),
() => this.currentSprite.isDraggable
);
padlock.label.isBold = false;
padlock.label.setColor(this.buttonLabelColor);
padlock.color = tabColors[2];
padlock.highlightColor = tabColors[0];
padlock.pressColor = tabColors[1];
padlock.tick.shadowOffset = MorphicPreferences.isFlat ?
ZERO : new Point(-1, -1);
padlock.tick.shadowColor = BLACK;
padlock.tick.color = this.buttonLabelColor;
padlock.tick.isBold = false;
padlock.tick.fixLayout();
padlock.setPosition(nameField.bottomLeft().add(2));
padlock.fixLayout();
this.spriteBar.add(padlock);
if (this.currentSprite instanceof StageMorph) {
padlock.hide();
}
// tab bar
tabBar.tabTo = function (tabString) {
var active;
if (myself.currentTab === tabString) {return; }
myself.world().hand.destroyTemporaries();
myself.currentTab = tabString;
this.children.forEach(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
() => tabBar.tabTo('scripts'),
localize('Scripts'), // label
() => this.currentTab === 'scripts' // query
);
tab.padding = 3;
tab.corner = tabCorner;
tab.edge = 1;
tab.labelShadowOffset = new Point(-1, -1);
tab.labelShadowColor = tabColors[1];
tab.labelColor = this.buttonLabelColor;
tab.getPressRenderColor = function () {
if (MorphicPreferences.isFlat ||
SyntaxElementMorph.prototype.alpha > 0.85) {
return this.pressColor;
}
return this.pressColor.mixed(
Math.max(SyntaxElementMorph.prototype.alpha - 0.15, 0),
SpriteMorph.prototype.paletteColor
);
};
tab.fixLayout();
tabBar.add(tab);
tab = new TabMorph(
tabColors,
null, // target
() => tabBar.tabTo('costumes'),
localize(this.currentSprite instanceof SpriteMorph ?
'Costumes' : 'Backgrounds'
),
() => this.currentTab === 'costumes' // query
);
tab.padding = 3;
tab.corner = tabCorner;
tab.edge = 1;
tab.labelShadowOffset = new Point(-1, -1);
tab.labelShadowColor = tabColors[1];
tab.labelColor = this.buttonLabelColor;
tab.fixLayout();
tabBar.add(tab);
tab = new TabMorph(
tabColors,
null, // target
() => tabBar.tabTo('sounds'),
localize('Sounds'), // label
() => this.currentTab === 'sounds' // query
);
tab.padding = 3;
tab.corner = tabCorner;
tab.edge = 1;
tab.labelShadowOffset = new Point(-1, -1);
tab.labelShadowColor = tabColors[1];
tab.labelColor = this.buttonLabelColor;
tab.fixLayout();
tabBar.add(tab);
tabBar.fixLayout();
tabBar.children.forEach(each =>
each.refresh()
);
this.spriteBar.tabBar = tabBar;
this.spriteBar.add(this.spriteBar.tabBar);
this.spriteBar.fixLayout = function () {
this.tabBar.setLeft(this.left());
this.tabBar.setBottom(this.bottom() + myself.padding);
};
};
IDE_Morph.prototype.createSpriteEditor = function () {
// assumes that the logo pane and the stage have already been created
var scripts = this.currentSprite.scripts;
if (this.spriteEditor) {
this.spriteEditor.destroy();
}
if (this.currentTab === 'scripts') {
scripts.isDraggable = false;
scripts.color = this.groupColor;
scripts.cachedTexture = this.scriptsPaneTexture;
this.spriteEditor = new ScrollFrameMorph(
scripts,
null,
this.sliderColor
);
this.spriteEditor.color = this.groupColor;
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;
scripts.updateToolbar();
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 = (droppedMorph) => {
if (droppedMorph instanceof DialogBoxMorph) {
this.world().add(droppedMorph);
} else if (droppedMorph instanceof SpriteMorph) {
this.removeSprite(droppedMorph);
} else {
droppedMorph.destroy();
}
};
this.add(this.spriteEditor);
}
this.spriteEditor.mouseEnterDragging = (morph) => {
if (morph instanceof BlockMorph) {
this.spriteBar.tabBar.tabTo('scripts');
} else if (morph instanceof CostumeIconMorph) {
this.spriteBar.tabBar.tabTo('costumes');
} else if (morph instanceof SoundIconMorph) {
this.spriteBar.tabBar.tabTo('sounds');
}
};
this.spriteEditor.contents.mouseEnterDragging =
this.spriteEditor.mouseEnterDragging;
};
IDE_Morph.prototype.createCorralBar = function () {
// assumes the stage has already been created
var padding = 5,
newbutton,
paintbutton,
cambutton,
trashbutton,
myself = this,
colors = MorphicPreferences.isFlat ? this.tabColors
: [
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.corralBar.setWidth(this.stage.width());
this.add(this.corralBar);
// new sprite button
newbutton = new PushButtonMorph(
this,
"addNewSprite",
new SymbolMorph("turtle", 14)
);
newbutton.corner = 12;
newbutton.color = colors[0];
newbutton.highlightColor = colors[1];
newbutton.pressColor = colors[2];
newbutton.labelMinExtent = new Point(36, 18);
newbutton.padding = 0;
newbutton.labelShadowOffset = new Point(-1, -1);
newbutton.labelShadowColor = colors[1];
newbutton.labelColor = this.buttonLabelColor;
newbutton.contrast = this.buttonContrast;
newbutton.hint = "add a new Turtle sprite";
newbutton.fixLayout();
newbutton.setCenter(this.corralBar.center());
newbutton.setLeft(this.corralBar.left() + padding);
this.corralBar.add(newbutton);
paintbutton = new PushButtonMorph(
this,
"paintNewSprite",
new SymbolMorph("brush", 15)
);
paintbutton.corner = 12;
paintbutton.color = colors[0];
paintbutton.highlightColor = colors[1];
paintbutton.pressColor = colors[2];
paintbutton.labelMinExtent = new Point(36, 18);
paintbutton.padding = 0;
paintbutton.labelShadowOffset = new Point(-1, -1);
paintbutton.labelShadowColor = colors[1];
paintbutton.labelColor = this.buttonLabelColor;
paintbutton.contrast = this.buttonContrast;
paintbutton.hint = "paint a new sprite";
paintbutton.fixLayout();
paintbutton.setCenter(this.corralBar.center());
paintbutton.setLeft(
this.corralBar.left() + padding + newbutton.width() + padding
);
this.corralBar.add(paintbutton);
if (CamSnapshotDialogMorph.prototype.enableCamera) {
cambutton = new PushButtonMorph(
this,
"newCamSprite",
new SymbolMorph("camera", 15)
);
cambutton.corner = 12;
cambutton.color = colors[0];
cambutton.highlightColor = colors[1];
cambutton.pressColor = colors[2];
cambutton.labelMinExtent = new Point(36, 18);
cambutton.padding = 0;
cambutton.labelShadowOffset = new Point(-1, -1);
cambutton.labelShadowColor = colors[1];
cambutton.labelColor = this.buttonLabelColor;
cambutton.contrast = this.buttonContrast;
cambutton.hint = "take a camera snapshot and\n" +
"import it as a new sprite";
cambutton.fixLayout();
cambutton.setCenter(this.corralBar.center());
cambutton.setLeft(
this.corralBar.left() +
padding +
newbutton.width() +
padding +
paintbutton.width() +
padding
);
this.corralBar.add(cambutton);
document.addEventListener(
'cameraDisabled',
event => {
cambutton.disable();
cambutton.hint =
CamSnapshotDialogMorph.prototype.notSupportedMessage;
}
);
}
// trash button
trashbutton = new PushButtonMorph(
this,
"undeleteSprites",
new SymbolMorph("trash", 18)
);
trashbutton.corner = 12;
trashbutton.color = colors[0];
trashbutton.highlightColor = colors[1];
trashbutton.pressColor = colors[2];
trashbutton.labelMinExtent = new Point(36, 18);
trashbutton.padding = 0;
trashbutton.labelShadowOffset = new Point(-1, -1);
trashbutton.labelShadowColor = colors[1];
trashbutton.labelColor = this.buttonLabelColor;
trashbutton.contrast = this.buttonContrast;
// trashbutton.hint = "bring back deleted sprites";
trashbutton.fixLayout();
trashbutton.setCenter(this.corralBar.center());
trashbutton.setRight(this.corralBar.right() - padding);
this.corralBar.add(trashbutton);
trashbutton.wantsDropOf = (morph) =>
morph instanceof SpriteMorph || morph instanceof SpriteIconMorph;
trashbutton.reactToDropOf = (droppedMorph) => {
if (droppedMorph instanceof SpriteMorph) {
this.removeSprite(droppedMorph);
} else if (droppedMorph instanceof SpriteIconMorph) {
droppedMorph.destroy();
this.removeSprite(droppedMorph.object);
}
};
this.corralBar.fixLayout = function () {
function updateDisplayOf(button) {
if (button && button.right() > trashbutton.left() - padding) {
button.hide();
} else {
button.show();
}
}
this.setWidth(myself.stage.width());
trashbutton.setRight(this.right() - padding);
updateDisplayOf(cambutton);
updateDisplayOf(paintbutton);
};
};
IDE_Morph.prototype.createCorral = function (keepSceneAlbum) {
// assumes the corral bar has already been created
var frame, padding = 5, myself = this,
album = this.corral? this.corral.album : null;
this.createStageHandle();
this.createPaletteHandle();
if (this.corral) {
this.corral.destroy();
}
this.corral = new Morph();
this.corral.color = this.groupColor;
this.corral.getRenderColor = ScriptsMorph.prototype.getRenderColor;
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 = (morph) => morph instanceof SpriteIconMorph;
frame.contents.reactToDropOf = (spriteIcon) =>
this.corral.reactToDropOf(spriteIcon);
frame.alpha = 0;
this.sprites.asArray().forEach(morph => {
if (!morph.isTemporary) {
frame.contents.add(new SpriteIconMorph(morph));
}
});
this.corral.frame = frame;
this.corral.add(frame);
// scenes corral
this.corral.album = keepSceneAlbum ? album
: new SceneAlbumMorph(this, this.sliderColor);
this.corral.album.color = this.frameColor;
this.corral.add(this.corral.album);
this.corral.fixLayout = function () {
this.stageIcon.setCenter(this.center());
this.stageIcon.setLeft(this.left() + padding);
// scenes
if (myself.scenes.length() < 2) {
this.album.hide();
} else {
this.stageIcon.setTop(this.top());
this.album.show();
this.album.setLeft(this.left());
this.album.setTop(this.stageIcon.bottom() + padding);
this.album.setWidth(this.stageIcon.width() + padding * 2);
this.album.setHeight(
this.height() - this.stageIcon.height() - 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(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();
myself.recordUnsavedChanges();
};
this.corral.refresh = function () {
this.stageIcon.refresh();
this.frame.contents.children.forEach(icon =>
icon.refresh()
);
};
this.corral.wantsDropOf = (morph) => morph instanceof SpriteIconMorph;
this.corral.reactToDropOf = function (spriteIcon) {
var idx = 1,
pos = spriteIcon.position();
spriteIcon.destroy();
this.frame.contents.children.forEach(icon => {
if (pos.gt(icon.position()) || pos.y > icon.bottom()) {
idx += 1;
}
});
myself.sprites.add(spriteIcon.object, idx);
myself.createCorral(true); // keep scenes
myself.fixLayout();
};
};
// IDE_Morph layout
IDE_Morph.prototype.fixLayout = function (situation) {
// situation is a string, i.e.
// 'selectSprite' or 'refreshPalette' or 'tabEditor'
var padding = this.padding,
flag,
maxPaletteWidth;
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());
this.categories.setWidth(this.paletteWidth);
if (this.categories.scroller) {
this.categories.scroller.setWidth(this.paletteWidth);
}
}
// palette
this.palette.setLeft(this.logo.left());
this.palette.setTop(this.categories.bottom());
this.palette.setHeight(this.bottom() - this.palette.top());
this.palette.setWidth(this.paletteWidth);
if (situation !== 'refreshPalette') {
// stage
if (this.isEmbedMode) {
this.stage.setScale(Math.floor(Math.min(
this.width() / this.stage.dimensions.x,
this.height() / this.stage.dimensions.y
) * 100) / 100);
flag = this.embedPlayButton.flag;
flag.size = Math.floor(Math.min(
this.width(), this.height())) / 5;
flag.fixLayout();
this.embedPlayButton.size = flag.size * 1.6;
this.embedPlayButton.fixLayout();
if (this.embedOverlay) {
this.embedOverlay.setExtent(this.extent());
}
this.stage.setCenter(this.center());
this.embedPlayButton.setCenter(this.stage.center());
flag.setCenter(this.embedPlayButton.center());
flag.setLeft(flag.left() + flag.size * 0.1); // account for slight asymmetry
} else 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 ? this.stageRatio : 1);
this.stage.setTop(this.logo.bottom() + padding);
this.stage.setRight(this.right());
maxPaletteWidth = Math.max(
200,
this.width() -
this.stage.width() -
this.spriteBar.tabBar.width() -
(this.padding * 2)
);
if (this.paletteWidth > maxPaletteWidth) {
this.paletteWidth = maxPaletteWidth;
this.fixLayout();
}
this.stageHandle.fixLayout();
this.paletteHandle.fixLayout();
}
// spriteBar
this.spriteBar.setLeft(this.paletteWidth + padding);
this.spriteBar.setTop(this.logo.bottom() + padding);
this.spriteBar.setExtent(new Point(
Math.max(0, this.stage.left() - padding - this.spriteBar.left()),
//this.categories.bottom() - this.spriteBar.top() - padding - 8
this.spriteBar.top() + 44
));
this.spriteBar.fixLayout();
// spriteEditor
if (this.spriteEditor.isVisible) {
this.spriteEditor.setPosition(new Point(
this.spriteBar.left(),
this.spriteBar.bottom() + padding
));
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();
}
}
};
// IDE_Morph project properties
IDE_Morph.prototype.getProjectName = function () {
return this.scenes.at(1).name;
};
IDE_Morph.prototype.setProjectName = function (string) {
var projectScene = this.scenes.at(1),
name = this.newSceneName(string, projectScene);
if (name !== projectScene.name) {
projectScene.name = name;
projectScene.stage.version = Date.now();
this.recordUnsavedChanges();
if (projectScene === this.scene) {
this.controlBar.updateLabel();
}
}
return name;
};
IDE_Morph.prototype.getProjectNotes = function () {
return this.scenes.at(1).notes;
};
IDE_Morph.prototype.setProjectNotes = function (string) {
var projectScene = this.scenes.at(1);
if (string !== projectScene.notes) {
projectScene.notes = string;
projectScene.stage.version = Date.now();
this.recordUnsavedChanges();
if (projectScene === this.scene) {
this.controlBar.updateLabel();
}
}
};
// IDE_Morph resizing
IDE_Morph.prototype.setExtent = function (point) {
var padding = new Point(430, 110),
minExt,
ext,
maxWidth,
minWidth,
maxHeight,
minRatio,
maxRatio;
// determine the minimum dimensions making sense for the current mode
if (this.isAppMode) {
if (this.isEmbedMode) {
minExt = new Point(100, 100);
} else {
minExt = this.stage.dimensions.add(
this.controlBar.height() + 10
);
}
} else {
if (this.stageRatio > 1) {
minExt = padding.add(this.stage.dimensions);
} else {
minExt = padding.add(
this.stage.dimensions.multiplyBy(this.stageRatio)
);
}
}
ext = point.max(minExt);
// adjust stage ratio if necessary
maxWidth = ext.x -
(200 + this.spriteBar.tabBar.width() + (this.padding * 2));
minWidth = SpriteIconMorph.prototype.thumbSize.x * 3;
maxHeight = (ext.y - SpriteIconMorph.prototype.thumbSize.y * 3.5);
minRatio = minWidth / this.stage.dimensions.x;
maxRatio = Math.min(
(maxWidth / this.stage.dimensions.x),
(maxHeight / this.stage.dimensions.y)
);
this.stageRatio = Math.min(maxRatio, Math.max(minRatio, this.stageRatio));
// apply
IDE_Morph.uber.setExtent.call(this, ext);
this.fixLayout();
};
// IDE_Morph rendering
IDE_Morph.prototype.render = function (ctx) {
var frame;
IDE_Morph.uber.render.call(this, ctx);
if (this.isAppMode && this.stage) {
// draw a subtle outline rectangle around the stage
// in presentation mode
frame = this.stage.bounds.translateBy(
this.position().neg()
).expandBy(2);
ctx.strokeStyle = (MorphicPreferences.isFlat ? this.backgroundColor
: this.groupColor).toString();
ctx.lineWidth = 1;
ctx.beginPath();
ctx.moveTo(frame.origin.x, frame.origin.y);
ctx.lineTo(frame.corner.x, frame.origin.y);
ctx.lineTo(frame.corner.x, frame.corner.y);
ctx.lineTo(frame.origin.x, frame.corner.y);
ctx.closePath();
ctx.stroke();
}
};
// 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.beginBulkDrop = function () {
this.bulkDropInProgress = true;
this.cachedSceneFlag = this.isAddingScenes;
this.isAddingScenes = true;
};
IDE_Morph.prototype.endBulkDrop = function () {
this.isAddingScenes = this.cachedSceneFlag;
this.bulkDropInProgress = false;
};
IDE_Morph.prototype.droppedImage = function (aCanvas, name) {
var costume = new Costume(
aCanvas,
this.currentSprite.newCostumeName(
name ? name.split('.')[0] : '' // up to period
)
);
if (costume.isTainted()) {
this.inform(
'Unable to import this image',
'The picture you wish to import has been\n' +
'tainted by a restrictive cross-origin policy\n' +
'making it unusable for costumes in Snap!. \n\n' +
'Try downloading this picture first to your\n' +
'computer, and import it from there.'
);
return;
}
this.currentSprite.addCostume(costume);
this.currentSprite.wearCostume(costume);
this.spriteBar.tabBar.tabTo('costumes');
this.hasChangedMedia = true;
this.recordUnsavedChanges();
};
IDE_Morph.prototype.droppedSVG = function (anImage, name) {
var myself,
viewBox,
w = 300, h = 150, // setting HTMLImageElement default values
scale = 1,
svgNormalized,
headerLenght = anImage.src.search('base64') + 7,
// usually 26 from "data:image/svg+xml;base64,"
svgStrEncoded = anImage.src.substring(headerLenght),
svgObj = new DOMParser().parseFromString(
atob(svgStrEncoded), "image/svg+xml"
).firstElementChild,
normalizing = false;
name = name.split('.')[0];
// checking for svg 'width' and 'height' attributes
if (svgObj.attributes.getNamedItem("width") &&
svgObj.attributes.getNamedItem("height")) {
w = parseFloat(svgObj.attributes.getNamedItem("width").value);
h = parseFloat(svgObj.attributes.getNamedItem("height").value);
} else {
normalizing = true;
}
// checking for svg 'viewBox' attribute
if (svgObj.attributes.getNamedItem("viewBox")) {
viewBox = svgObj.attributes.getNamedItem('viewBox').value;
viewBox = viewBox.split(/[ ,]/).filter(item => item);
if (viewBox.length == 4) {
if (normalizing) {
w = parseFloat(viewBox[2]);
h = parseFloat(viewBox[3]);
}
}
} else {
svgObj.setAttribute('viewBox', '0 0 ' + w + ' ' + h);
normalizing = true;
}
// checking if the costume is bigger than the stage and, if so, fit it
if (this.stage.dimensions.x < w || this.stage.dimensions.y < h) {
scale = Math.min(
(this.stage.dimensions.x / w),
(this.stage.dimensions.y / h)
);
normalizing = true;
w = w * scale;
h = h * scale;
}
// loading image, normalized if it needed
// all the images are:
// sized, with 'width' and 'height' attributes
// fitted to stage dimensions
// and with their 'viewBox' attribute
if (normalizing) {
svgNormalized = new Image(w, h);
svgObj.setAttribute('width', w);
svgObj.setAttribute('height', h);
svgNormalized.src = 'data:image/svg+xml;base64,' +
btoa(new XMLSerializer().serializeToString(svgObj));
myself = this;
svgNormalized.onload = () => myself.loadSVG(svgNormalized, name);
} else {
this.loadSVG(anImage, name);
}
};
IDE_Morph.prototype.loadSVG = function (anImage, name) {
var costume = new SVG_Costume(anImage, name);
this.currentSprite.addCostume(costume);
this.currentSprite.wearCostume(costume);
this.spriteBar.tabBar.tabTo('costumes');
this.hasChangedMedia = true;
};
IDE_Morph.prototype.droppedAudio = function (anAudio, name) {
if (anAudio.src.indexOf('data:audio') !== 0) {
// fetch and base 64 encode samples using FileReader
this.getURL(
anAudio.src,
blob => {
var reader = new window.FileReader();
reader.readAsDataURL(blob);
reader.onloadend = () => {
var base64 = reader.result;
base64 = 'data:audio/ogg;base64,' +
base64.split(',')[1];
anAudio.src = base64;
this.droppedAudio(anAudio, name);
};
},
'blob'
);
} else {
this.currentSprite.addSound(anAudio, name.split('.')[0]); // up to '.'
this.spriteBar.tabBar.tabTo('sounds');
this.hasChangedMedia = true;
this.recordUnsavedChanges();
}
};
IDE_Morph.prototype.droppedText = function (aString, name, fileType) {
var lbl = name ? name.split('.')[0] : '',
ext = name ? name.slice(name.lastIndexOf('.') + 1).toLowerCase() : '',
setting = this.isAddingScenes;
// handle the special situation of adding a scene to the current project
if (this.isAddingNextScene) {
this.isAddingScenes = true;
if (aString.indexOf(' {
location.hash = '';
this.openProjectString(aString);
}
);
return;
}
if (aString.indexOf('';
}
if (aString.indexOf('