diff --git a/HISTORY.md b/HISTORY.md
index f241d431..407a807e 100755
--- a/HISTORY.md
+++ b/HISTORY.md
@@ -1,5 +1,99 @@
# Snap! (BYOB) History
+## in development for v7:
+* **New Features:**
+ * Scenes
+ * unified blocks palette option, thanks, Michael!
+* **Notable Changes:**
+ * saved projects remember the last edited srpite
+* **Notable Fixes:**
+ * made scrollbars in the wardrobe and jukebox more responsive
+
+### 2021-07-02
+* gui, object, store, etc.: unified blocks palette option, thanks, Michael!
+
+### 2021-05-21
+* gui, scenes, store: proxied thumbnail, name and notes in project, restored in XML
+* gui: distinguished project name from scene names, removed hidden "export as plain text" option
+* gui: sceneified project notes
+* gui: adjusted project thumbnail in "save" dialog
+* gui: some cleanups
+* gui, scenes: sceneified unsaved changes management
+* blocks: fixed search-blocks for scenesMenu
+
+### 2021-05-20
+* gui: marked projectName to be refactored and sceneified
+
+### 2021-05-19
+* gui: disabled scene icon context menu for project scene
+* gui: disabled dragging the project scene icon
+* gui: made sure the project scene stays in place
+* gui: added exporting single scenes
+* scenes, store: removed redundant properties "notes" and "thumbnail" from project
+* store: removed "thumbnail" property from scene xml
+
+### 2021-05-18
+* gui: fixed exporting media only for a single scene
+* gui: fixed cloud file format components
+* gui: "projectized" cloud file format for a single scene
+* gui: fixed cloud file format for multi-scene projects
+* gui: ensured unique scene names
+
+### 2021-05-11
+* gui: add multi-scene projects
+* gui: adjusted scene album rendering
+* gui: tweaked scene album rendering
+
+### 2021-05-10
+* gui: project menu entries for "new scene" and "add scene"
+
+### 2021-04-28
+* gui: only show scene album if the project has more than a single scene
+
+### 2021-04-23
+* store: serialize sprite-order from scenes
+* gui: sceneified refreshIDE()
+* gui: sceneified toggling dynamic input labels and switching languages
+* gui: sceneified "zoom blocks"
+* store: moved sprite-selection attribute from stage to scenes tag
+* scenes, store, gui: remember last edited scene in a project
+
+### 2021-04-22
+* store, gui: first pass at deserializing multi-scene projects
+* gui, scenes: migrated "new project" feature
+* gui: replaced openScene() with openProject()
+
+### 2021-04-21
+* store, gui: refactored project loading structure
+
+### 2021-04-20
+* scenes, store, gui: multi-scene project serialization format, first pass
+
+### 2021-04-16
+* scenes, store, gui: remember last edited sprite in a scene / project
+* scenes: removed Project class
+* scenes, store, gui: export multi-scene projects
+
+### 2021-04-14
+* scenes: new Project class
+* store: sceneified projects
+* gui: switched to scene-based project serialization
+
+### 2021-04-12
+* blocks, objects, threads, gui: new "switch to scene _" command primitive
+* morphic, gui: support bulk-file-drop for importing scenes
+* gui: tweaked scene album colors
+
+### 2021-04-08
+* gui: scroll selected scene icon into view
+
+### 2021-04-01
+* gui: made scrollbars in the wardrobe and jukebox more responsive
+
+### 2021-04-01
+* gui: made scene icons selectable
+* gui: made scene icons observe the scene's stage versions
+
## in development:
* **New Features:**
@@ -187,6 +281,23 @@
* fixed DEAL in the APL library, thanks, Brian!
* objects: fixed a resizing edge case bug for the stage prompter (ASK command)
+### 2021-03-31
+* gui: tweaked scene icon settings
+* gui: moved stage icon to the top of the corral
+
+### 2021-03-30
+* gui: added documentation
+* gui: added SceneIconMorph and SceneAlbumMorph prototypes
+* gui: turned scenes into an observable list
+* gui: added scene icon thumbnails
+
+### 2021-03-25
+* gui, scenes: sceneified trash
+* gui: first "live" multi-scene experiment
+
+### 2021-03-19
+* gui, store, scenes: capture global settings in scenes
+
## 6.7.3
* **Notable Changes:**
* hyperized "key _ pressed?" predicate
@@ -201,7 +312,11 @@
* threads: hyperized "key _ pressed?" predicate
* prepared patch
+### 2021-03-18
+* gui, scenes, objects: more scene-refactorings
+
### 2021-03-17
+* objects, gui, paint, sketch, store: de-globalized stage dimensions
* new dev version
* threads fixed repeat for non-numbers, thanks Stefan!
* updated list-utilities library, thanks, Brian!
@@ -215,6 +330,15 @@
* Catalan, thanks, Joan!
### 2021-03-15
+* gui: marked methods for scene refactorings
+
+### 2021-03-12
+* scenes, gui, store: added scenes class
+
+### 2021-03-11
+* gui, store: refactor loading a project into the IDE
+
+### 2021-03-09
* new dev version
* Catalan translation update, thanks, Joan!
* lists, apl: fixed "transpose", thanks, Brian!
diff --git a/snap.html b/snap.html
index 8da01e70..7da2dd31 100755
--- a/snap.html
+++ b/snap.html
@@ -3,25 +3,26 @@
- Snap! 6.10.0 - dev - Build Your Own Blocks
+ Snap! 7 - dev - Build Your Own Blocks
-
+
-
+
-
-
+
+
+
-
+
-
+
diff --git a/src/blocks.js b/src/blocks.js
index 0c38e66f..259a1702 100644
--- a/src/blocks.js
+++ b/src/blocks.js
@@ -158,7 +158,7 @@ CustomCommandBlockMorph, ToggleButtonMorph, DialMorph, SnapExtensions*/
// Global stuff ////////////////////////////////////////////////////////
-modules.blocks = '2021-June-18';
+modules.blocks = '2021-July-02';
var SyntaxElementMorph;
var BlockMorph;
@@ -753,6 +753,11 @@ SyntaxElementMorph.prototype.labelParts = {
'parameters' : ['parameters']
}
},
+ '%scn': {
+ type: 'input',
+ tags: 'read-only',
+ menu: 'scenesMenu'
+ },
// video
@@ -926,7 +931,7 @@ SyntaxElementMorph.prototype.labelParts = {
type: 'ring slot'
tags: 'static',
kind: 'command', 'reporter', 'predicate'
-
+
*/
'%rc': {
type: 'ring slot',
@@ -2052,7 +2057,7 @@ SyntaxElementMorph.prototype.fixLayout = function () {
return;
}
}
-
+
this.fixHighlight();
};
@@ -2460,6 +2465,7 @@ BlockSymbolMorph.prototype.getShadowRenderColor = function () {
%r - round reporter slot
%p - hexagonal predicate slot
%vid - chameleon colored rectangular drop-down for video modes
+ %scn - chameleon colored rectangular drop-down for scene names
rings:
@@ -4246,7 +4252,7 @@ BlockMorph.prototype.render = function (ctx) {
this.outlinePath(ctx, 0);
ctx.closePath();
ctx.fill();
-
+
// add 3D-Effect:
this.drawEdges(ctx);
}
@@ -5882,11 +5888,11 @@ function ReporterBlockMorph(isPredicate) {
ReporterBlockMorph.prototype.init = function (isPredicate) {
ReporterBlockMorph.uber.init.call(this);
this.isPredicate = isPredicate || false;
-
+
this.bounds.setExtent(new Point(50, 22).multiplyBy(this.scale));
this.fixLayout();
this.rerender();
-
+
this.cachedSlotSpec = null; // don't serialize
this.isLocalVarTemplate = null; // don't serialize
};
@@ -6577,7 +6583,7 @@ RingMorph.prototype.render = function (ctx) {
// ctx.closePath();
ctx.clip('evenodd');
ctx.fillRect(0, 0, this.width(), this.height());
-
+
// add 3D-Effect:
this.drawEdges(ctx);
}
@@ -9759,7 +9765,7 @@ InputSlotMorph.prototype.audioMenu = function (searching) {
'spectrum' : ['spectrum'],
'resolution' : ['resolution']
};
- if (searching) {return {}; }
+ if (searching) {return dict; }
if (this.world().currentKey === 16) { // shift
dict['~'] = null;
@@ -9769,6 +9775,28 @@ InputSlotMorph.prototype.audioMenu = function (searching) {
return dict;
};
+InputSlotMorph.prototype.scenesMenu = function (searching) {
+ var dict = {},
+ scenes;
+ if (!searching) {
+ scenes = this.parentThatIsA(IDE_Morph).scenes;
+ if (scenes.length() > 1) {
+ scenes.itemsArray().forEach(scn => {
+ if (scn.name) {
+ dict[scn.name] = scn.name;
+ }
+ });
+ }
+ }
+ dict['~'] = null;
+ dict.next = ['next'];
+ dict.previous = ['previous'];
+ dict['1 '] = 1; // trailing space needed to prevent undesired sorting
+ dict.last = ['last'];
+ dict.random = ['random'];
+ return dict;
+};
+
InputSlotMorph.prototype.setChoices = function (dict, readonly) {
// externally specify choices and read-only status,
// used for custom blocks
@@ -11002,7 +11030,8 @@ BooleanSlotMorph.prototype.drawKnob = function (ctx, progress) {
var w = this.width(),
r = this.height() / 2,
shift = this.edge / 2,
- slideStep = (this.width() - this.height()) / 4 * Math.max(0, (progress || 0)),
+ slideStep = (this.width() - this.height()) / 4 *
+ Math.max(0, (progress || 0)),
gradient,
x,
y = r,
@@ -14272,7 +14301,7 @@ ScriptFocusMorph.prototype.reactToKeyEvent = function (key) {
cmd = new CommandBlockMorph();
cmd.setSpec('command %cmdRing');
-
+
rings = new CommandBlockMorph();
rings.setSpec('reporter %repRing predicate %predRing');
diff --git a/src/gui.js b/src/gui.js
index d0f1c75f..4e1a0f28 100644
--- a/src/gui.js
+++ b/src/gui.js
@@ -39,10 +39,15 @@
IDE_Morph
ProjectDialogMorph
+ LibraryImportDialogMorph
SpriteIconMorph
TurtleIconMorph
CostumeIconMorph
WardrobeMorph
+ SoundIconMorph
+ JukeboxMorph
+ SceneIconMorph
+ SceneAlbumMorph
StageHandleMorph
PaletteHandleMorph
CamSnapshotDialogMorph
@@ -62,8 +67,8 @@
*/
/*global modules, Morph, SpriteMorph, SyntaxElementMorph, Color, Cloud, Audio,
-ListWatcherMorph, TextMorph, newCanvas, useBlurredShadows, VariableFrame, Sound,
-StringMorph, Point, MenuMorph, morphicVersion, DialogBoxMorph, normalizeCanvas,
+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,
@@ -74,11 +79,11 @@ CommandBlockMorph, BooleanSlotMorph, RingReporterSlotMorph, ScriptFocusMorph,
BlockLabelPlaceHolderMorph, SpeechBubbleMorph, XML_Element, WatcherMorph, WHITE,
BlockRemovalDialogMorph,TableMorph, isSnapObject, isRetinaEnabled, SliderMorph,
disableRetinaSupport, enableRetinaSupport, isRetinaSupported, MediaRecorder,
-Animation, BoxMorph, BlockEditorMorph, BlockDialogMorph, Note, ZERO, BLACK*/
+Animation, BoxMorph, BlockDialogMorph, Project, ZERO, BLACK*/
// Global stuff ////////////////////////////////////////////////////////
-modules.gui = '2021-June-23';
+modules.gui = '2021-July-02';
// Declarations
@@ -91,6 +96,8 @@ var TurtleIconMorph;
var WardrobeMorph;
var SoundIconMorph;
var JukeboxMorph;
+var SceneIconMorph;
+var SceneAlbumMorph;
var StageHandleMorph;
var PaletteHandleMorph;
var CamSnapshotDialogMorph;
@@ -144,6 +151,8 @@ IDE_Morph.prototype.setDefaultDesign = function () {
= 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;
@@ -183,6 +192,8 @@ IDE_Morph.prototype.setFlatDesign = function () {
= 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);
@@ -227,15 +238,18 @@ IDE_Morph.prototype.init = function (isAutoFill) {
this.source = null;
this.serializer = new SnapSerializer();
- this.globalVariables = new VariableFrame();
- this.currentSprite = new SpriteMorph(this.globalVariables);
- this.sprites = new List([this.currentSprite]);
+ // 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 = 'motion';
this.currentTab = 'scripts';
- this.projectName = '';
- this.projectNotes = '';
-
- this.trash = []; // deleted sprites
// logoURL is disabled because the image data is hard-copied
// to avoid tainting the world canvas
@@ -263,9 +277,7 @@ IDE_Morph.prototype.init = function (isAutoFill) {
this.filePicker = null;
// incrementally saving projects to the cloud is currently unused
- this.hasChangedMedia = false;
-
- this.hasUnsavedEdits = false; // keeping track of when to internally backup
+ this.hasChangedMedia = false; // +++ sceneify, or get of it
this.isAnimating = true;
this.paletteWidth = 200; // initially same as logo width
@@ -278,6 +290,9 @@ IDE_Morph.prototype.init = function (isAutoFill) {
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);
@@ -371,7 +386,7 @@ IDE_Morph.prototype.openIn = function (world) {
world.worldCanvas.focus();
}
}
-
+
function autoRun () {
// wait until all costumes and sounds are loaded
if (isLoadingAssets()) {
@@ -1121,7 +1136,7 @@ IDE_Morph.prototype.createControlBar = function () {
x = Math.min(
startButton.left() - (3 * padding + 2 * stageSizeButton.width()),
- myself.right() - StageMorph.prototype.dimensions.x *
+ myself.right() - myself.stage.dimensions.x *
(myself.isSmallStage ? myself.stageRatio : 1)
);
[stageSizeButton, appModeButton].forEach(button => {
@@ -1189,7 +1204,7 @@ IDE_Morph.prototype.createControlBar = function () {
};
this.controlBar.updateLabel = function () {
- var prefix = myself.hasUnsavedEdits ? '\u270E ' : '',
+ var prefix = myself.hasUnsavedEdits() ? '\u270E ' : '',
suffix = myself.world().isDevMode ?
' - ' + localize('development mode') : '',
txt;
@@ -1201,7 +1216,7 @@ IDE_Morph.prototype.createControlBar = function () {
return;
}
txt = new StringMorph(
- prefix + (myself.projectName || localize('untitled')) + suffix,
+ prefix + (myself.scene.name || localize('untitled')) + suffix,
14,
'sans-serif',
true,
@@ -1230,7 +1245,8 @@ IDE_Morph.prototype.createControlBar = function () {
};
IDE_Morph.prototype.createCategories = function () {
- var myself = this;
+ var myself = this,
+ categorySelectionAction;
if (this.categories) {
this.categories.destroy();
@@ -1240,6 +1256,27 @@ IDE_Morph.prototype.createCategories = function () {
this.categories.bounds.setWidth(this.paletteWidth);
// this.categories.getRenderColor = ScriptsMorph.prototype.getRenderColor;
+
+ if (this.scene.unifiedPalette) {
+ categorySelectionAction = scrollToCategory;
+ } else {
+ categorySelectionAction = changePalette;
+ }
+
+ function changePalette(category) {
+ return () => {
+ myself.currentCategory = category;
+ myself.categories.children.forEach(each =>
+ each.refresh()
+ );
+ myself.refreshPalette(true);
+ };
+ }
+
+ function scrollToCategory(category) {
+ return () => myself.scrollPaletteToCategory(category);
+ }
+
function addCategoryButton(category) {
var labelWidth = 75,
colors = [
@@ -1252,13 +1289,7 @@ IDE_Morph.prototype.createCategories = function () {
button = new ToggleButtonMorph(
colors,
myself, // the IDE is the target
- () => {
- myself.currentCategory = category;
- myself.categories.children.forEach(each =>
- each.refresh()
- );
- myself.refreshPalette(true);
- },
+ categorySelectionAction(category),
category[0].toUpperCase().concat(category.slice(1)), // label
() => myself.currentCategory === category, // query
null, // env
@@ -1406,20 +1437,11 @@ IDE_Morph.prototype.createPaletteHandle = function () {
};
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);
+ if (this.stage) {
+ this.stage.destroy();
}
- this.add(this.stage);
+ this.add(this.scene.stage);
+ this.stage = this.scene.stage;
};
IDE_Morph.prototype.createStageHandle = function () {
@@ -1903,9 +1925,10 @@ IDE_Morph.prototype.createCorralBar = function () {
};
};
-IDE_Morph.prototype.createCorral = function () {
+IDE_Morph.prototype.createCorral = function (keepSceneAlbum) {
// assumes the corral bar has already been created
- var frame, padding = 5, myself = this;
+ var frame, padding = 5, myself = this,
+ album = this.corral? this.corral.album : null;
this.createStageHandle();
this.createPaletteHandle();
@@ -1944,9 +1967,30 @@ IDE_Morph.prototype.createCorral = function () {
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(),
@@ -2000,7 +2044,7 @@ IDE_Morph.prototype.createCorral = function () {
}
});
myself.sprites.add(spriteIcon.object, idx);
- myself.createCorral();
+ myself.createCorral(true); // keep scenes
myself.fixLayout();
};
};
@@ -2114,10 +2158,40 @@ IDE_Morph.prototype.fixLayout = function (situation) {
}
};
+// IDE_Morph project properties
+
+IDE_Morph.prototype.getProjectName = function () {
+ return this.scenes.at(1).name;
+};
+
IDE_Morph.prototype.setProjectName = function (string) {
- this.projectName = string.replace(/['"]/g, ''); // filter quotation marks
- this.hasChangedMedia = true;
- this.controlBar.updateLabel();
+ 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(); // +++ sceneify this
+ 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(); // +++ sceneify this
+ if (projectScene === this.scene) {
+ this.controlBar.updateLabel();
+ }
+ }
};
// IDE_Morph resizing
@@ -2137,16 +2211,16 @@ IDE_Morph.prototype.setExtent = function (point) {
if (this.isEmbedMode) {
minExt = new Point(100, 100);
} else {
- minExt = StageMorph.prototype.dimensions.add(
+ minExt = this.stage.dimensions.add(
this.controlBar.height() + 10
);
}
} else {
if (this.stageRatio > 1) {
- minExt = padding.add(StageMorph.prototype.dimensions);
+ minExt = padding.add(this.stage.dimensions);
} else {
minExt = padding.add(
- StageMorph.prototype.dimensions.multiplyBy(this.stageRatio)
+ this.stage.dimensions.multiplyBy(this.stageRatio)
);
}
}
@@ -2206,6 +2280,17 @@ IDE_Morph.prototype.reactToWorldResize = function (rect) {
}
};
+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,
@@ -2274,11 +2359,10 @@ IDE_Morph.prototype.droppedSVG = function (anImage, name) {
}
// checking if the costume is bigger than the stage and, if so, fit it
- if (StageMorph.prototype.dimensions.x < w ||
- StageMorph.prototype.dimensions.y < h) {
+ if (this.stage.dimensions.x < w || this.stage.dimensions.y < h) {
scale = Math.min(
- (StageMorph.prototype.dimensions.x / w),
- (StageMorph.prototype.dimensions.y / h)
+ (this.stage.dimensions.x / w),
+ (this.stage.dimensions.y / h)
);
normalizing = true;
w = w * scale;
@@ -2289,7 +2373,7 @@ IDE_Morph.prototype.droppedSVG = function (anImage, name) {
// all the images are:
// sized, with 'width' and 'height' attributes
// fitted to stage dimensions
- // and with their 'viewBox' attribute
+ // and with their 'viewBox' attribute
if (normalizing) {
svgNormalized = new Image(w, h);
svgObj.setAttribute('width', w);
@@ -2340,7 +2424,23 @@ IDE_Morph.prototype.droppedAudio = function (anAudio, name) {
IDE_Morph.prototype.droppedText = function (aString, name, fileType) {
var lbl = name ? name.split('.')[0] : '',
- ext = name ? name.slice(name.lastIndexOf('.') + 1).toLowerCase() : '';
+ 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(' block.category === category
+ );
+
+ palette.scrollY(palette.top() - firstInCategory.top() + palette.padding);
+ palette.adjustScrollBars();
+};
+
IDE_Morph.prototype.pressStart = function () {
if (this.world().currentKey === 16) { // shiftClicked
this.toggleFastTracking();
@@ -2540,6 +2650,7 @@ IDE_Morph.prototype.selectSprite = function (sprite) {
this.currentSprite.scripts.focus.stopEditing();
}
this.currentSprite = sprite;
+ this.scene.currentSprite = sprite;
this.createPalette();
this.createSpriteBar();
this.createSpriteEditor();
@@ -2583,12 +2694,16 @@ IDE_Morph.prototype.refreshIDE = function () {
if (Process.prototype.isCatchingErrors) {
try {
- projectData = this.serializer.serialize(this.stage);
+ projectData = this.serializer.serialize(
+ new Project(this.scenes, this.scene)
+ );
} catch (err) {
this.showMessage('Serialization failed: ' + err);
}
} else {
- projectData = this.serializer.serialize(this.stage);
+ projectData = this.serializer.serialize(
+ new Project(this.scenes, this.scene)
+ );
}
SpriteMorph.prototype.initBlocks();
this.buildPanes();
@@ -2736,13 +2851,17 @@ IDE_Morph.prototype.hasLocalStorage = function () {
// IDE_Morph recording unsaved changes
+IDE_Morph.prototype.hasUnsavedEdits = function () {
+ return this.scenes.itemsArray().some(any => any.hasUnsavedEdits);
+};
+
IDE_Morph.prototype.recordUnsavedChanges = function () {
- this.hasUnsavedEdits = true;
+ this.scene.hasUnsavedEdits = true;
this.updateChanges();
};
IDE_Morph.prototype.recordSavedChanges = function () {
- this.hasUnsavedEdits = false;
+ this.scenes.itemsArray().forEach(scene => scene.hasUnsavedEdits = false);
this.updateChanges();
};
@@ -2766,7 +2885,7 @@ IDE_Morph.prototype.backup = function (callback) {
// Save the current project for the currently logged in user
// to localstorage, then perform the given callback, e.g.
// load a new project.
- if (this.hasUnsavedEdits) {
+ if (this.hasUnsavedEdits()) {
this.confirm(
'Replace the current project with a new one?',
'Unsaved Changes!',
@@ -2781,7 +2900,9 @@ IDE_Morph.prototype.backupAndDo = function (callback) {
// private
var username = this.cloud.username;
try {
- localStorage['-snap-backup-'] = this.serializer.serialize(this.stage);
+ localStorage['-snap-backup-'] = this.serializer.serialize(
+ new Project(this.scenes, this.scene)
+ );
delete localStorage['-snap-bakflag-'];
if (username) {
localStorage['-snap-bakuser-'] = username;
@@ -3146,7 +3267,7 @@ IDE_Morph.prototype.removeSprite = function (sprite) {
if (idx > 0) {
this.sprites.remove(idx);
}
- this.createCorral();
+ this.createCorral(true); // keep scenes
this.fixLayout();
this.currentSprite = detect(
this.stage.children,
@@ -3157,7 +3278,7 @@ IDE_Morph.prototype.removeSprite = function (sprite) {
this.recordUnsavedChanges();
// remember the deleted sprite so it can be recovered again later
- this.trash.push(sprite);
+ this.scene.trash.push(sprite);
};
IDE_Morph.prototype.newSoundName = function (name) {
@@ -3180,6 +3301,14 @@ IDE_Morph.prototype.newSpriteName = function (name, ignoredSprite) {
return this.newName(name, all);
};
+IDE_Morph.prototype.newSceneName = function (name, ignoredScene) {
+ var sName = name.replace(/['"]/g, ''), // filter out quotation marks
+ all = this.scenes.asArray().filter(each =>
+ each !== ignoredScene
+ ).map(each => each.name);
+ return this.newName(sName, all);
+};
+
IDE_Morph.prototype.newName = function (name, elements) {
var ix = name.indexOf('('),
stem = (ix < 0) ? name : name.substring(0, ix),
@@ -3323,8 +3452,9 @@ IDE_Morph.prototype.cloudMenu = function () {
menu.addItem(
'export project media only...',
() => {
- if (this.projectName) {
- this.exportProjectMedia(this.projectName);
+ var pn = this.getProjectName();
+ if (pn) {
+ this.exportProjectMedia(pn);
} else {
this.prompt(
'Export Project As...',
@@ -3340,8 +3470,9 @@ IDE_Morph.prototype.cloudMenu = function () {
menu.addItem(
'export project without media...',
() => {
- if (this.projectName) {
- this.exportProjectNoMedia(this.projectName);
+ var pn = this.getProjectName();
+ if (pn) {
+ this.exportProjectNoMedia(pn);
} else {
this.prompt(
'Export Project As...',
@@ -3357,8 +3488,9 @@ IDE_Morph.prototype.cloudMenu = function () {
menu.addItem(
'export project as cloud data...',
() => {
- if (this.projectName) {
- this.exportProjectAsCloudData(this.projectName);
+ var pn = this.getProjectName();
+ if (pn) {
+ this.exportProjectAsCloudData(pn);
} else {
this.prompt(
'Export Project As...',
@@ -3503,7 +3635,7 @@ IDE_Morph.prototype.settingsMenu = function () {
}
*/
Process.prototype.enableJS = !Process.prototype.enableJS;
- this.currentSprite.blocksCache.operators = null;
+ this.currentSprite.primitivesCache.operators = null;
this.currentSprite.paletteCache.operators = null;
this.refreshPalette();
},
@@ -3514,6 +3646,14 @@ IDE_Morph.prototype.settingsMenu = function () {
'NOTE: You will have to manually\n' +
'sign in again to access your account.' */
);
+ addPreference(
+ 'Add scenes',
+ () => this.isAddingScenes = !this.isAddingScenes,
+ this.isAddingScenes,
+ 'uncheck to replace the current project,\nwith a new one',
+ 'check to add other projects,\nto this one',
+ true
+ );
if (isRetinaSupported()) {
addPreference(
'Retina display support',
@@ -3736,7 +3876,7 @@ IDE_Morph.prototype.settingsMenu = function () {
() => {
SpriteMorph.prototype.enableFirstClass =
!SpriteMorph.prototype.enableFirstClass;
- this.currentSprite.blocksCache.sensing = null;
+ this.currentSprite.primitivesCache.sensing = null;
this.currentSprite.paletteCache.sensing = null;
this.refreshPalette();
},
@@ -3810,7 +3950,7 @@ IDE_Morph.prototype.settingsMenu = function () {
() => {
Process.prototype.enableCompiling =
!Process.prototype.enableCompiling;
- this.currentSprite.blocksCache.operators = null;
+ this.currentSprite.primitivesCache.operators = null;
this.currentSprite.paletteCache.operators = null;
this.refreshPalette();
},
@@ -3848,7 +3988,7 @@ IDE_Morph.prototype.settingsMenu = function () {
() => {
StageMorph.prototype.enableCodeMapping =
!StageMorph.prototype.enableCodeMapping;
- this.currentSprite.blocksCache.variables = null;
+ this.currentSprite.primitivesCache.variables = null;
this.currentSprite.paletteCache.variables = null;
this.refreshPalette();
},
@@ -3862,7 +4002,7 @@ IDE_Morph.prototype.settingsMenu = function () {
() => {
StageMorph.prototype.enableInheritance =
!StageMorph.prototype.enableInheritance;
- this.currentSprite.blocksCache.variables = null;
+ this.currentSprite.primitivesCache.variables = null;
this.currentSprite.paletteCache.variables = null;
this.refreshPalette();
},
@@ -3880,6 +4020,14 @@ IDE_Morph.prototype.settingsMenu = function () {
'check to enable\nusing operators on lists and tables',
false
);
+ addPreference(
+ 'Unified Palette',
+ () => this.toggleUnifiedPalette(),
+ this.scene.unifiedPalette,
+ 'uncheck to show only the selected category\'s blocks',
+ 'check to show all blocks in a single palette',
+ false
+ );
addPreference(
'Persist linked sublist IDs',
() => StageMorph.prototype.enableSublistIDs =
@@ -3911,7 +4059,7 @@ IDE_Morph.prototype.projectMenu = function () {
backup = this.availableBackup(shiftClicked);
menu = new MenuMorph(this);
- menu.addItem('Project notes...', 'editProjectNotes');
+ menu.addItem('Notes...', 'editNotes');
menu.addLine();
menu.addPair('New', 'createNewProject', '^N');
menu.addPair('Open...', 'openProjectsBrowser', '^O');
@@ -3939,46 +4087,22 @@ IDE_Morph.prototype.projectMenu = function () {
'importLocalFile',
'file menu import hint' // looks up the actual text in the translator
);
-
- if (shiftClicked) {
- menu.addItem(
- localize(
- 'Export project...') + ' ' + localize('(in a new window)'
- ),
- () => {
- if (this.projectName) {
- this.exportProject(this.projectName, shiftClicked);
- } else {
- this.prompt(
- 'Export Project As...',
- // false - override the shiftClick setting to use XML:
- name => this.exportProject(name, false),
- null,
- 'exportProject'
- );
- }
- },
- 'show project data as XML\nin a new browser window',
- new Color(100, 0, 0)
- );
- }
menu.addItem(
- shiftClicked ?
- 'Export project as plain text...' : 'Export project...',
+ 'Export project...',
() => {
- if (this.projectName) {
- this.exportProject(this.projectName, shiftClicked);
+ var pn = this.getProjectName();
+ if (pn) {
+ this.exportProject(pn);
} else {
this.prompt(
'Export Project As...',
- name => this.exportProject(name, shiftClicked),
+ name => this.exportProject(name),
null,
'exportProject'
);
}
},
- 'save project data as XML\nto your downloads folder',
- shiftClicked ? new Color(100, 0, 0) : null
+ 'save project data as XML\nto your downloads folder'
);
if (this.stage.globalBlocks.length) {
@@ -4019,6 +4143,12 @@ IDE_Morph.prototype.projectMenu = function () {
);
}
+ menu.addLine();
+ if (this.scenes.length() > 1) {
+ menu.addItem('Scenes...', 'scenesMenu');
+ }
+ menu.addPair('New scene', 'createNewScene');
+ menu.addPair('Add scene...', 'addScene');
menu.addLine();
menu.addItem(
'Libraries...',
@@ -4061,7 +4191,7 @@ IDE_Morph.prototype.projectMenu = function () {
'Select a sound from the media library'
);
- if (this.trash.length) {
+ if (this.scene.trash.length) {
menu.addLine();
menu.addItem(
'Undelete sprites...',
@@ -4141,6 +4271,8 @@ IDE_Morph.prototype.parseResourceFile = function (text) {
IDE_Morph.prototype.importLocalFile = function () {
var inp = document.createElement('input'),
+ addingScenes = this.isAddingScenes,
+ myself = this,
world = this.world();
if (this.filePicker) {
@@ -4163,6 +4295,9 @@ IDE_Morph.prototype.importLocalFile = function () {
() => {
document.body.removeChild(inp);
this.filePicker = null;
+ if (addingScenes) {
+ myself.isAddingNextScene = true;
+ }
world.hand.processDrop(inp.files);
},
false
@@ -4341,11 +4476,11 @@ IDE_Morph.prototype.undeleteSprites = function (pos) {
var menu = new MenuMorph(sprite => this.undelete(sprite, pos), null, this);
pos = pos || this.corralBar.bottomRight();
- if (!this.trash.length) {
+ if (!this.scene.trash.length) {
this.showMessage('trash is empty');
return;
}
- this.trash.forEach(sprite =>
+ this.scene.trash.forEach(sprite =>
menu.addItem(
[
sprite.thumbnail(new Point(24, 24), null, true), // no corpse
@@ -4378,7 +4513,7 @@ IDE_Morph.prototype.undelete = function (aSprite, pos) {
this.sprites.add(aSprite);
this.corral.addSprite(aSprite);
this.selectSprite(aSprite);
- this.trash = this.trash.filter(sprite => sprite.isCorpse);
+ this.scene.updateTrash();
}
);
};
@@ -4390,7 +4525,7 @@ IDE_Morph.prototype.aboutSnap = function () {
module, btn1, btn2, btn3, btn4, licenseBtn, translatorsBtn,
world = this.world();
- aboutTxt = 'Snap! 6.10.0 - dev -\nBuild Your Own Blocks\n\n'
+ aboutTxt = 'Snap! 7 - dev -\nBuild Your Own Blocks\n\n'
+ 'Copyright \u24B8 2008-2021 Jens M\u00F6nig and '
+ 'Brian Harvey\n'
+ 'jens@moenig.org, bh@cs.berkeley.edu\n\n'
@@ -4572,11 +4707,33 @@ IDE_Morph.prototype.aboutSnap = function () {
dlg.fixLayout();
};
+IDE_Morph.prototype.scenesMenu = function () {
+ var menu = new MenuMorph(scn => this.switchToScene(scn), null, this),
+ world = this.world(),
+ pos = this.controlBar.projectButton.bottomLeft(),
+ tick = new SymbolMorph(
+ 'tick',
+ MorphicPreferences.menuFontSize * 0.75
+ ),
+ empty = tick.fullCopy();
-IDE_Morph.prototype.editProjectNotes = function () {
- var dialog = new DialogBoxMorph().withKey('projectNotes'),
+ empty.render = nop;
+ this.scenes.asArray().forEach(scn =>
+ menu.addItem(
+ [
+ this.scene === scn ? tick : empty,
+ scn.name
+ ],
+ scn
+ )
+ );
+ menu.popup(world, pos);
+};
+
+IDE_Morph.prototype.editNotes = function () {
+ var dialog = new DialogBoxMorph().withKey('notes'),
frame = new ScrollFrameMorph(),
- text = new TextMorph(this.projectNotes || ''),
+ text = new TextMorph(this.scene.notes || ''),
size = 250,
world = this.world();
@@ -4606,13 +4763,13 @@ IDE_Morph.prototype.editProjectNotes = function () {
dialog.target = this;
dialog.action = (note) => {
- this.projectNotes = note;
- this.recordUnsavedChanges();
+ this.scene.notes = note;
+ this.recordUnsavedChanges(); // +++ sceneify this
};
dialog.justDropped = () => text.edit();
- dialog.labelString = 'Project Notes';
+ dialog.labelString = 'Notes';
dialog.createLabel();
dialog.addBody(frame);
dialog.addButton('ok', 'OK');
@@ -4624,48 +4781,34 @@ IDE_Morph.prototype.editProjectNotes = function () {
};
IDE_Morph.prototype.newProject = function () {
+ var project = new Project();
+
+ project.addDefaultScene();
this.source = this.cloud.username ? 'cloud' : null;
- if (this.stage) {
- this.stage.destroy();
- }
if (location.hash.substr(0, 6) !== '#lang:') {
location.hash = '';
}
- this.globalVariables = new VariableFrame();
- this.currentSprite = new SpriteMorph(this.globalVariables);
- this.sprites = new List([this.currentSprite]);
- StageMorph.prototype.dimensions = new Point(480, 360);
- StageMorph.prototype.hiddenPrimitives = {};
- StageMorph.prototype.codeMappings = {};
- StageMorph.prototype.codeHeaders = {};
- StageMorph.prototype.enableCodeMapping = false;
- StageMorph.prototype.enableInheritance = true;
- StageMorph.prototype.enableSublistIDs = false;
- StageMorph.prototype.enablePenLogging = false;
- SpriteMorph.prototype.useFlatLineEnds = false;
- Process.prototype.enableLiveCoding = false;
- Process.prototype.enableHyperOps = true;
- this.hasUnsavedEdits = false;
- this.setProjectName('');
- this.projectNotes = '';
- this.trash = [];
- this.createStage();
- this.add(this.stage);
- this.createCorral();
- this.selectSprite(this.stage.children[0]);
- this.fixLayout();
+ this.openProject(project);
+};
+
+IDE_Morph.prototype.createNewScene = function () {
+ var setting = this.isAddingScenes;
+ this.isAddingScenes = true;
+ this.newProject();
+ this.isAddingScenes = setting;
};
IDE_Morph.prototype.save = function () {
// temporary hack - only allow exporting projects to disk
// when running Snap! locally without a web server
+ var pn = this.getProjectName();
if (location.protocol === 'file:') {
- if (this.projectName) {
- this.exportProject(this.projectName, false);
+ if (pn) {
+ this.exportProject(pn);
} else {
this.prompt(
'Export Project As...',
- name => this.exportProject(name, false),
+ name => this.exportProject(name),
null,
'exportProject'
);
@@ -4680,11 +4823,11 @@ IDE_Morph.prototype.save = function () {
if (this.cloud.disabled) {this.source = 'disk'; }
- if (this.projectName) {
+ if (pn) {
if (this.source === 'disk') {
- this.exportProject(this.projectName);
+ this.exportProject(pn);
} else if (this.source === 'cloud') {
- this.saveProjectToCloud(this.projectName);
+ this.saveProjectToCloud(pn);
} else {
this.saveProjectsBrowser();
}
@@ -4693,18 +4836,17 @@ IDE_Morph.prototype.save = function () {
}
};
-IDE_Morph.prototype.exportProject = function (name, plain) {
+IDE_Morph.prototype.exportProject = function (name) {
// Export project XML, saving a file to disk
- // newWindow requests displaying the project in a new tab.
- var menu, str, dataPrefix;
-
+ var menu, str;
if (name) {
- this.setProjectName(name);
- dataPrefix = 'data:text/' + plain ? 'plain,' : 'xml,';
+ name = this.setProjectName(name);
try {
menu = this.showMessage('Exporting');
- str = this.serializer.serialize(this.stage);
- this.setURL('#open:' + dataPrefix + encodeURIComponent(str));
+ str = this.serializer.serialize(
+ new Project(this.scenes, this.scene)
+ );
+ this.setURL('#open:data:text/xml,' + encodeURIComponent(str));
this.saveXMLAs(str, name);
menu.destroy();
this.recordSavedChanges();
@@ -4830,7 +4972,7 @@ IDE_Morph.prototype.exportScriptsPicture = function () {
y += padding;
y += each.height;
});
- this.saveCanvasAs(pic, this.projectName || localize('Untitled'));
+ this.saveCanvasAs(pic, this.scene.name || localize('Untitled'));
};
IDE_Morph.prototype.exportProjectSummary = function (useDropShadows) {
@@ -4933,7 +5075,7 @@ IDE_Morph.prototype.exportProjectSummary = function (useDropShadows) {
}
}
- pname = this.projectName || localize('untitled');
+ pname = this.scene.name || localize('untitled');
html = new XML_Element('html');
html.attributes.lang = SnapTranslator.language;
@@ -4994,7 +5136,7 @@ IDE_Morph.prototype.exportProjectSummary = function (useDropShadows) {
}
// project notes
- notes = Process.prototype.reportTextSplit(this.projectNotes, 'line');
+ notes = Process.prototype.reportTextSplit(this.scene.notes, 'line');
notes.asArray().forEach(paragraph => add(paragraph));
// table of contents
@@ -5114,6 +5256,11 @@ IDE_Morph.prototype.exportProjectSummary = function (useDropShadows) {
IDE_Morph.prototype.openProjectString = function (str, callback) {
var msg;
+ if (this.bulkDropInProgress || this.isAddingScenes) {
+ this.rawOpenProjectString(str);
+ if (callback) {callback(); }
+ return;
+ }
this.nextSteps([
() => msg = this.showMessage('Opening project...'),
() => {
@@ -5127,29 +5274,17 @@ IDE_Morph.prototype.openProjectString = function (str, callback) {
IDE_Morph.prototype.rawOpenProjectString = function (str) {
this.toggleAppMode(false);
this.spriteBar.tabBar.tabTo('scripts');
- StageMorph.prototype.hiddenPrimitives = {};
- StageMorph.prototype.codeMappings = {};
- StageMorph.prototype.codeHeaders = {};
- StageMorph.prototype.enableCodeMapping = false;
- StageMorph.prototype.enableInheritance = true;
- StageMorph.prototype.enableSublistIDs = false;
- StageMorph.prototype.enablePenLogging = false;
- Process.prototype.enableLiveCoding = false;
- this.trash = [];
- this.hasUnsavedEdits = false;
if (Process.prototype.isCatchingErrors) {
try {
- this.serializer.openProject(
- this.serializer.load(str, this),
- this
+ this.openProject(
+ this.serializer.load(str, this)
);
} catch (err) {
this.showMessage('Load failed: ' + err);
}
} else {
- this.serializer.openProject(
- this.serializer.load(str, this),
- this
+ this.openProject(
+ this.serializer.load(str, this)
);
}
this.stopFastTracking();
@@ -5168,28 +5303,22 @@ IDE_Morph.prototype.openCloudDataString = function (str) {
};
IDE_Morph.prototype.rawOpenCloudDataString = function (str) {
- var model;
- StageMorph.prototype.hiddenPrimitives = {};
- StageMorph.prototype.codeMappings = {};
- StageMorph.prototype.codeHeaders = {};
- StageMorph.prototype.enableCodeMapping = false;
- StageMorph.prototype.enableInheritance = true;
- StageMorph.prototype.enableSublistIDs = false;
- StageMorph.prototype.enablePenLogging = false;
- Process.prototype.enableLiveCoding = false;
- this.trash = [];
- this.hasUnsavedEdits = false;
+ var model,
+ setting = this.isAddingScenes;
+
+ if (this.isAddingNextScene) {
+ this.isAddingScenes = true;
+ }
if (Process.prototype.isCatchingErrors) {
try {
model = this.serializer.parse(str);
this.serializer.loadMediaModel(model.childNamed('media'));
- this.serializer.openProject(
+ this.openProject(
this.serializer.loadProjectModel(
model.childNamed('project'),
this,
model.attributes.remixID
- ),
- this
+ )
);
} catch (err) {
this.showMessage('Load failed: ' + err);
@@ -5197,16 +5326,17 @@ IDE_Morph.prototype.rawOpenCloudDataString = function (str) {
} else {
model = this.serializer.parse(str);
this.serializer.loadMediaModel(model.childNamed('media'));
- this.serializer.openProject(
+ this.openProject(
this.serializer.loadProjectModel(
model.childNamed('project'),
this,
model.attributes.remixID
- ),
- this
+ )
);
}
this.stopFastTracking();
+ this.isAddingScenes = setting;
+ this.isAddingNextScene = false;
};
IDE_Morph.prototype.openBlocksString = function (str, name, silently) {
@@ -5385,7 +5515,7 @@ IDE_Morph.prototype.rawOpenDataString = function (str, name, type) {
}
};
-IDE_Morph.prototype.openProject = function (name) {
+IDE_Morph.prototype.openProjectName = function (name) {
var str;
if (name) {
this.showMessage('opening project\n' + name);
@@ -5396,6 +5526,56 @@ IDE_Morph.prototype.openProject = function (name) {
}
};
+IDE_Morph.prototype.openProject = function (project) {
+ if (this.isAddingScenes) {
+ project.scenes.itemsArray().forEach(scene => {
+ scene.name = this.newSceneName(scene.name, scene);
+ this.scenes.add(scene);
+ });
+ } else {
+ this.scenes = project.scenes;
+ }
+ this.switchToScene(
+ project.currentScene || project.scenes.at(1),
+ true // refresh album
+ );
+};
+
+IDE_Morph.prototype.switchToScene = function (scene, refreshAlbum) {
+ if (!scene || !scene.stage) {
+ return;
+ }
+ this.siblings().forEach(morph =>
+ morph.destroy()
+ );
+ this.scene.captureGlobalSettings();
+ this.scene = scene;
+ this.globalVariables = scene.globalVariables;
+ this.stage.destroy();
+ this.add(scene.stage);
+ this.stage = scene.stage;
+ this.sprites = scene.sprites;
+ this.stage.pauseGenericHatBlocks();
+ this.createCorral(!refreshAlbum); // keep scenes
+ this.selectSprite(this.scene.currentSprite);
+ this.corral.album.updateSelection();
+ this.corral.album.contents.children.forEach(function (morph) {
+ if (morph.state) {
+ morph.scrollIntoView();
+ }
+ });
+ scene.applyGlobalSettings();
+ this.world().keyboardFocus = this.stage;
+
+ if (this.currentCategory != 'unified' && scene.unifiedPalette) {
+ this.toggleUnifiedPalette();
+ } else if (this.currentCategory == 'unified' && !scene.unifiedPalette) {
+ this.toggleUnifiedPalette();
+ }
+
+ this.fixLayout();
+};
+
IDE_Morph.prototype.setURL = function (str) {
// Set the URL to a project's XML contents
location.hash = this.projectsInURLs ? str : '';
@@ -5553,17 +5733,17 @@ IDE_Morph.prototype.switchToDevMode = function () {
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.primitivesCache[category] = null;
this.stage.children.forEach(m => {
if (m instanceof SpriteMorph) {
- m.blocksCache[category] = null;
+ m.primitivesCache[category] = null;
}
});
} else {
- this.stage.blocksCache = {};
+ this.stage.primitivesCache = {};
this.stage.children.forEach(m => {
if (m instanceof SpriteMorph) {
- m.blocksCache = {};
+ m.primitivesCache = {};
}
});
}
@@ -5574,9 +5754,11 @@ 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.paletteCache.unified = null;
this.stage.children.forEach(m => {
if (m instanceof SpriteMorph) {
m.paletteCache[category] = null;
+ m.paletteCache.unified = null;
}
});
} else {
@@ -5616,23 +5798,9 @@ IDE_Morph.prototype.toggleZebraColoring = function () {
};
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);
+ this.refreshIDE();
};
IDE_Morph.prototype.toggleBlurredShadows = function () {
@@ -5844,6 +6012,22 @@ IDE_Morph.prototype.toggleStageSize = function (isSmall, forcedRatio) {
}
};
+IDE_Morph.prototype.toggleUnifiedPalette = function () {
+ this.scene.unifiedPalette = !this.scene.unifiedPalette;
+ if (this.scene.unifiedPalette) {
+ this.currentCategory = 'unified';
+ } else {
+ this.currentCategory = 'motion';
+ }
+
+ this.createCategories();
+ this.categories.fixLayout();
+ this.fixLayout();
+ this.flushBlocksCache();
+ this.currentSprite.palette(this.currentCategory);
+ this.refreshPalette(true);
+};
+
IDE_Morph.prototype.setPaletteWidth = function (newWidth) {
var msecs = this.isAnimating ? 100 : 0,
world = this.world();
@@ -5863,6 +6047,22 @@ IDE_Morph.prototype.createNewProject = function () {
this.backup(() => this.newProject());
};
+IDE_Morph.prototype.addScene = function () {
+ var setting = this.isAddingScenes;
+ if (location.protocol === 'file:') {
+ // bypass the project import dialog and directly pop up
+ // the local file picker.
+ // this should not be necessary, we should be able
+ // to access the cloud even when running Snap! locally
+ // to be worked on.... (jens)
+ this.isAddingScenes = true;
+ this.importLocalFile();
+ this.isAddingScenes = setting;
+ return;
+ }
+ new ProjectDialogMorph(this, 'add').popUp();
+};
+
IDE_Morph.prototype.openProjectsBrowser = function () {
if (location.protocol === 'file:') {
// bypass the project import dialog and directly pop up
@@ -5882,7 +6082,7 @@ IDE_Morph.prototype.saveProjectsBrowser = function () {
if (location.protocol === 'file:') {
this.prompt(
'Export Project As...',
- name => this.exportProject(name, false),
+ name => this.exportProject(name),
null,
'exportProject'
);
@@ -5988,12 +6188,16 @@ IDE_Morph.prototype.reflectLanguage = function (lang, callback, noSave) {
if (!this.loadNewProject) {
if (Process.prototype.isCatchingErrors) {
try {
- projectData = this.serializer.serialize(this.stage);
+ projectData = this.serializer.serialize(
+ new Project(this.scenes, this.scene)
+ );
} catch (err) {
this.showMessage('Serialization failed: ' + err);
}
} else {
- projectData = this.serializer.serialize(this.stage);
+ projectData = this.serializer.serialize(
+ new Project(this.scenes, this.scene)
+ );
}
}
SpriteMorph.prototype.initBlocks();
@@ -6096,12 +6300,16 @@ IDE_Morph.prototype.setBlocksScale = function (num) {
var projectData;
if (Process.prototype.isCatchingErrors) {
try {
- projectData = this.serializer.serialize(this.stage);
+ projectData = this.serializer.serialize(
+ new Project(this.scenes, this.scene)
+ );
} catch (err) {
this.showMessage('Serialization failed: ' + err);
}
} else {
- projectData = this.serializer.serialize(this.stage);
+ projectData = this.serializer.serialize(
+ new Project(this.scenes, this.scene)
+ );
}
SyntaxElementMorph.prototype.setScale(num);
CommentMorph.prototype.refreshScale();
@@ -6177,7 +6385,7 @@ IDE_Morph.prototype.userSetStageSize = function () {
this
).promptVector(
"Stage size",
- StageMorph.prototype.dimensions,
+ this.stage.dimensions,
new Point(480, 360),
'Stage width',
'Stage height',
@@ -6195,16 +6403,16 @@ IDE_Morph.prototype.setStageExtent = function (aPoint) {
function zoom() {
myself.step = function () {
var delta = ext.subtract(
- StageMorph.prototype.dimensions
+ myself.stage.dimensions
).divideBy(2);
if (delta.abs().lt(new Point(5, 5))) {
- StageMorph.prototype.dimensions = ext;
+ myself.stage.dimensions = ext;
delete myself.step;
} else {
- StageMorph.prototype.dimensions =
- StageMorph.prototype.dimensions.add(delta);
+ myself.stage.dimensions =
+ myself.stage.dimensions.add(delta);
}
- myself.stage.setExtent(StageMorph.prototype.dimensions);
+ myself.stage.setExtent(myself.stage.dimensions);
myself.stage.clearPenTrails();
myself.fixLayout();
this.setExtent(world.extent());
@@ -6219,8 +6427,8 @@ IDE_Morph.prototype.setStageExtent = function (aPoint) {
if (this.isAnimating) {
zoom();
} else {
- StageMorph.prototype.dimensions = ext;
- this.stage.setExtent(StageMorph.prototype.dimensions);
+ this.stage.dimensions = ext;
+ this.stage.setExtent(this.stage.dimensions);
this.stage.clearPenTrails();
this.fixLayout();
this.setExtent(world.extent());
@@ -6436,21 +6644,22 @@ IDE_Morph.prototype.logout = function () {
};
IDE_Morph.prototype.buildProjectRequest = function () {
- var xml = this.serializer.serialize(this.stage),
- thumbnail = normalizeCanvas(
- this.stage.thumbnail(
- SnapSerializer.prototype.thumbnailSize
- )).toDataURL(),
- body;
+ var proj = new Project(this.scenes, this.scene),
+ body,
+ xml;
this.serializer.isCollectingMedia = true;
+ xml = this.serializer.serialize(proj);
body = {
- notes: this.projectNotes,
+ notes: proj.notes,
xml: xml,
- media: this.hasChangedMedia ?
- this.serializer.mediaXML(this.projectName) : null,
- thumbnail: thumbnail,
- remixID: this.stage.remixID
+ /*
+ media: this.hasChangedMedia ? // incremental media upload, disabled
+ this.serializer.mediaXML(proj.name) : null,
+ */
+ media: this.serializer.mediaXML(proj.name),
+ thumbnail: proj.thumbnail.toDataURL(),
+ remixID: this.stage.remixID // +++ sceneify remixID
};
this.serializer.isCollectingMedia = false;
this.serializer.flushMedia();
@@ -6498,7 +6707,7 @@ IDE_Morph.prototype.saveProjectToCloud = function (name) {
var projectBody, projectSize;
if (name) {
- this.setProjectName(name);
+ name = this.setProjectName(name);
}
this.showMessage('Saving project\nto the cloud...');
@@ -6509,7 +6718,7 @@ IDE_Morph.prototype.saveProjectToCloud = function (name) {
'Uploading ' + Math.round(projectSize / 1024) + ' KB...'
);
this.cloud.saveProject(
- this.projectName,
+ name,
projectBody,
() => {
this.recordSavedChanges();
@@ -6526,8 +6735,9 @@ IDE_Morph.prototype.exportProjectMedia = function (name) {
this.setProjectName(name);
try {
menu = this.showMessage('Exporting');
+ this.serializer.serialize(new Project(this.scenes, this.scene));
media = this.serializer.mediaXML(name);
- this.saveXMLAs(media, this.projectName + ' media');
+ this.saveXMLAs(media, this.getProjectName() + ' media');
menu.destroy();
this.showMessage('Exported!', 1);
} catch (err) {
@@ -6548,12 +6758,14 @@ IDE_Morph.prototype.exportProjectNoMedia = function (name) {
var menu, str;
this.serializer.isCollectingMedia = true;
if (name) {
- this.setProjectName(name);
+ name = this.setProjectName(name);
if (Process.prototype.isCatchingErrors) {
try {
menu = this.showMessage('Exporting');
- str = this.serializer.serialize(this.stage);
- this.saveXMLAs(str, this.projectName);
+ str = this.serializer.serialize(
+ new Project(this.scenes, this.scene)
+ );
+ this.saveXMLAs(str, name);
menu.destroy();
this.showMessage('Exported!', 1);
} catch (err) {
@@ -6562,8 +6774,10 @@ IDE_Morph.prototype.exportProjectNoMedia = function (name) {
}
} else {
menu = this.showMessage('Exporting');
- str = this.serializer.serialize(this.stage);
- this.saveXMLAs(str, this.projectName);
+ str = this.serializer.serialize(
+ new Project(this.scenes, this.scene)
+ );
+ this.saveXMLAs(str, name);
menu.destroy();
this.showMessage('Exported!', 1);
}
@@ -6576,14 +6790,16 @@ IDE_Morph.prototype.exportProjectAsCloudData = function (name) {
var menu, str, media, dta;
this.serializer.isCollectingMedia = true;
if (name) {
- this.setProjectName(name);
+ name = this.setProjectName(name);
if (Process.prototype.isCatchingErrors) {
try {
menu = this.showMessage('Exporting');
- str = this.serializer.serialize(this.stage);
+ str = this.serializer.serialize(
+ new Project(this.scenes, this.scene)
+ );
media = this.serializer.mediaXML(name);
dta = '' + str + media + '';
- this.saveXMLAs(str, this.projectName);
+ this.saveXMLAs(dta, name);
menu.destroy();
this.showMessage('Exported!', 1);
} catch (err) {
@@ -6592,10 +6808,12 @@ IDE_Morph.prototype.exportProjectAsCloudData = function (name) {
}
} else {
menu = this.showMessage('Exporting');
- str = this.serializer.serialize(this.stage);
+ str = this.serializer.serialize(
+ new Project(this.scenes, this.scene)
+ );
media = this.serializer.mediaXML(name);
dta = '' + str + media + '';
- this.saveXMLAs(str, this.projectName);
+ this.saveXMLAs(str, name);
menu.destroy();
this.showMessage('Exported!', 1);
}
@@ -6885,12 +7103,23 @@ ProjectDialogMorph.prototype.init = function (ide, task) {
);
// override inherited properites:
- this.labelString = this.task === 'save' ? 'Save Project' : 'Open Project';
+ switch (this.task) {
+ case 'save':
+ this.labelString = 'Save Project';
+ break;
+ case 'add':
+ this.labelString = 'Add Scene';
+ break;
+ default: // 'open'
+ this.task = 'open';
+ this.labelString = 'Open Project';
+ }
+
this.createLabel();
this.key = 'project' + task;
// build contents
- if (task === 'open' && this.source === 'disk') {
+ if ((task === 'open' || task === 'add') && this.source === 'disk') {
// give the user a chance to switch to another source
this.source = null;
this.buildContents();
@@ -6933,7 +7162,7 @@ ProjectDialogMorph.prototype.buildContents = function () {
this.addSourceButton('cloud', localize('Cloud'), 'cloud');
}
- if (this.task === 'open') {
+ if (this.task === 'open' || this.task === 'add') {
this.buildFilterField();
this.addSourceButton('examples', localize('Examples'), 'poster');
if (this.hasLocalProjects() || this.ide.world().currentKey === 16) {
@@ -6947,7 +7176,7 @@ ProjectDialogMorph.prototype.buildContents = function () {
this.body.add(this.srcBar);
if (this.task === 'save') {
- this.nameField = new InputFieldMorph(this.ide.projectName);
+ this.nameField = new InputFieldMorph(this.ide.getProjectName());
this.body.add(this.nameField);
}
@@ -6987,7 +7216,7 @@ ProjectDialogMorph.prototype.buildContents = function () {
this.body.add(this.preview);
if (this.task === 'save') {
- thumbnail = this.ide.stage.thumbnail(
+ thumbnail = this.ide.scenes.at(1).stage.thumbnail(
SnapSerializer.prototype.thumbnailSize
);
this.preview.texture = null;
@@ -7008,10 +7237,10 @@ ProjectDialogMorph.prototype.buildContents = function () {
this.notesField.acceptsDrops = false;
this.notesField.contents.acceptsDrops = false;
- if (this.task === 'open') {
+ if (this.task === 'open' || this.task === 'add') {
this.notesText = new TextMorph('');
} else { // 'save'
- this.notesText = new TextMorph(this.ide.projectNotes);
+ this.notesText = new TextMorph(this.ide.getProjectNotes());
this.notesText.isEditable = true;
this.notesText.enableSelecting();
}
@@ -7028,6 +7257,11 @@ ProjectDialogMorph.prototype.buildContents = function () {
this.action = 'openProject';
this.recoverButton = this.addButton('recoveryDialog', 'Recover', true);
this.recoverButton.hide();
+ } else if (this.task === 'add') {
+ this.addButton('addScene', 'Add');
+ this.action = 'addScene';
+ this.recoverButton = this.addButton('recoveryDialog', 'Recover', true);
+ this.recoverButton.hide();
} else { // 'save'
this.addButton('saveProject', 'Save');
this.action = 'saveProject';
@@ -7256,7 +7490,7 @@ ProjectDialogMorph.prototype.buildFilterField = function () {
// ProjectDialogMorph ops
ProjectDialogMorph.prototype.setSource = function (source) {
- var msg;
+ var msg, setting;
this.source = source;
this.srcBar.children.forEach(button =>
@@ -7293,7 +7527,14 @@ ProjectDialogMorph.prototype.setSource = function (source) {
this.projectList = [];
} else {
this.destroy();
- this.ide.importLocalFile();
+ if (this.task === 'add') {
+ setting = this.ide.isAddingScenes;
+ this.ide.isAddingScenes = true;
+ this.ide.importLocalFile();
+ this.ide.isAddingScenes = setting;
+ } else {
+ this.ide.importLocalFile();
+ }
return;
}
break;
@@ -7368,7 +7609,7 @@ ProjectDialogMorph.prototype.setSource = function (source) {
this.shareButton.hide();
this.unshareButton.hide();
- if (this.task === 'open') {
+ if (this.task === 'open' || this.task === 'add') {
this.recoverButton.hide();
}
@@ -7381,7 +7622,7 @@ ProjectDialogMorph.prototype.setSource = function (source) {
}
this.buttons.fixLayout();
this.fixLayout();
- if (this.task === 'open') {
+ if (this.task === 'open' || this.task === 'add') {
this.clearDetails();
}
};
@@ -7458,7 +7699,7 @@ ProjectDialogMorph.prototype.installCloudProjectList = function (pl) {
if (this.nameField) {
this.nameField.setContents(item.projectname || '');
}
- if (this.task === 'open') {
+ if (this.task === 'open' || this.task === 'add') {
this.notesText.text = item.notes || '';
this.notesText.rerender();
this.notesField.contents.adjustBounds();
@@ -7506,7 +7747,7 @@ ProjectDialogMorph.prototype.installCloudProjectList = function (pl) {
this.edit();
};
this.body.add(this.listField);
- if (this.task === 'open') {
+ if (this.task === 'open' || this.task === 'add') {
this.recoverButton.show();
}
this.shareButton.show();
@@ -7514,7 +7755,7 @@ ProjectDialogMorph.prototype.installCloudProjectList = function (pl) {
this.deleteButton.show();
this.buttons.fixLayout();
this.fixLayout();
- if (this.task === 'open') {
+ if (this.task === 'open' || this.task === 'add') {
this.clearDetails();
}
};
@@ -7536,6 +7777,27 @@ ProjectDialogMorph.prototype.recoveryDialog = function () {
new ProjectRecoveryDialogMorph(this.ide, proj.projectname, this).popUp();
};
+ProjectDialogMorph.prototype.addScene = function () {
+ var proj = this.listField.selected,
+ src;
+ if (!proj) {return; }
+ this.ide.isAddingNextScene = true;
+ this.ide.source = this.source;
+ if (this.source === 'cloud') {
+ this.addCloudScene(proj);
+ } else if (this.source === 'examples') {
+ // Note "file" is a property of the parseResourceFile function.
+ src = this.ide.getURL(this.ide.resourceURL('Examples', proj.fileName));
+ this.ide.openProjectString(src);
+ this.destroy();
+
+ } else { // 'local'
+ this.ide.source = null;
+ this.ide.openProjectName(proj.name);
+ this.destroy();
+ }
+};
+
ProjectDialogMorph.prototype.openProject = function () {
var proj = this.listField.selected,
src;
@@ -7551,11 +7813,19 @@ ProjectDialogMorph.prototype.openProject = function () {
} else { // 'local'
this.ide.source = null;
- this.ide.backup(() => this.ide.openProject(proj.name));
+ this.ide.backup(() => this.ide.openProjectName(proj.name));
this.destroy();
}
};
+ProjectDialogMorph.prototype.addCloudScene = function (project, delta) {
+ // no need to backup
+ this.ide.nextSteps([
+ () => this.ide.showMessage('Fetching project\nfrom the cloud...'),
+ () => this.rawOpenCloudProject(project, delta)
+ ]);
+};
+
ProjectDialogMorph.prototype.openCloudProject = function (project, delta) {
this.ide.backup(
() => {
@@ -7593,8 +7863,8 @@ ProjectDialogMorph.prototype.saveProject = function () {
var name = this.nameField.contents().text.text,
notes = this.notesText.text;
- if (this.ide.projectNotes !== notes) {
- this.ide.projectNotes = notes;
+ if (this.ide.getProjectNotes() !== notes) {
+ this.ide.setProjectNotes(notes);
}
if (name) {
if (this.source === 'cloud') {
@@ -7617,7 +7887,7 @@ ProjectDialogMorph.prototype.saveProject = function () {
this.saveCloudProject();
}
} else if (this.source === 'disk') {
- this.ide.exportProject(name, false);
+ this.ide.exportProject(name);
this.ide.source = 'disk';
this.destroy();
}
@@ -7704,7 +7974,7 @@ ProjectDialogMorph.prototype.shareProject = function () {
this.ide.showMessage('shared.', 2);
// Set the Shared URL if the project is currently open
- if (proj.projectname === ide.projectName) {
+ if (proj.projectname === ide.getProjectName()) {
var usr = ide.cloud.username,
projectId = 'Username=' +
encodeURIComponent(usr.toLowerCase()) +
@@ -7748,7 +8018,7 @@ ProjectDialogMorph.prototype.unshareProject = function () {
this.buttons.fixLayout();
this.rerender();
this.ide.showMessage('unshared.', 2);
- if (proj.projectname === ide.projectName) {
+ if (proj.projectname === ide.getProjectName()) {
location.hash = '';
}
},
@@ -7788,7 +8058,7 @@ ProjectDialogMorph.prototype.publishProject = function () {
this.ide.showMessage('published.', 2);
// Set the Shared URL if the project is currently open
- if (proj.projectname === ide.projectName) {
+ if (proj.projectname === ide.getProjectName()) {
var usr = ide.cloud.username,
projectId = 'Username=' +
encodeURIComponent(usr.toLowerCase()) +
@@ -8889,7 +9159,7 @@ SpriteIconMorph.prototype.prepareToBeGrabbed = function () {
if (ide) {
idx = ide.sprites.asArray().indexOf(this.object);
ide.sprites.remove(idx + 1);
- ide.createCorral();
+ ide.createCorral(true); // keep scenes
ide.fixLayout();
}
};
@@ -9459,7 +9729,7 @@ WardrobeMorph.prototype.init = function (aSprite, sliderColor) {
WardrobeMorph.uber.init.call(this, null, null, sliderColor);
// configure inherited properties
- this.fps = 2;
+ // this.fps = 2; // commented out to make scrollbars more responsive
this.updateList();
};
@@ -9581,7 +9851,9 @@ WardrobeMorph.prototype.updateList = function () {
WardrobeMorph.prototype.updateSelection = function () {
this.contents.children.forEach(function (morph) {
- if (morph.refresh) {morph.refresh(); }
+ if (morph.refresh) {
+ morph.refresh();
+ }
});
this.spriteVersion = this.sprite.version;
};
@@ -9606,11 +9878,14 @@ WardrobeMorph.prototype.removeCostumeAt = function (idx) {
};
WardrobeMorph.prototype.paintNew = function () {
- var cos = new Costume(
+ var ide = this.parentThatIsA(IDE_Morph),
+ cos = new Costume(
newCanvas(null, true),
- this.sprite.newCostumeName(localize('Untitled'))
- ),
- ide = this.parentThatIsA(IDE_Morph);
+ this.sprite.newCostumeName(localize('Untitled')),
+ null, // rotation center
+ null, // don't shrink-to-fit
+ ide.stage.dimensions // max extent
+ );
cos.edit(
this.world(),
@@ -9922,7 +10197,7 @@ JukeboxMorph.prototype.init = function (aSprite, sliderColor) {
// configure inherited properties
this.acceptsDrops = false;
- this.fps = 2;
+ // this.fps = 2; // commented out to make scrollbars more responsive
this.updateList();
};
@@ -9991,7 +10266,9 @@ JukeboxMorph.prototype.updateList = function () {
JukeboxMorph.prototype.updateSelection = function () {
this.contents.children.forEach(morph => {
- if (morph.refresh) {morph.refresh(); }
+ if (morph.refresh) {
+ morph.refresh();
+ }
});
this.spriteVersion = this.sprite.version;
};
@@ -10037,6 +10314,363 @@ JukeboxMorph.prototype.reactToDropOf = function (icon) {
this.updateList();
};
+// SceneIconMorph ////////////////////////////////////////////////////
+
+/*
+ I am a selectable element in a SceneAlbum, keeping
+ a self-updating thumbnail of the scene I'm respresenting, and a
+ self-updating label of the scene's name (in case it is changed
+ elsewhere)
+*/
+
+// SceneIconMorph inherits from ToggleButtonMorph (Widgets)
+// ... and copies methods from SpriteIconMorph
+
+SceneIconMorph.prototype = new ToggleButtonMorph();
+SceneIconMorph.prototype.constructor = SceneIconMorph;
+SceneIconMorph.uber = ToggleButtonMorph.prototype;
+
+// SceneIconMorph settings
+
+SceneIconMorph.prototype.thumbSize = new Point(40, 30);
+SceneIconMorph.prototype.labelShadowOffset = null;
+SceneIconMorph.prototype.labelShadowColor = null;
+SceneIconMorph.prototype.labelColor = WHITE;
+SceneIconMorph.prototype.fontSize = 9;
+
+// SceneIconMorph instance creation:
+
+function SceneIconMorph(aScene) {
+ this.init(aScene);
+}
+
+SceneIconMorph.prototype.init = function (aScene) {
+ var colors, action, query;
+
+ colors = [
+ IDE_Morph.prototype.frameColor,
+ IDE_Morph.prototype.groupColor,
+ IDE_Morph.prototype.groupColor
+ ];
+
+ action = () => {
+ // make my scene the current one
+ var ide = this.parentThatIsA(IDE_Morph),
+ album = this.parentThatIsA(SceneAlbumMorph);
+ album.scene = this.object;
+ ide.switchToScene(this.object);
+ };
+
+ query = () => {
+ // answer true if my scene is the current one
+ var album = this.parentThatIsA(SceneAlbumMorph);
+ if (album) {
+ return album.scene === this.object;
+ }
+ return false;
+ };
+
+ // additional properties:
+ this.object = aScene || new Scene(); // mandatory, actually
+ this.version = this.object.stage.version;
+ this.thumbnail = null;
+
+ // initialize inherited properties:
+ SceneIconMorph.uber.init.call(
+ this,
+ colors, // color overrides, : [normal, highlight, pressed]
+ null, // target - not needed here
+ action, // a toggle function
+ this.object.name || localize('untitled'), // label string
+ query, // predicate/selector
+ null, // environment
+ null // hint
+ );
+
+ // override defaults and build additional components
+ this.isDraggable = true;
+ this.createThumbnail();
+ this.padding = 2;
+ this.corner = 8;
+ this.fixLayout();
+ this.fps = 1;
+};
+
+SceneIconMorph.prototype.createThumbnail = function () {
+ if (this.thumbnail) {
+ this.thumbnail.destroy();
+ }
+
+ this.thumbnail = new Morph();
+ this.thumbnail.isCachingImage = true;
+ this.thumbnail.bounds.setExtent(this.thumbSize);
+ this.thumbnail.cachedImage = this.object.stage.thumbnail(
+ this.thumbSize,
+ this.thumbnail.cachedImage
+ );
+ this.add(this.thumbnail);
+};
+
+SceneIconMorph.prototype.createLabel = function () {
+ var txt;
+
+ if (this.label) {
+ this.label.destroy();
+ }
+ txt = new StringMorph(
+ this.object.name || localize('untitled'),
+ this.fontSize,
+ this.fontStyle,
+ false, // 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);
+};
+
+// SceneIconMorph stepping
+
+SceneIconMorph.prototype.step = function () {
+ if (this.version !== this.object.stage.version) {
+ this.createThumbnail();
+ this.createLabel();
+ this.fixLayout();
+ this.version = this.object.stage.version;
+ this.refresh();
+ }
+};
+
+// SceneIconMorph layout
+
+SceneIconMorph.prototype.fixLayout
+ = SpriteIconMorph.prototype.fixLayout;
+
+// SceneIconMorph menu
+
+SceneIconMorph.prototype.userMenu = function () {
+ var menu = new MenuMorph(this);
+ if (!(this.object instanceof Scene)) {
+ return null;
+ }
+ if (!this.isProjectScene()) {
+ menu.addItem("rename", "renameScene");
+ menu.addItem("delete", "removeScene");
+ }
+ menu.addItem("export", "exportScene");
+ return menu;
+};
+
+SceneIconMorph.prototype.renameScene = function () {
+ var scene = this.object,
+ ide = this.parentThatIsA(IDE_Morph);
+ new DialogBoxMorph(
+ null,
+ answer => {
+ if (answer && (answer !== scene.name)) {
+ scene.name = ide.newSceneName(
+ answer,
+ scene
+ );
+ scene.stage.version = Date.now(); // +++ also do this in other places
+ if (scene === ide.scene) {
+ ide.controlBar.updateLabel();
+ }
+ ide.recordUnsavedChanges(); // ++++ sceneify unsaved changes
+ }
+ }
+ ).prompt(
+ 'rename scene',
+ scene.name,
+ this.world()
+ );
+};
+
+SceneIconMorph.prototype.removeScene = function () {
+ var album = this.parentThatIsA(SceneAlbumMorph),
+ idx = this.parent.children.indexOf(this) + 1,
+ off = 0, // 2,
+ ide = this.parentThatIsA(IDE_Morph);
+ album.removeSceneAt(idx - off); // ignore buttons
+ if (ide.scene === this.object) {
+ ide.switchToScene(ide.scenes.at(1));
+ }
+};
+
+SceneIconMorph.prototype.exportScene = function () {
+ // Export scene as project XML, saving a file to disk
+ var menu, str,
+ ide = this.parentThatIsA(IDE_Morph),
+ name = this.object.name || localize('untitled');
+
+ try {
+ menu = ide.showMessage('Exporting');
+ str = ide.serializer.serialize(
+ new Project(new List([this.object]), this.object)
+ );
+ ide.saveXMLAs(str, name);
+ menu.destroy();
+ ide.showMessage('Exported!', 1);
+ } catch (err) {
+ if (Process.prototype.isCatchingErrors) {
+ ide.showMessage('Export failed: ' + err);
+ } else {
+ throw err;
+ }
+ }
+};
+
+// SceneIconMorph ops
+
+SceneIconMorph.prototype.isProjectScene = function (anIDE) {
+ // the first scene of a project cannot be renamed, deleted or rearranged,
+ // because its name and project notes are those of the project
+ var ide = anIDE || this.parentThatIsA(IDE_Morph);
+ return ide.scenes.indexOf(this.object) === 1;
+};
+
+// SceneIconMorph drawing
+
+SceneIconMorph.prototype.render
+ = SpriteIconMorph.prototype.render;
+
+// SceneIconMorph drag & drop
+
+SceneIconMorph.prototype.rootForGrab = function () {
+ return this;
+};
+
+SceneIconMorph.prototype.prepareToBeGrabbed = function () {
+ this.mouseClickLeft(); // select me
+ this.removeScene();
+};
+
+// SceneAlbumMorph ///////////////////////////////////////////////////////
+
+// I am a watcher on a project's scenes list
+
+// SceneAlbumMorph inherits from ScrollFrameMorph
+
+SceneAlbumMorph.prototype = new ScrollFrameMorph();
+SceneAlbumMorph.prototype.constructor = SceneAlbumMorph;
+SceneAlbumMorph.uber = ScrollFrameMorph.prototype;
+
+// SceneAlbumMorph instance creation:
+
+function SceneAlbumMorph(anIDE, sliderColor) {
+ this.init(anIDE, sliderColor);
+}
+
+SceneAlbumMorph.prototype.init = function (anIDE, sliderColor) {
+ // additional properties
+ this.ide = anIDE;
+ this.scene = anIDE.scene;
+ this.version = null;
+
+ // initialize inherited properties
+ SceneAlbumMorph.uber.init.call(this, null, null, sliderColor);
+
+ // configure inherited properties
+ // this.fps = 2; // commented out to make scrollbars more responsive
+ this.updateList();
+ this.updateSelection();
+};
+
+// SceneAlbumMorph updating
+
+SceneAlbumMorph.prototype.updateList = function () {
+ var x = this.left() + 5,
+ y = this.top() + 5,
+ padding = 4,
+ oldPos = this.contents.position(),
+ icon;
+
+ this.changed();
+
+ this.contents.destroy();
+ this.contents = new FrameMorph(this);
+ this.contents.acceptsDrops = false;
+ this.contents.reactToDropOf = (icon) => {
+ this.reactToDropOf(icon);
+ };
+ this.addBack(this.contents);
+
+ this.ide.scenes.asArray().forEach((scene, i) => {
+ icon = new SceneIconMorph(scene);
+ if (i < 1) {
+ icon.isDraggable = false; // project scene cannot be rearranged
+ }
+ icon.setPosition(new Point(x, y));
+ this.addContents(icon);
+ y = icon.bottom() + padding;
+ });
+ this.version = this.ide.scenes.lastChanged;
+
+ this.contents.setPosition(oldPos);
+ this.adjustScrollBars();
+ this.changed();
+
+ this.updateSelection();
+};
+
+SceneAlbumMorph.prototype.updateSelection = function () {
+ this.scene = this.ide.scene;
+ this.contents.children.forEach(function (morph) {
+ if (morph.refresh) {
+ morph.refresh();
+ }
+ });
+};
+
+// SceneAlbumMorph stepping
+
+SceneAlbumMorph.prototype.step = function () {
+ if (this.version !== this.ide.scenes.lastChanged) {
+ this.updateList();
+ }
+ if (this.scene !== this.ide.scene) {
+ this.updateSelection();
+ }
+};
+
+// Wardrobe ops
+
+SceneAlbumMorph.prototype.removeSceneAt = function (idx) {
+ this.ide.scenes.remove(idx);
+ this.updateList();
+};
+
+// SceneAlbumMorph drag & drop
+
+SceneAlbumMorph.prototype.wantsDropOf = function (morph) {
+ return morph instanceof SceneIconMorph;
+};
+
+SceneAlbumMorph.prototype.reactToDropOf = function (icon) {
+ var idx = 0,
+ scene = icon.object,
+ top = icon.top();
+ icon.destroy();
+ this.contents.children.forEach(item => {
+ if (item instanceof SceneIconMorph && item.top() < top - 4) {
+ idx += 1;
+ }
+ });
+ idx = Math.max(idx, 1); // the project scene cannot the rearranged
+ this.ide.scenes.add(scene, idx + 1);
+ this.updateList();
+ icon.mouseClickLeft(); // select
+};
+
// StageHandleMorph ////////////////////////////////////////////////////////
// I am a horizontal resizing handle for a StageMorph
@@ -10419,7 +11053,9 @@ CamSnapshotDialogMorph.prototype.ok = function () {
this.accept(
new Costume(
this.videoView.fullImage(),
- this.sprite.newCostumeName('camera')
+ this.sprite.newCostumeName('camera'),
+ null,
+ true // no shrink-wrap
).flipped()
);
};
@@ -10522,7 +11158,7 @@ SoundRecorderDialogMorph.prototype.buildContents = function () {
audio: {
channelCount: 1 // force mono, currently only works on FF
}
-
+
}
).then(stream => {
this.mediaRecorder = new MediaRecorder(stream);
diff --git a/src/morphic.js b/src/morphic.js
index 3cd4ea53..b28267b3 100644
--- a/src/morphic.js
+++ b/src/morphic.js
@@ -678,6 +678,15 @@
droppedBinary(anArrayBuffer, name)
+ In case multiple files are dropped simulateneously the events
+
+ beginBulkDrop()
+ endBulkDrop()
+
+ are dispatched to to Morphs interested in bracketing the bulk operation,
+ and the endBulkDrop() event is only signalled after the contents last file
+ has been asynchronously made available.
+
(e) keyboard events
-------------------
@@ -11583,6 +11592,9 @@ HandMorph.prototype.processMouseScroll = function (event) {
droppedSVG
droppedAudio
droppedText
+
+ beginBulkDrop
+ endBulkDrop
*/
HandMorph.prototype.processDrop = function (event) {
@@ -11596,11 +11608,20 @@ HandMorph.prototype.processDrop = function (event) {
droppedAudio(audio, name)
droppedText(text, name, type)
- events to interested Morphs at the mouse pointer
+ events to interested Morphs at the mouse pointer.
+
+ In case multiple files are dropped simulateneously also displatch
+ the events
+
+ beginBulkDrop()
+ endBulkDrop()
+
+ to Morphs interested in bracketing the bulk operation
*/
var files = event instanceof FileList ? event
: event.target.files || event.dataTransfer.files,
file,
+ fileCount,
url = event.dataTransfer ?
event.dataTransfer.getData('URL') : null,
txt = event.dataTransfer ?
@@ -11614,11 +11635,15 @@ HandMorph.prototype.processDrop = function (event) {
function readSVG(aFile) {
var pic = new Image(),
- frd = new FileReader();
- while (!target.droppedSVG) {
- target = target.parent;
+ frd = new FileReader(),
+ trg = target;
+ while (!trg.droppedSVG) {
+ trg = trg.parent;
}
- pic.onload = () => target.droppedSVG(pic, aFile.name);
+ pic.onload = () => {
+ trg.droppedSVG(pic, aFile.name);
+ bulkDrop();
+ };
frd = new FileReader();
frd.onloadend = (e) => pic.src = e.target.result;
frd.readAsDataURL(aFile);
@@ -11626,14 +11651,16 @@ HandMorph.prototype.processDrop = function (event) {
function readImage(aFile) {
var pic = new Image(),
- frd = new FileReader();
- while (!target.droppedImage) {
- target = target.parent;
+ frd = new FileReader(),
+ trg = target;
+ while (!trg.droppedImage) {
+ trg = trg.parent;
}
pic.onload = () => {
canvas = newCanvas(new Point(pic.width, pic.height), true);
canvas.getContext('2d').drawImage(pic, 0, 0);
- target.droppedImage(canvas, aFile.name);
+ trg.droppedImage(canvas, aFile.name);
+ bulkDrop();
};
frd = new FileReader();
frd.onloadend = (e) => pic.src = e.target.result;
@@ -11642,39 +11669,64 @@ HandMorph.prototype.processDrop = function (event) {
function readAudio(aFile) {
var snd = new Audio(),
- frd = new FileReader();
- while (!target.droppedAudio) {
- target = target.parent;
+ frd = new FileReader(),
+ trg = target;
+ while (!trg.droppedAudio) {
+ trg = trg.parent;
}
frd.onloadend = (e) => {
snd.src = e.target.result;
- target.droppedAudio(snd, aFile.name);
+ trg.droppedAudio(snd, aFile.name);
+ bulkDrop();
};
frd.readAsDataURL(aFile);
}
function readText(aFile) {
- var frd = new FileReader();
- while (!target.droppedText) {
- target = target.parent;
+ var frd = new FileReader(),
+ trg = target;
+ while (!trg.droppedText) {
+ trg = trg.parent;
}
frd.onloadend = (e) => {
- target.droppedText(e.target.result, aFile.name, aFile.type);
+ trg.droppedText(e.target.result, aFile.name, aFile.type);
+ bulkDrop();
};
frd.readAsText(aFile);
}
function readBinary(aFile) {
- var frd = new FileReader();
- while (!target.droppedBinary) {
- target = target.parent;
+ var frd = new FileReader(),
+ trg = target;
+ while (!trg.droppedBinary) {
+ trg = trg.parent;
}
frd.onloadend = (e) => {
- target.droppedBinary(e.target.result, aFile.name);
+ trg.droppedBinary(e.target.result, aFile.name);
+ bulkDrop();
};
frd.readAsArrayBuffer(aFile);
}
+ function beginBulkDrop() {
+ var trg = target;
+ while (!trg.beginBulkDrop) {
+ trg = trg.parent;
+ }
+ trg.beginBulkDrop();
+ }
+
+ function bulkDrop() {
+ var trg = target;
+ fileCount -= 1;
+ if (files.length > 1 && fileCount === 0) {
+ while (!trg.endBulkDrop) {
+ trg = trg.parent;
+ }
+ trg.endBulkDrop();
+ }
+ }
+
function readURL(url, callback) {
var request = new XMLHttpRequest();
request.open('GET', url);
@@ -11708,6 +11760,10 @@ HandMorph.prototype.processDrop = function (event) {
}
if (files.length > 0) {
+ fileCount = files.length;
+ if (fileCount > 1) {
+ beginBulkDrop();
+ }
for (i = 0; i < files.length; i += 1) {
file = files[i];
suffix = file.name.slice(
@@ -12250,13 +12306,17 @@ WorldMorph.prototype.wantsDropOf = function () {
return this.acceptsDrops;
};
-WorldMorph.prototype.droppedImage = function () {
- return null;
-};
+WorldMorph.prototype.droppedImage = nop;
-WorldMorph.prototype.droppedSVG = function () {
- return null;
-};
+WorldMorph.prototype.droppedSVG = nop;
+
+WorldMorph.prototype.droppedAudio = nop;
+
+WorldMorph.prototype.droppedText;
+
+WorldMorph.prototype.beginBulkDrop = nop;
+
+WorldMorph.prototype.endBulkDrop = nop;
// WorldMorph text field tabbing:
diff --git a/src/objects.js b/src/objects.js
index d70502f5..c12703bb 100644
--- a/src/objects.js
+++ b/src/objects.js
@@ -84,7 +84,7 @@ BlockEditorMorph, BlockDialogMorph, PrototypeHatBlockMorph, BooleanSlotMorph,
localize, TableMorph, TableFrameMorph, normalizeCanvas, VectorPaintEditorMorph,
AlignmentMorph, Process, WorldMap, copyCanvas, useBlurredShadows*/
-modules.objects = '2021-June-14';
+modules.objects = '2021-July-02';
var SpriteMorph;
var StageMorph;
@@ -426,6 +426,12 @@ SpriteMorph.prototype.initBlocks = function () {
spec: 'go back %n layers',
defaults: [1]
},
+ doSwitchToScene: {
+ type: 'command',
+ category: 'looks',
+ spec: 'switch to scene %scn',
+ defaults: [['next']]
+ },
// Looks - Debugging primitives for development mode
doScreenshot: {
@@ -850,7 +856,7 @@ SpriteMorph.prototype.initBlocks = function () {
doTellTo: {
type: 'command',
category: 'control',
- // spec: 'tell %spr to %cl' // I liked this version much better, -Jens
+ // spec: 'tell %spr to %cl' // I liked this version better, -Jens
spec: 'tell %spr to %cmdRing %inputs'
},
reportAskFor: {
@@ -1699,7 +1705,7 @@ SpriteMorph.prototype.blockAlternatives = {
changeBackgroundHSVA: ['setBackgroundHSVA'],
changeSize: ['setSize'],
setSize: ['changeSize'],
-
+
// control:
doBroadcast: ['doBroadcastAndWait', 'doSend'],
doBroadcastAndWait: ['doBroadcast', 'doSend'],
@@ -1831,7 +1837,7 @@ SpriteMorph.prototype.init = function (globals) {
this.rotatesWithAnchor = true;
this.layers = null; // cache for dragging nested sprites, don't serialize
- this.blocksCache = {}; // not to be serialized (!)
+ this.primitivesCache = {}; // not to be serialized (!)
this.paletteCache = {}; // not to be serialized (!)
this.rotationOffset = ZERO; // not to be serialized (!)
this.idx = 0; // not to be serialized (!) - used for de-serialization
@@ -1896,7 +1902,7 @@ SpriteMorph.prototype.fullCopy = function (forClone) {
c.gainNode = null;
c.pannerNode = null;
c.freqPlayer = null;
- c.blocksCache = {};
+ c.primitivesCache = {};
c.paletteCache = {};
c.imageData = {};
c.cachedHSV = c.color.hsv();
@@ -2103,7 +2109,7 @@ SpriteMorph.prototype.fixLayout = function () {
this.setCenter(currentCenter, true); // just me
this.rotationOffset = this.extent().divideBy(2);
}
- };
+};
SpriteMorph.prototype.render = function (ctx) {
var myself = this,
@@ -2279,9 +2285,8 @@ SpriteMorph.prototype.variableBlock = function (varName, isLocalTemplate) {
// SpriteMorph block templates
-SpriteMorph.prototype.blockTemplates = function (category) {
- var blocks = [], myself = this, varNames, button,
- cat = category || 'motion', txt,
+SpriteMorph.prototype.blockTemplates = function (category = 'motion') {
+ var blocks = [], myself = this, varNames,
inheritedVars = this.inheritedVariableNames();
function block(selector, isGhosted) {
@@ -2342,29 +2347,7 @@ SpriteMorph.prototype.blockTemplates = function (category) {
);
}
- function helpMenu() {
- var menu = new MenuMorph(this);
- menu.addItem('help...', 'showHelp');
- return menu;
- }
-
- function addVar(pair) {
- var ide;
- if (pair) {
- if (myself.isVariableNameInUse(pair[0], pair[1])) {
- myself.inform('that name is already in use');
- } else {
- ide = myself.parentThatIsA(IDE_Morph);
- myself.addVariable(pair[0], pair[1]);
- myself.toggleVariableWatcher(pair[0], pair[1]);
- ide.flushBlocksCache('variables'); // b/c of inheritance
- ide.refreshPalette();
- ide.recordUnsavedChanges();
- }
- }
- }
-
- if (cat === 'motion') {
+ if (category === 'motion') {
blocks.push(block('forward'));
blocks.push(block('turn'));
@@ -2390,10 +2373,8 @@ SpriteMorph.prototype.blockTemplates = function (category) {
blocks.push(block('yPosition', this.inheritsAttribute('y position')));
blocks.push(watcherToggle('direction'));
blocks.push(block('direction', this.inheritsAttribute('direction')));
- blocks.push('=');
- blocks.push(this.makeBlockButton(cat));
- } else if (cat === 'looks') {
+ } else if (category === 'looks') {
blocks.push(block('doSwitchToCostume'));
blocks.push(block('doWearNextCostume'));
@@ -2426,17 +2407,13 @@ SpriteMorph.prototype.blockTemplates = function (category) {
blocks.push('-');
blocks.push(block('goToLayer'));
blocks.push(block('goBack'));
+ blocks.push('-');
+ blocks.push(block('doSwitchToScene'));
- // for debugging: ///////////////
-
+ // for debugging: ///////////////
if (this.world().isDevMode) {
blocks.push('-');
- txt = new TextMorph(localize(
- 'development mode \ndebugging primitives:'
- ));
- txt.fontSize = 9;
- txt.setColor(this.paletteTextColor);
- blocks.push(txt);
+ blocks.push(this.devModeText());
blocks.push('-');
blocks.push(block('log'));
blocks.push(block('alert'));
@@ -2444,12 +2421,7 @@ SpriteMorph.prototype.blockTemplates = function (category) {
blocks.push(block('doScreenshot'));
}
- /////////////////////////////////
-
- blocks.push('=');
- blocks.push(this.makeBlockButton(cat));
-
- } else if (cat === 'sound') {
+ } else if (category === 'sound') {
blocks.push(block('playSound'));
blocks.push(block('doPlaySoundUntilDone'));
@@ -2481,26 +2453,15 @@ SpriteMorph.prototype.blockTemplates = function (category) {
blocks.push(block('playFreq'));
blocks.push(block('stopFreq'));
- // for debugging: ///////////////
-
+ // for debugging: ///////////////
if (this.world().isDevMode) {
blocks.push('-');
- txt = new TextMorph(localize(
- 'development mode \ndebugging primitives:'
- ));
- txt.fontSize = 9;
- txt.setColor(this.paletteTextColor);
- blocks.push(txt);
+ blocks.push(this.devModeText());
blocks.push('-');
blocks.push(block('doPlayFrequency'));
}
- /////////////////////////////////
-
- blocks.push('=');
- blocks.push(this.makeBlockButton(cat));
-
- } else if (cat === 'pen') {
+ } else if (category === 'pen') {
blocks.push(block('clear'));
blocks.push('-');
@@ -2525,10 +2486,8 @@ SpriteMorph.prototype.blockTemplates = function (category) {
blocks.push('-');
blocks.push(block('doPasteOn'));
blocks.push(block('doCutFrom'));
- blocks.push('=');
- blocks.push(this.makeBlockButton(cat));
- } else if (cat === 'control') {
+ } else if (category === 'control') {
blocks.push(block('receiveGo'));
blocks.push(block('receiveKey'));
@@ -2575,10 +2534,8 @@ SpriteMorph.prototype.blockTemplates = function (category) {
blocks.push(block('removeClone'));
blocks.push('-');
blocks.push(block('doPauseAll'));
- blocks.push('=');
- blocks.push(this.makeBlockButton(cat));
- } else if (cat === 'sensing') {
+ } else if (category === 'sensing') {
blocks.push(block('reportTouchingObject'));
blocks.push(block('reportTouchingColor'));
@@ -2621,17 +2578,10 @@ SpriteMorph.prototype.blockTemplates = function (category) {
blocks.push('-');
blocks.push(block('reportDate'));
- // for debugging: ///////////////
-
+ // for debugging: ///////////////
if (this.world().isDevMode) {
-
blocks.push('-');
- txt = new TextMorph(localize(
- 'development mode \ndebugging primitives:'
- ));
- txt.fontSize = 9;
- txt.setColor(this.paletteTextColor);
- blocks.push(txt);
+ blocks.push(this.devModeText());
blocks.push('-');
blocks.push(watcherToggle('reportThreadCount'));
blocks.push(block('reportThreadCount'));
@@ -2639,13 +2589,7 @@ SpriteMorph.prototype.blockTemplates = function (category) {
blocks.push(block('reportFrameCount'));
blocks.push(block('reportYieldCount'));
}
-
- /////////////////////////////////
-
- blocks.push('=');
- blocks.push(this.makeBlockButton(cat));
-
- } else if (cat === 'operators') {
+ } else if (category === 'operators') {
blocks.push(block('reifyScript'));
blocks.push(block('reifyReporter'));
@@ -2687,84 +2631,24 @@ SpriteMorph.prototype.blockTemplates = function (category) {
blocks.push('-');
blocks.push(block('reportJSFunction'));
if (Process.prototype.enableCompiling) {
- blocks.push(block('reportCompiled'));
+ blocks.push(block('reportCompiled'));
}
}
-
- // for debugging: ///////////////
-
+ // for debugging: ///////////////
if (this.world().isDevMode) {
blocks.push('-');
- txt = new TextMorph(localize(
- 'development mode \ndebugging primitives:'
- ));
- txt.fontSize = 9;
- txt.setColor(this.paletteTextColor);
- blocks.push(txt);
+ blocks.push(this.devModeText());
blocks.push('-');
blocks.push(block('reportTypeOf'));
blocks.push(block('reportTextFunction'));
}
- /////////////////////////////////
-
- blocks.push('=');
- blocks.push(this.makeBlockButton(cat));
-
- } else if (cat === 'variables') {
-
- button = new PushButtonMorph(
- null,
- function () {
- new VariableDialogMorph(
- null,
- addVar,
- myself
- ).prompt(
- 'Variable name',
- null,
- myself.world()
- );
- },
- 'Make a variable'
- );
- button.userMenu = helpMenu;
- button.selector = 'addVariable';
- button.showHelp = BlockMorph.prototype.showHelp;
- blocks.push(button);
+ } else if (category === 'variables') {
+ blocks.push(this.makeVariableButton());
if (this.deletableVariableNames().length > 0) {
- button = new PushButtonMorph(
- null,
- function () {
- var menu = new MenuMorph(
- myself.deleteVariable,
- null,
- myself
- );
- myself.deletableVariableNames().forEach(name =>
- menu.addItem(
- name,
- name,
- null,
- null,
- null,
- null,
- null,
- null,
- true // verbatim - don't translate
- )
- );
- menu.popUpAtHand(myself.world());
- },
- 'Delete a variable'
- );
- button.userMenu = helpMenu;
- button.selector = 'deleteVariable';
- button.showHelp = BlockMorph.prototype.showHelp;
- blocks.push(button);
+ blocks.push(this.deleteVariableButton());
}
-
blocks.push('-');
varNames = this.reachableGlobalVariableNames(true);
@@ -2791,17 +2675,14 @@ SpriteMorph.prototype.blockTemplates = function (category) {
blocks.push(block('doHideVar'));
blocks.push(block('doDeclareVariables'));
- // inheritance:
+ // inheritance:
if (StageMorph.prototype.enableInheritance) {
blocks.push('-');
blocks.push(block('doDeleteAttr'));
}
- ///////////////////////////////
-
blocks.push('=');
-
blocks.push(block('reportNewList'));
blocks.push(block('reportNumbers'));
blocks.push('-');
@@ -2829,16 +2710,10 @@ SpriteMorph.prototype.blockTemplates = function (category) {
blocks.push(block('doInsertInList'));
blocks.push(block('doReplaceInList'));
- // for debugging: ///////////////
-
+ // for debugging: ///////////////
if (this.world().isDevMode) {
blocks.push('-');
- txt = new TextMorph(localize(
- 'development mode \ndebugging primitives:'
- ));
- txt.fontSize = 9;
- txt.setColor(this.paletteTextColor);
- blocks.push(txt);
+ blocks.push(this.devModeText());
blocks.push('-');
blocks.push(block('doShowTable'));
blocks.push('-');
@@ -2846,38 +2721,156 @@ SpriteMorph.prototype.blockTemplates = function (category) {
blocks.push(block('reportApplyExtension'));
}
- /////////////////////////////////
-
- blocks.push('=');
-
if (StageMorph.prototype.enableCodeMapping) {
+ blocks.push('=');
blocks.push(block('doMapCodeOrHeader'));
blocks.push(block('doMapValueCode'));
blocks.push(block('doMapListCode'));
blocks.push('-');
blocks.push(block('reportMappedCode'));
+ }
+ }
+
+ return blocks;
+};
+
+// Utitlies displayed in the palette
+SpriteMorph.prototype.makeVariableButton = function () {
+ var button, myself = this;
+
+ function addVar(pair) {
+ var ide;
+ if (pair) {
+ if (myself.isVariableNameInUse(pair[0], pair[1])) {
+ myself.inform('that name is already in use');
+ } else {
+ ide = myself.parentThatIsA(IDE_Morph);
+ myself.addVariable(pair[0], pair[1]);
+ myself.toggleVariableWatcher(pair[0], pair[1]);
+ ide.flushBlocksCache('variables'); // b/c of inheritance
+ ide.refreshPalette();
+ ide.recordUnsavedChanges();
+ }
+ }
+ }
+
+ button = new PushButtonMorph(
+ null,
+ function () {
+ new VariableDialogMorph(
+ null,
+ addVar,
+ myself
+ ).prompt(
+ 'Variable name',
+ null,
+ myself.world()
+ );
+ },
+ 'Make a variable'
+ );
+ button.userMenu = this.helpMenu;
+ button.selector = 'addVariable';
+ button.showHelp = BlockMorph.prototype.showHelp;
+ return button;
+};
+
+SpriteMorph.prototype.deleteVariableButton = function () {
+ var button, myself = this;
+ button = new PushButtonMorph(
+ null,
+ function () {
+ var menu = new MenuMorph(
+ myself.deleteVariable,
+ null,
+ myself
+ );
+ myself.deletableVariableNames().forEach(name =>
+ menu.addItem(
+ name,
+ name,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ true // verbatim - don't translate
+ )
+ );
+ menu.popUpAtHand(myself.world());
+ },
+ 'Delete a variable'
+ );
+ button.userMenu = this.helpMenu;
+ button.selector = 'deleteVariable';
+ button.showHelp = BlockMorph.prototype.showHelp;
+ return button;
+};
+
+SpriteMorph.prototype.devModeText = function () {
+ var txt = new TextMorph(
+ localize('development mode \ndebugging primitives:')
+ );
+ txt.fontSize = 9;
+ txt.setColor(this.paletteTextColor);
+ return txt;
+};
+
+SpriteMorph.prototype.helpMenu = function () {
+ // return a 1 item context menu for anything that implements
+ // a 'showHelp' method.
+ var menu = new MenuMorph(this);
+ menu.addItem('help...', 'showHelp');
+ return menu;
+};
+
+// returns an array alock templates for a selected category.
+SpriteMorph.prototype.customBlockTemplatesForCategory = function (category) {
+ var ide = this.parentThatIsA(IDE_Morph), blocks = [],
+ isInherited = false, block, inheritedBlocks;
+
+ function addCustomBlock(definition) {
+ if (definition.category === category ||
+ (Array.isArray(category) && category.includes(definition.category))
+ ) {
+ block = definition.templateInstance();
+ if (isInherited) {block.ghost(); }
+ blocks.push(block);
+ }
+ }
+
+ // global custom blocks:
+ if (ide && ide.stage) {
+ ide.stage.globalBlocks.forEach(addCustomBlock);
+ if (this.customBlocks.length) {blocks.push('='); }
+ }
+
+ // local custom blocks:
+ this.customBlocks.forEach(addCustomBlock);
+
+ // inherited custom blocks:
+ if (this.exemplar) {
+ inheritedBlocks = this.inheritedBlocks(true);
+ if (this.customBlocks.length && inheritedBlocks.length) {
blocks.push('=');
}
+ isInherited = true;
+ inheritedBlocks.forEach(addCustomBlock);
+ }
- blocks.push(this.makeBlockButton());
- }
return blocks;
};
SpriteMorph.prototype.makeBlockButton = function (category) {
- // answer a button that prompts the user to make a new block
+ // answer a button that prompts the user to make a new block
var button = new PushButtonMorph(
this,
- 'makeBlock',
+ 'makeBlock',
'Make a block'
);
- button.userMenu = function () {
- var menu = new MenuMorph(this);
- menu.addItem('help...', 'showHelp');
- return menu;
- };
-
+ button.userMenu = this.helpMenu;
button.selector = 'addCustomBlock';
button.showHelp = BlockMorph.prototype.showHelp;
return button;
@@ -2907,7 +2900,7 @@ SpriteMorph.prototype.makeBlock = function () {
},
this
);
- if (category !== 'variables') {
+ if (category !== 'variables' || category !== 'unified') {
dlg.category = category;
dlg.categories.children.forEach(each => each.refresh());
dlg.types.children.forEach(each => {
@@ -2922,6 +2915,17 @@ SpriteMorph.prototype.makeBlock = function () {
);
};
+SpriteMorph.prototype.getPrimitiveTemplates = function (category) {
+ var blocks = this.primitivesCache[category];
+ if (!blocks) {
+ blocks = this.blockTemplates(category);
+ if (this.isCachingPrimitives) {
+ this.primitivesCache[category] = blocks;
+ }
+ }
+ return blocks;
+};
+
SpriteMorph.prototype.palette = function (category) {
if (!this.paletteCache[category]) {
this.paletteCache[category] = this.freshPalette(category);
@@ -2937,7 +2941,6 @@ SpriteMorph.prototype.freshPalette = function (category) {
ry = 0,
blocks,
hideNextSpace = false,
- stage = this.parentThatIsA(StageMorph),
shade = new Color(140, 140, 140),
searchButton,
makeButton;
@@ -2948,7 +2951,7 @@ SpriteMorph.prototype.freshPalette = function (category) {
palette.growth = new Point(0, MorphicPreferences.scrollBarSize);
// toolbar:
-
+
palette.toolBar = new AlignmentMorph('column');
searchButton = new PushButtonMorph(
@@ -2963,7 +2966,7 @@ SpriteMorph.prototype.freshPalette = function (category) {
searchButton.edge = 0;
searchButton.padding = 3;
searchButton.fixLayout();
- palette.toolBar.add(searchButton);
+ palette.toolBar.add(searchButton);
makeButton = new PushButtonMorph(
this,
@@ -3087,14 +3090,31 @@ SpriteMorph.prototype.freshPalette = function (category) {
return menu;
};
- // primitives:
+ if (category === 'unified') {
+ // In a Unified Palette custom blocks appear following each category,
+ // but there is only 1 make a block button (at the end).
+ blocks = this.categories.reduce((blocks, category) =>
+ blocks.concat(
+ this.getPrimitiveTemplates(category),
+ '=',
+ this.customBlockTemplatesForCategory(category),
+ '='
+ ),
+ []);
+ } else {
+ // ensure we do not modify the cached array
+ blocks = this.getPrimitiveTemplates(category).slice();
+ }
+ blocks.push('=');
+ blocks.push(this.makeBlockButton(category));
- blocks = this.blocksCache[category];
- if (!blocks) {
- blocks = this.blockTemplates(category);
- if (this.isCachingPrimitives) {
- this.blocksCache[category] = blocks;
- }
+ if (category === 'variables') {
+ category = ['variables', 'lists', 'other'];
+ }
+
+ if (category !== 'unified') {
+ blocks.push('=');
+ blocks.push(...this.customBlockTemplatesForCategory(category));
}
blocks.forEach(block => {
@@ -3130,74 +3150,6 @@ SpriteMorph.prototype.freshPalette = function (category) {
}
});
- // global custom blocks:
-
- if (stage) {
- y += unit * 1.6;
-
- stage.globalBlocks.forEach(definition => {
- var block;
- if (definition.category === category ||
- (category === 'variables'
- && contains(
- ['lists', 'other'],
- definition.category
- ))) {
- block = definition.templateInstance();
- y += unit * 0.3;
- block.setPosition(new Point(x, y));
- palette.addContents(block);
- x = 0;
- y += block.height();
- }
- });
- }
-
- // local custom blocks:
-
- y += unit * 1.6;
- this.customBlocks.forEach(definition => {
- var block;
- if (definition.category === category ||
- (category === 'variables'
- && contains(
- ['lists', 'other'],
- definition.category
- ))) {
- block = definition.templateInstance();
- y += unit * 0.3;
- block.setPosition(new Point(x, y));
- palette.addContents(block);
- x = 0;
- y += block.height();
- }
- });
-
- // inherited custom blocks:
-
- // y += unit * 1.6;
- if (this.exemplar) {
- this.inheritedBlocks(true).forEach(definition => {
- var block;
- if (definition.category === category ||
- (category === 'variables'
- && contains(
- ['lists', 'other'],
- definition.category
- ))) {
- block = definition.templateInstance();
- y += unit * 0.3;
- block.setPosition(new Point(x, y));
- palette.addContents(block);
- block.ghost();
- x = 0;
- y += block.height();
- }
- });
- }
-
- //layout
-
palette.scrollX(palette.padding);
palette.scrollY(palette.padding);
return palette;
@@ -3339,6 +3291,7 @@ SpriteMorph.prototype.searchBlocks = function (
var myself = this,
unit = SyntaxElementMorph.prototype.fontSize,
ide = this.parentThatIsA(IDE_Morph),
+ oldTop = ide.palette.contents.top(),
oldSearch = '',
searchBar = new InputFieldMorph(searchString || ''),
searchPane = ide.createPalette('forSearch'),
@@ -3446,6 +3399,7 @@ SpriteMorph.prototype.searchBlocks = function (
searchBar.cancel = function () {
ide.refreshPalette();
+ ide.palette.contents.setTop(oldTop);
ide.palette.adjustScrollBars();
};
@@ -3644,7 +3598,7 @@ SpriteMorph.prototype.addVariable = function (name, isGlobal) {
}
} else {
this.variables.addVar(name);
- this.blocksCache.variables = null;
+ this.primitivesCache.variables = null;
}
};
@@ -3746,7 +3700,9 @@ SpriteMorph.prototype.doWearPreviousCostume = function () {
};
SpriteMorph.prototype.doSwitchToCostume = function (id, noShadow) {
- var w = 0, h = 0;
+ var w = 0,
+ h = 0,
+ stage;
if (id instanceof List) { // try to turn a list of pixels into a costume
if (this.costume) {
// recycle dimensions of current costume
@@ -3755,8 +3711,9 @@ SpriteMorph.prototype.doSwitchToCostume = function (id, noShadow) {
}
if (w * h !== id.length()) {
// assume stage's dimensions
- w = StageMorph.prototype.dimensions.x;
- h = StageMorph.prototype.dimensions.y;
+ stage = this.parentThatIsA(StageMorph);
+ w = stage.dimensions.x;
+ h = stage.dimensions.y;
}
id = Process.prototype.reportNewCostume(
id,
@@ -4373,7 +4330,7 @@ SpriteMorph.prototype.setColorComponentHSVA = function (idx, num) {
idx = +idx;
if (idx < 0 || idx > 3) {return; }
- if (idx == 0) {
+ if (idx === 0) {
if (n < 0 || n > 100) { // wrap the hue
n = (n < 0 ? 100 : 0) + n % 100;
}
@@ -6006,7 +5963,7 @@ SpriteMorph.prototype.xRight = function () {
}
return this.right();
};
-
+
SpriteMorph.prototype.yTop = function () {
var stage = this.parentThatIsA(StageMorph);
@@ -6293,7 +6250,7 @@ SpriteMorph.prototype.toggleVariableWatcher = function (varName, isGlobal) {
globals = this.globalVariables(),
watcher,
others;
-
+
if (stage === null) {
return null;
}
@@ -7679,7 +7636,7 @@ StageMorph.uber = FrameMorph.prototype;
// StageMorph preferences settings
-StageMorph.prototype.dimensions = new Point(480, 360); // unscaled extent
+StageMorph.prototype.dimensions = new Point(480, 360); // fallback unscaled ext
StageMorph.prototype.frameRate = 0; // unscheduled per default
StageMorph.prototype.isCachingPrimitives
@@ -7707,6 +7664,7 @@ function StageMorph(globals) {
StageMorph.prototype.init = function (globals) {
this.name = localize('Stage');
+ this.dimensions = new Point(480, 360); // unscaled extent
this.instrument = null;
this.threads = new ThreadManager();
this.variables = new VariableFrame(globals || null, this);
@@ -7742,7 +7700,7 @@ StageMorph.prototype.init = function (globals) {
this.cachedHSV = [0, 0, 0]; // for background hsv support, not serialized
this.keysPressed = {}; // for handling keyboard events, do not persist
- this.blocksCache = {}; // not to be serialized (!)
+ this.primitivesCache = {}; // not to be serialized (!)
this.paletteCache = {}; // not to be serialized (!)
this.lastAnswer = ''; // last user input, do not persist
this.activeSounds = []; // do not persist
@@ -7791,6 +7749,7 @@ StageMorph.prototype.init = function (globals) {
StageMorph.uber.init.call(this);
+ this.setExtent(this.dimensions);
this.isCachingImage = true;
this.cachedHSV = this.color.hsv();
this.acceptsDrops = false;
@@ -8525,9 +8484,8 @@ StageMorph.prototype.pauseGenericHatBlocks = function () {
// StageMorph block templates
-StageMorph.prototype.blockTemplates = function (category) {
- var blocks = [], myself = this, varNames, button,
- cat = category || 'motion', txt;
+StageMorph.prototype.blockTemplates = function (category = 'motion') {
+ var blocks = [], myself = this, varNames, txt;
function block(selector) {
if (myself.hiddenPrimitives[selector]) {
@@ -8584,35 +8542,14 @@ StageMorph.prototype.blockTemplates = function (category) {
);
}
- function addVar(pair) {
- if (pair) {
- var ide;
- if (myself.isVariableNameInUse(pair[0])) {
- myself.inform('that name is already in use');
- } else {
- ide = myself.parentThatIsA(IDE_Morph);
- myself.addVariable(pair[0], pair[1]);
- myself.toggleVariableWatcher(pair[0], pair[1]);
- myself.blocksCache[cat] = null;
- myself.paletteCache[cat] = null;
- ide.refreshPalette();
- ide.recordUnsavedChanges();
- }
- }
- }
+ if (category === 'motion') {
- if (cat === 'motion') {
-
- txt = new TextMorph(localize(
- 'Stage selected:\nno motion primitives'
- ));
+ txt = new TextMorph(localize('Stage selected:\nno motion primitives'));
txt.fontSize = 9;
txt.setColor(this.paletteTextColor);
blocks.push(txt);
- blocks.push('=');
- blocks.push(this.makeBlockButton(cat));
- } else if (cat === 'looks') {
+ } else if (category === 'looks') {
blocks.push(block('doSwitchToCostume'));
blocks.push(block('doWearNextCostume'));
@@ -8632,17 +8569,13 @@ StageMorph.prototype.blockTemplates = function (category) {
blocks.push(block('hide'));
blocks.push(watcherToggle('reportShown'));
blocks.push(block('reportShown'));
+ blocks.push('-');
+ blocks.push(block('doSwitchToScene'));
- // for debugging: ///////////////
-
+ // for debugging: ///////////////
if (this.world().isDevMode) {
blocks.push('-');
- txt = new TextMorph(localize(
- 'development mode \ndebugging primitives:'
- ));
- txt.fontSize = 9;
- txt.setColor(this.paletteTextColor);
- blocks.push(txt);
+ blocks.push(this.devModeText());
blocks.push('-');
blocks.push(block('log'));
blocks.push(block('alert'));
@@ -8650,12 +8583,7 @@ StageMorph.prototype.blockTemplates = function (category) {
blocks.push(block('doScreenshot'));
}
- /////////////////////////////////
-
- blocks.push('=');
- blocks.push(this.makeBlockButton(cat));
-
- } else if (cat === 'sound') {
+ } else if (category === 'sound') {
blocks.push(block('playSound'));
blocks.push(block('doPlaySoundUntilDone'));
@@ -8687,26 +8615,15 @@ StageMorph.prototype.blockTemplates = function (category) {
blocks.push(block('playFreq'));
blocks.push(block('stopFreq'));
- // for debugging: ///////////////
-
+ // for debugging: ///////////////
if (this.world().isDevMode) {
blocks.push('-');
- txt = new TextMorph(localize(
- 'development mode \ndebugging primitives:'
- ));
- txt.fontSize = 9;
- txt.setColor(this.paletteTextColor);
- blocks.push(txt);
+ blocks.push(this.devModeText());
blocks.push('-');
blocks.push(block('doPlayFrequency'));
}
- /////////////////////////////////
-
- blocks.push('=');
- blocks.push(this.makeBlockButton(cat));
-
- } else if (cat === 'pen') {
+ } else if (category === 'pen') {
blocks.push(block('clear'));
blocks.push('-');
@@ -8718,10 +8635,8 @@ StageMorph.prototype.blockTemplates = function (category) {
blocks.push('-');
blocks.push(block('doPasteOn'));
blocks.push(block('doCutFrom'));
- blocks.push('=');
- blocks.push(this.makeBlockButton(cat));
- } else if (cat === 'control') {
+ } else if (category === 'control') {
blocks.push(block('receiveGo'));
blocks.push(block('receiveKey'));
@@ -8766,10 +8681,8 @@ StageMorph.prototype.blockTemplates = function (category) {
blocks.push(block('newClone'));
blocks.push('-');
blocks.push(block('doPauseAll'));
- blocks.push('=');
- blocks.push(this.makeBlockButton(cat));
- } else if (cat === 'sensing') {
+ } else if (category === 'sensing') {
blocks.push(block('doAsk'));
blocks.push(watcherToggle('getLastAnswer'));
@@ -8807,17 +8720,10 @@ StageMorph.prototype.blockTemplates = function (category) {
blocks.push('-');
blocks.push(block('reportDate'));
- // for debugging: ///////////////
-
+ // for debugging: ///////////////
if (this.world().isDevMode) {
-
blocks.push('-');
- txt = new TextMorph(localize(
- 'development mode \ndebugging primitives:'
- ));
- txt.fontSize = 9;
- txt.setColor(this.paletteTextColor);
- blocks.push(txt);
+ blocks.push(this.devModeText());
blocks.push('-');
blocks.push(watcherToggle('reportThreadCount'));
blocks.push(block('reportThreadCount'));
@@ -8825,13 +8731,8 @@ StageMorph.prototype.blockTemplates = function (category) {
blocks.push(block('reportFrameCount'));
blocks.push(block('reportYieldCount'));
}
-
- /////////////////////////////////
-
- blocks.push('=');
- blocks.push(this.makeBlockButton(cat));
-
- } else if (cat === 'operators') {
+ }
+ if (category === 'operators') {
blocks.push(block('reifyScript'));
blocks.push(block('reifyReporter'));
@@ -8869,7 +8770,7 @@ StageMorph.prototype.blockTemplates = function (category) {
blocks.push(block('reportIsA'));
blocks.push(block('reportIsIdentical'));
- if (Process.prototype.enableJS) {
+ if (Process.prototype.enableJS) { // (Process.prototype.enableJS) {
blocks.push('-');
blocks.push(block('reportJSFunction'));
if (Process.prototype.enableCompiling) {
@@ -8877,74 +8778,22 @@ StageMorph.prototype.blockTemplates = function (category) {
}
}
- // for debugging: ///////////////
-
+ // for debugging: ///////////////
if (this.world().isDevMode) {
blocks.push('-');
- txt = new TextMorph(
- 'development mode \ndebugging primitives:'
- );
- txt.fontSize = 9;
- txt.setColor(this.paletteTextColor);
- blocks.push(txt);
+ blocks.push(this.devModeText());
blocks.push('-');
blocks.push(block('reportTypeOf'));
blocks.push(block('reportTextFunction'));
}
- //////////////////////////////////
-
- blocks.push('=');
- blocks.push(this.makeBlockButton(cat));
-
- } else if (cat === 'variables') {
-
- button = new PushButtonMorph(
- null,
- function () {
- new VariableDialogMorph(
- null,
- addVar,
- myself
- ).prompt(
- 'Variable name',
- null,
- myself.world()
- );
- },
- 'Make a variable'
- );
- blocks.push(button);
+ }
+ if (category === 'variables') {
+ blocks.push(this.makeVariableButton());
if (this.variables.allNames().length > 0) {
- button = new PushButtonMorph(
- null,
- function () {
- var menu = new MenuMorph(
- myself.deleteVariable,
- null,
- myself
- );
- myself.variables.allNames().forEach(name =>
- menu.addItem(
- name,
- name,
- null,
- null,
- null,
- null,
- null,
- null,
- true // verbatim - don't translate
- )
- );
- menu.popUpAtHand(myself.world());
- },
- 'Delete a variable'
- );
- blocks.push(button);
+ blocks.push(this.deleteVariableButton());
}
-
blocks.push('-');
varNames = this.reachableGlobalVariableNames(true);
@@ -8998,16 +8847,10 @@ StageMorph.prototype.blockTemplates = function (category) {
blocks.push(block('doInsertInList'));
blocks.push(block('doReplaceInList'));
- // for debugging: ///////////////
-
+ // for debugging: ///////////////
if (this.world().isDevMode) {
blocks.push('-');
- txt = new TextMorph(localize(
- 'development mode \ndebugging primitives:'
- ));
- txt.fontSize = 9;
- txt.setColor(this.paletteTextColor);
- blocks.push(txt);
+ blocks.push(this.devModeText());
blocks.push('-');
blocks.push(block('doShowTable'));
blocks.push('-');
@@ -9015,8 +8858,6 @@ StageMorph.prototype.blockTemplates = function (category) {
blocks.push(block('reportApplyExtension'));
}
- /////////////////////////////////
-
blocks.push('=');
if (StageMorph.prototype.enableCodeMapping) {
@@ -9025,11 +8866,9 @@ StageMorph.prototype.blockTemplates = function (category) {
blocks.push(block('doMapListCode'));
blocks.push('-');
blocks.push(block('reportMappedCode'));
- blocks.push('=');
}
-
- blocks.push(this.makeBlockButton());
}
+
return blocks;
};
@@ -9154,7 +8993,9 @@ StageMorph.prototype.fancyThumbnail = function (
ctx.restore();
}
this.children.forEach(morph => {
- if ((isSnapObject(morph) || !noWatchers) && morph.isVisible && (morph !== excludedSprite)) {
+ if ((isSnapObject(morph) || !noWatchers) &&
+ morph.isVisible && (morph !== excludedSprite)
+ ) {
fb = morph.fullBounds();
fimg = morph.fullImage();
if (fimg.width && fimg.height) {
@@ -9265,7 +9106,7 @@ StageMorph.prototype.setColorComponentHSVA = function (idx, num) {
idx = +idx;
if (idx < 0 || idx > 3) {return; }
- if (idx == 0) {
+ if (idx === 0) {
if (n < 0 || n > 100) { // wrap the hue
n = (n < 0 ? 100 : 0) + n % 100;
}
@@ -9310,18 +9151,37 @@ StageMorph.prototype.categories = SpriteMorph.prototype.categories;
StageMorph.prototype.blockColor = SpriteMorph.prototype.blockColor;
StageMorph.prototype.paletteColor = SpriteMorph.prototype.paletteColor;
StageMorph.prototype.setName = SpriteMorph.prototype.setName;
-StageMorph.prototype.makeBlockButton = SpriteMorph.prototype.makeBlockButton;
-StageMorph.prototype.makeBlock = SpriteMorph.prototype.makeBlock;
-StageMorph.prototype.palette = SpriteMorph.prototype.palette;
-StageMorph.prototype.freshPalette = SpriteMorph.prototype.freshPalette;
-StageMorph.prototype.blocksMatching = SpriteMorph.prototype.blocksMatching;
-StageMorph.prototype.searchBlocks = SpriteMorph.prototype.searchBlocks;
StageMorph.prototype.reporterize = SpriteMorph.prototype.reporterize;
StageMorph.prototype.variableBlock = SpriteMorph.prototype.variableBlock;
StageMorph.prototype.showingWatcher = SpriteMorph.prototype.showingWatcher;
StageMorph.prototype.addVariable = SpriteMorph.prototype.addVariable;
StageMorph.prototype.deleteVariable = SpriteMorph.prototype.deleteVariable;
+// StageMorph Palette Utilities
+
+StageMorph.prototype.makeBlock = SpriteMorph.prototype.makeBlock;
+StageMorph.prototype.helpMenu = SpriteMorph.prototype.helpMenu;
+StageMorph.prototype.makeBlockButton = SpriteMorph.prototype.makeBlockButton;
+
+StageMorph.prototype.makeVariableButton
+ = SpriteMorph.prototype.makeVariableButton;
+
+StageMorph.prototype.devModeText = SpriteMorph.prototype.devModeText;
+
+StageMorph.prototype.deleteVariableButton
+ = SpriteMorph.prototype.deleteVariableButton;
+
+StageMorph.prototype.customBlockTemplatesForCategory
+ = SpriteMorph.prototype.customBlockTemplatesForCategory;
+
+StageMorph.prototype.getPrimitiveTemplates
+ = SpriteMorph.prototype.getPrimitiveTemplates;
+
+StageMorph.prototype.palette = SpriteMorph.prototype.palette;
+StageMorph.prototype.freshPalette = SpriteMorph.prototype.freshPalette;
+StageMorph.prototype.blocksMatching = SpriteMorph.prototype.blocksMatching;
+StageMorph.prototype.searchBlocks = SpriteMorph.prototype.searchBlocks;
+
// StageMorph neighbor detection
StageMorph.prototype.neighbors = SpriteMorph.prototype.neighbors;
@@ -9908,10 +9768,10 @@ SpriteBubbleMorph.prototype.fixLayout = function () {
// Costume instance creation
-function Costume(canvas, name, rotationCenter, noFit) {
+function Costume(canvas, name, rotationCenter, noFit, maxExtent) {
this.contents = canvas ? normalizeCanvas(canvas, true)
: newCanvas(null, true);
- if (!noFit) {this.shrinkToFit(this.maxExtent()); }
+ if (!noFit) {this.shrinkToFit(maxExtent || this.maxExtent()); }
this.name = name || null;
this.rotationCenter = rotationCenter || this.center();
this.version = Date.now(); // for observer optimization
@@ -10071,7 +9931,8 @@ Costume.prototype.flipped = function () {
new Point(
this.width() - this.rotationCenter.x,
this.rotationCenter.y
- )
+ ),
+ true // no shrink-wrap
);
return flipped;
};
@@ -10116,7 +9977,7 @@ Costume.prototype.edit = function (aWorld, anIDE, isnew, oncancel, onsubmit) {
editor.openIn(
aWorld,
isnew ?
- newCanvas(StageMorph.prototype.dimensions, true) :
+ newCanvas(anIDE.stage.dimensions, true) :
this.contents,
isnew ?
null :
@@ -10363,7 +10224,7 @@ SVG_Costume.prototype.edit = function (
editor.oncancel = oncancel || nop;
editor.openIn(
aWorld,
- isnew ? newCanvas(StageMorph.prototype.dimensions) : this.contents,
+ isnew ? newCanvas(anIDE.stage.dimensions) : this.contents,
isnew ? new Point(240, 180) : this.rotationCenter,
(img, rc, shapes) => {
myself.contents = img;
@@ -12184,7 +12045,6 @@ StagePrompterMorph.prototype.init = function (question) {
if (this.label) {this.add(this.label); }
this.add(this.inputField);
this.add(this.button);
- this.setWidth(StageMorph.prototype.dimensions.x - 20);
this.fixLayout();
};
diff --git a/src/paint.js b/src/paint.js
index 79b1d146..6645d2d3 100644
--- a/src/paint.js
+++ b/src/paint.js
@@ -71,18 +71,19 @@
2020 Apr 14 - Morphic2 migration (Jens)
2020 May 17 - Pipette alpha fix (Joan)
- 2020 July 13 - modified scale buttons (Jadga)
+ 2020 Jul 13 - modified scale buttons (Jadga)
+
+ 2021 Mar 17 - moved stage dimension handling to scenes (Jens)
*/
-/*global Point, Rectangle, DialogBoxMorph, AlignmentMorph, PushButtonMorph,
-Color, SymbolMorph, newCanvas, Morph, StringMorph, Costume, SpriteMorph, nop,
-localize, InputFieldMorph, SliderMorph, ToggleMorph, ToggleButtonMorph,
-BoxMorph, modules, radians, MorphicPreferences, getDocumentPositionOf,
-StageMorph, isNil, SVG_Costume*/
+/*global Point, Rectangle, DialogBoxMorph, AlignmentMorph, PushButtonMorph, nop,
+Color, SymbolMorph, newCanvas, Morph, StringMorph, Costume, SpriteMorph, isNil,
+localize, InputFieldMorph, SliderMorph, ToggleMorph, ToggleButtonMorph, modules,
+BoxMorph, radians, MorphicPreferences, getDocumentPositionOf, SVG_Costume*/
// Global stuff ////////////////////////////////////////////////////////
-modules.paint = '2020-July-13';
+modules.paint = '2021-March-17';
// Declarations
@@ -106,6 +107,7 @@ function PaintEditorMorph() {
PaintEditorMorph.prototype.init = function () {
// additional properties:
+ this.ide = null;
this.paper = null; // paint canvas
this.oncancel = null;
@@ -116,15 +118,16 @@ PaintEditorMorph.prototype.init = function () {
this.labelString = "Paint Editor";
this.createLabel();
- // build contents:
- this.buildContents();
+ // building the contents happens when I am opened with an IDE
+ // so my extent can be adjusted accordingly (jens)
+ // this.buildContents();
};
PaintEditorMorph.prototype.buildContents = function () {
var myself = this;
this.paper = new PaintCanvasMorph(function () {return myself.shift; });
- this.paper.setExtent(StageMorph.prototype.dimensions);
+ this.paper.setExtent(this.ide.stage.dimensions);
this.addBody(new AlignmentMorph('row', this.padding));
this.controls = new AlignmentMorph('column', this.padding / 2);
@@ -293,6 +296,8 @@ PaintEditorMorph.prototype.openIn = function (
this.callback = callback || nop;
this.ide = anIDE;
+ this.buildContents();
+
this.processKeyUp = function () {
myself.shift = false;
myself.propertiesControls.constrain.refresh();
diff --git a/src/scenes.js b/src/scenes.js
new file mode 100644
index 00000000..5cced175
--- /dev/null
+++ b/src/scenes.js
@@ -0,0 +1,211 @@
+/*
+
+ scenes.js
+
+ multi-scene support for Snap!
+
+ written by Jens Mönig
+ jens@moenig.org
+
+ Copyright (C) 2021 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 morphic.js and objects.js
+
+ toc
+ ---
+ the following list shows the order in which all constructors are
+ defined. Use this list to locate code in this document:
+
+ Project
+ Scene
+
+ credits
+ -------
+ scenes have been inspired by Ted Kaehlers's personal demos of HyperCard
+ and many discussions with Ted about the design and practice of HyperCard,
+ and by personal discussions with Wolfgang Slany about his design of
+ scenes in Catrobat/PocketCode, which I love and admire.
+
+*/
+
+// Global stuff ////////////////////////////////////////////////////////
+
+/*global modules, VariableFrame, StageMorph, SpriteMorph, Process, List,
+normalizeCanvas, SnapSerializer*/
+
+modules.scenes = '2021-July-02';
+
+
+// Projecct /////////////////////////////////////////////////////////
+
+// I am a container for a set of one or more Snap! scenes,
+// the IDE operates on an instance of me
+
+// Project instance creation:
+
+function Project(scenes, current) {
+ var projectScene;
+
+ this.scenes = scenes || new List();
+ this.currentScene = current;
+
+ // proxied for display
+ this.name = null;
+ this.notes = null;
+ this.thumbnail = null;
+
+ projectScene = this.scenes.at(1);
+ if (projectScene) {
+ this.name = projectScene.name;
+ this.notes = projectScene.notes;
+ this.thumbnail = normalizeCanvas(
+ projectScene.stage.thumbnail(SnapSerializer.prototype.thumbnailSize)
+ );
+ }
+
+ // for deserializing - do not persist
+ this.sceneIdx = null;
+
+ // for undeleting scenes - do not persist
+ this.trash = [];
+}
+
+Project.prototype.initialize = function () {
+ // initialize after deserializing
+ // only to be called by store
+ this.currentScene = this.scenes.at(+this.sceneIdx || 1);
+ return this;
+};
+
+Project.prototype.addDefaultScene = function () {
+ var scene = new Scene();
+ scene.addDefaultSprite();
+ this.scenes.add(scene);
+};
+
+// Scene /////////////////////////////////////////////////////////
+
+// I am a container for a Snap! stage, scene-global variables
+// and its associated settings.
+// I can be used as a slide in a presentation, a chapter in a narrative,
+// a level in a game, etc.
+
+// Scene instance creation:
+
+function Scene(aStageMorph) {
+ this.name = '';
+ this.notes = '';
+ this.globalVariables = aStageMorph ?
+ aStageMorph.globalVariables() : new VariableFrame();
+ this.stage = aStageMorph || new StageMorph(this.globalVariables);
+ this.hasUnsavedEdits = false;
+ this.unifiedPalette = false;
+
+ // cached IDE state
+ this.sprites = new List();
+ this.currentSprite = null;
+
+ // global settings (shared)
+ this.hiddenPrimitives = {};
+ this.codeMappings = {};
+ this.codeHeaders = {};
+
+ // global settings (copied)
+ this.enableCodeMapping = false;
+ this.enableInheritance = true;
+ this.enableSublistIDs = false;
+ this.enablePenLogging = false;
+ this.useFlatLineEnds = false;
+ this.enableLiveCoding = false;
+ this.enableHyperOps = true;
+
+ // for deserializing - do not persist
+ this.spritesDict = {};
+ this.targetStage = null;
+ this.spriteIdx = null;
+
+ // for undeleting sprites - do not persist
+ this.trash = [];
+}
+
+Scene.prototype.initialize = function () {
+ // initialize after deserializing
+ // only to be called by store
+ var objs = this.stage.children.filter(
+ child => child instanceof SpriteMorph
+ );
+ objs.sort((x, y) => x.idx - y.idx);
+ this.sprites = new List(objs);
+ if (this.spriteIdx === null && this.sprites.length() > 0) {
+ this.currentSprite = this.sprites.at(1);
+ } else if (this.spriteIdx === 0) {
+ this.currentSprite = this.stage;
+ } else {
+ this.currentSprite = this.sprites.at(this.spriteIdx) ||
+ this.stage;
+ }
+ return this;
+};
+
+Scene.prototype.addDefaultSprite = function () {
+ var sprite = new SpriteMorph(this.globalVariables);
+ sprite.setPosition(
+ this.stage.center().subtract(
+ sprite.extent().divideBy(2)
+ )
+ );
+ this.stage.add(sprite);
+ this.sprites.add(sprite);
+ this.currentSprite = sprite;
+ return sprite;
+};
+
+Scene.prototype.captureGlobalSettings = function () {
+ this.hiddenPrimitives = StageMorph.prototype.hiddenPrimitives;
+ this.unifiedPalette = StageMorph.prototype.unifiedPalette;
+ this.codeMappings = StageMorph.prototype.codeMappings;
+ this.codeHeaders = StageMorph.prototype.codeHeaders;
+ this.enableCodeMapping = StageMorph.prototype.enableCodeMapping;
+ this.enableInheritance = StageMorph.prototype.enableInheritance;
+ this.enableSublistIDs = StageMorph.prototype.enableSublistIDs;
+ this.enablePenLogging = StageMorph.prototype.enablePenLogging;
+ this.useFlatLineEnds = SpriteMorph.prototype.useFlatLineEnds;
+ this.enableLiveCoding = Process.prototype.enableLiveCoding;
+ this.enableHyperOps = Process.prototype.enableHyperOps;
+};
+
+Scene.prototype.applyGlobalSettings = function () {
+ StageMorph.prototype.hiddenPrimitives = this.hiddenPrimitives;
+ StageMorph.prototype.unifiedPalette = this.unifiedPalette;
+ StageMorph.prototype.codeMappings = this.codeMappings;
+ StageMorph.prototype.codeHeaders = this.codeHeaders;
+ StageMorph.prototype.enableCodeMapping = this.enableCodeMapping;
+ StageMorph.prototype.enableInheritance = this.enableInheritance;
+ StageMorph.prototype.enableSublistIDs = this.enableSublistIDs;
+ StageMorph.prototype.enablePenLogging = this.enablePenLogging;
+ SpriteMorph.prototype.useFlatLineEnds = this.useFlatLineEnds;
+ Process.prototype.enableLiveCoding = this.enableLiveCoding;
+ Process.prototype.enableHyperOps = this.enableHyperOps;
+};
+
+Scene.prototype.updateTrash = function () {
+ this.trash = this.trash.filter(sprite => sprite.isCorpse);
+};
diff --git a/src/sketch.js b/src/sketch.js
index aa7eb760..6ffa04e4 100644
--- a/src/sketch.js
+++ b/src/sketch.js
@@ -54,14 +54,16 @@
- select primary color with right-click (in addition to shift-click)
2020, April 15 (Jens):
- migrated to new Morphic2 architecture
+ 2021, March 17 (Jens):
+ - moved stage dimension handling to scenes
*/
-/*global Point, Object, Rectangle, AlignmentMorph, Morph, XML_Element, nop,
-PaintColorPickerMorph, Color, SliderMorph, InputFieldMorph, ToggleMorph,
-TextMorph, Image, newCanvas, PaintEditorMorph, StageMorph, Costume, isNil,
-localize, PaintCanvasMorph, StringMorph, detect, modules*/
+/*global Point, Object, Rectangle, AlignmentMorph, Morph, XML_Element, localize,
+PaintColorPickerMorph, Color, SliderMorph, InputFieldMorph, ToggleMorph, isNil,
+TextMorph, Image, newCanvas, PaintEditorMorph, Costume, nop, PaintCanvasMorph,
+StringMorph, detect, modules*/
-modules.sketch = '2020-July-13';
+modules.sketch = '2021-March-17';
// Declarations
@@ -999,7 +1001,7 @@ VectorPaintEditorMorph.prototype.buildEdits = function () {
};
VectorPaintEditorMorph.prototype.convertToBitmap = function () {
- var canvas = newCanvas(StageMorph.prototype.dimensions),
+ var canvas = newCanvas(this.ide.stage.dimensions),
myself = this;
this.object = new Costume();
@@ -1053,7 +1055,14 @@ VectorPaintEditorMorph.prototype.openIn = function (
var myself = this,
isEmpty = isNil(shapes) || shapes.length === 0;
- VectorPaintEditorMorph.uber.openIn.call(this, world, null, oldrc, callback, anIDE);
+ VectorPaintEditorMorph.uber.openIn.call(
+ this,
+ world,
+ null,
+ oldrc,
+ callback,
+ anIDE
+ );
this.ide = anIDE;
this.paper.drawNew();
this.paper.changed();
@@ -1203,7 +1212,7 @@ VectorPaintEditorMorph.prototype.buildContents = function() {
this.paper.destroy();
this.paper = new VectorPaintCanvasMorph(myself.shift);
- this.paper.setExtent(StageMorph.prototype.dimensions);
+ this.paper.setExtent(this.ide.stage.dimensions);
this.body.add(this.paper);
this.refreshToolButtons();
diff --git a/src/store.js b/src/store.js
index fb7bc2f4..6002ec33 100644
--- a/src/store.js
+++ b/src/store.js
@@ -27,7 +27,7 @@
prerequisites:
--------------
- needs morphic.js, xml.js, and most of Snap!'s other modules
+ needs morphic.js, xml.js, scenes.js and most of Snap!'s other modules
hierarchy
@@ -49,20 +49,19 @@
*/
-/*global modules, XML_Element, VariableFrame, StageMorph, SpriteMorph,
-WatcherMorph, Point, CustomBlockDefinition, Context, ReporterBlockMorph,
+/*global modules, XML_Element, VariableFrame, StageMorph, SpriteMorph, console,
+WatcherMorph, Point, CustomBlockDefinition, Context, ReporterBlockMorph, Sound,
CommandBlockMorph, detect, CustomCommandBlockMorph, CustomReporterBlockMorph,
-Color, List, newCanvas, Costume, Sound, Audio, IDE_Morph, ScriptsMorph,
+Color, List, newCanvas, Costume, Audio, IDE_Morph, ScriptsMorph, ArgLabelMorph,
BlockMorph, ArgMorph, InputSlotMorph, TemplateSlotMorph, CommandSlotMorph,
FunctionSlotMorph, MultiArgMorph, ColorSlotMorph, nop, CommentMorph, isNil,
-localize, sizeOf, ArgLabelMorph, SVG_Costume, MorphicPreferences, Process,
-SyntaxElementMorph, Variable, isSnapObject, console, BooleanSlotMorph,
-normalizeCanvas, contains*/
+localize, SVG_Costume, MorphicPreferences, Process, isSnapObject, Variable,
+SyntaxElementMorph, BooleanSlotMorph, normalizeCanvas, contains, Scene,
+Project*/
// Global stuff ////////////////////////////////////////////////////////
-modules.store = '2021-June-24';
-
+modules.store = '2021-July-02';
// XML_Serializer ///////////////////////////////////////////////////////
/*
@@ -78,6 +77,7 @@ modules.store = '2021-June-24';
function XML_Serializer() {
this.contents = [];
this.media = [];
+ this.root = {};
this.isCollectingMedia = false;
this.isExportingBlocksLibrary = false;
}
@@ -87,7 +87,7 @@ function XML_Serializer() {
XML_Serializer.prototype.idProperty = 'serializationID';
XML_Serializer.prototype.mediaIdProperty = 'serializationMediaID';
XML_Serializer.prototype.mediaDetectionProperty = 'isMedia';
-XML_Serializer.prototype.version = 1; // increment on structural change
+XML_Serializer.prototype.version = 2; // increment on structural change
// XML_Serializer accessing:
@@ -109,6 +109,9 @@ XML_Serializer.prototype.store = function (object, mediaID) {
// when debugging, be sure to throw an error at this point
return '';
}
+ if (object instanceof Scene) {
+ this.root = object;
+ }
if (this.isCollectingMedia && object[this.mediaDetectionProperty]) {
this.addMedia(object, mediaID);
return this.format(
@@ -174,6 +177,7 @@ XML_Serializer.prototype.flush = function () {
// private - free all objects and empty my contents
this.contents.forEach(obj => delete obj[this.idProperty]);
this.contents = [];
+ this.root = {};
};
XML_Serializer.prototype.flushMedia = function () {
@@ -247,7 +251,7 @@ SnapSerializer.uber = XML_Serializer.prototype;
// SnapSerializer constants:
-SnapSerializer.prototype.app = 'Snap! 6, https://snap.berkeley.edu';
+SnapSerializer.prototype.app = 'Snap! 7dev, https://snap.berkeley.edu';
SnapSerializer.prototype.thumbnailSize = new Point(160, 120);
@@ -279,7 +283,7 @@ function SnapSerializer() {
// SnapSerializer initialization:
SnapSerializer.prototype.init = function () {
- this.project = {};
+ this.scene = new Scene();
this.objects = {};
this.mediaDict = {};
};
@@ -316,7 +320,9 @@ SnapSerializer.prototype.loadProjectModel = function (xmlNode, ide, remixID) {
// show a warning if the origin apps differ
var appInfo = xmlNode.attributes.app,
- app = appInfo ? appInfo.split(' ')[0] : null;
+ app = appInfo ? appInfo.split(' ')[0] : null,
+ scenesModel = xmlNode.childNamed('scenes'),
+ project = new Project();
if (ide && app && app !== this.app.split(' ')[0]) {
ide.inform(
@@ -326,18 +332,28 @@ SnapSerializer.prototype.loadProjectModel = function (xmlNode, ide, remixID) {
'\n\nand may be incompatible or fail to load here.'
);
}
- return this.rawLoadProjectModel(xmlNode, remixID);
+ if (scenesModel) {
+ if (scenesModel.attributes.select) {
+ project.sceneIdx = +scenesModel.attributes.select;
+ }
+ scenesModel.childrenNamed('scene').forEach(model =>
+ project.scenes.add(this.loadScene(model))
+ );
+ } else {
+ project.scenes.add(this.loadScene(xmlNode, remixID));
+ }
+ return project.initialize();
};
-SnapSerializer.prototype.rawLoadProjectModel = function (xmlNode, remixID) {
+SnapSerializer.prototype.loadScene = function (xmlNode, remixID) {
// private
- var project = {sprites: {}},
+ var scene = new Scene(),
model,
nameID;
- this.project = project;
+ this.scene = scene;
- model = {project: xmlNode };
+ model = {scene: xmlNode };
if (+xmlNode.attributes.version > this.version) {
throw 'Project uses newer version of Serializer';
}
@@ -345,8 +361,8 @@ SnapSerializer.prototype.rawLoadProjectModel = function (xmlNode, remixID) {
/* Project Info */
this.objects = {};
- project.name = model.project.attributes.name;
- if (!project.name) {
+ scene.name = model.scene.attributes.name;
+ if (!scene.name) {
nameID = 1;
while (
Object.prototype.hasOwnProperty.call(
@@ -356,138 +372,140 @@ SnapSerializer.prototype.rawLoadProjectModel = function (xmlNode, remixID) {
) {
nameID += 1;
}
- project.name = 'Untitled ' + nameID;
+ scene.name = 'Untitled ' + nameID;
}
- model.notes = model.project.childNamed('notes');
+ model.notes = model.scene.childNamed('notes');
if (model.notes) {
- project.notes = model.notes.contents;
+ scene.notes = model.notes.contents;
}
- model.globalVariables = model.project.childNamed('variables');
- project.globalVariables = new VariableFrame();
+ model.globalVariables = model.scene.childNamed('variables');
+
+ scene.unifiedPalette = model.scene.attributes.unifiedPalette === 'true';
/* Stage */
- model.stage = model.project.require('stage');
+ model.stage = model.scene.require('stage');
StageMorph.prototype.frameRate = 0;
- project.stage = new StageMorph(project.globalVariables);
- project.stage.remixID = remixID;
+ scene.stage.remixID = remixID;
+
if (Object.prototype.hasOwnProperty.call(
model.stage.attributes,
'id'
)) {
- this.objects[model.stage.attributes.id] = project.stage;
+ this.objects[model.stage.attributes.id] = scene.stage;
}
if (model.stage.attributes.name) {
- project.stage.name = model.stage.attributes.name;
+ scene.stage.name = model.stage.attributes.name;
}
if (model.stage.attributes.color) {
- project.stage.color = this.loadColor(model.stage.attributes.color);
- project.stage.cachedHSV = project.stage.color.hsv();
+ scene.stage.color = this.loadColor(model.stage.attributes.color);
+ scene.stage.cachedHSV = scene.stage.color.hsv();
}
if (model.stage.attributes.scheduled === 'true') {
- project.stage.fps = 30;
+ scene.stage.fps = 30;
StageMorph.prototype.frameRate = 30;
}
if (model.stage.attributes.volume) {
- project.stage.volume = +model.stage.attributes.volume;
+ scene.stage.volume = +model.stage.attributes.volume;
}
if (model.stage.attributes.pan) {
- project.stage.pan = +model.stage.attributes.pan;
+ scene.stage.pan = +model.stage.attributes.pan;
}
if (model.stage.attributes.penlog) {
- StageMorph.prototype.enablePenLogging =
+ scene.enablePenLogging =
(model.stage.attributes.penlog === 'true');
}
model.pentrails = model.stage.childNamed('pentrails');
if (model.pentrails) {
- project.pentrails = new Image();
- project.pentrails.onload = function () {
- if (project.stage.trailsCanvas) { // work-around a bug in FF
- normalizeCanvas(project.stage.trailsCanvas);
- var context = project.stage.trailsCanvas.getContext('2d');
- context.drawImage(project.pentrails, 0, 0);
- project.stage.changed();
+ scene.pentrails = new Image();
+ scene.pentrails.onload = function () {
+ if (scene.stage.trailsCanvas) { // work-around a bug in FF
+ normalizeCanvas(scene.stage.trailsCanvas);
+ var context = scene.stage.trailsCanvas.getContext('2d');
+ context.drawImage(scene.pentrails, 0, 0);
+ scene.stage.changed();
}
};
- project.pentrails.src = model.pentrails.contents;
+ scene.pentrails.src = model.pentrails.contents;
}
- project.stage.setTempo(model.stage.attributes.tempo);
- StageMorph.prototype.dimensions = new Point(480, 360);
+ scene.stage.setTempo(model.stage.attributes.tempo);
if (model.stage.attributes.width) {
- StageMorph.prototype.dimensions.x =
+ scene.stage.dimensions.x =
Math.max(+model.stage.attributes.width, 240);
}
if (model.stage.attributes.height) {
- StageMorph.prototype.dimensions.y =
+ scene.stage.dimensions.y =
Math.max(+model.stage.attributes.height, 180);
}
- project.stage.setExtent(StageMorph.prototype.dimensions);
- SpriteMorph.prototype.useFlatLineEnds =
+ scene.stage.setExtent(scene.stage.dimensions);
+ scene.useFlatLineEnds =
model.stage.attributes.lines === 'flat';
BooleanSlotMorph.prototype.isTernary =
model.stage.attributes.ternary !== 'false';
- Process.prototype.enableHyperOps =
+ scene.enableHyperOps =
model.stage.attributes.hyperops !== 'false';
- project.stage.isThreadSafe =
+ scene.stage.isThreadSafe =
model.stage.attributes.threadsafe === 'true';
- StageMorph.prototype.enableCodeMapping =
+ scene.enableCodeMapping =
model.stage.attributes.codify === 'true';
- StageMorph.prototype.enableInheritance =
+ scene.enableInheritance =
model.stage.attributes.inheritance !== 'false';
- StageMorph.prototype.enableSublistIDs =
+ scene.enableSublistIDs =
model.stage.attributes.sublistIDs === 'true';
- model.hiddenPrimitives = model.project.childNamed('hidden');
+ model.hiddenPrimitives = model.scene.childNamed('hidden');
if (model.hiddenPrimitives) {
model.hiddenPrimitives.contents.split(' ').forEach(
sel => {
if (sel) {
- StageMorph.prototype.hiddenPrimitives[sel] = true;
+ scene.hiddenPrimitives[sel] = true;
}
}
);
}
- model.codeHeaders = model.project.childNamed('headers');
+ model.codeHeaders = model.scene.childNamed('headers');
if (model.codeHeaders) {
model.codeHeaders.children.forEach(
- xml => StageMorph.prototype.codeHeaders[xml.tag] = xml.contents
+ xml => scene.codeHeaders[xml.tag] = xml.contents
);
}
- model.codeMappings = model.project.childNamed('code');
+ model.codeMappings = model.scene.childNamed('code');
if (model.codeMappings) {
model.codeMappings.children.forEach(
- xml => StageMorph.prototype.codeMappings[xml.tag] = xml.contents
+ xml => scene.codeMappings[xml.tag] = xml.contents
);
}
- model.globalBlocks = model.project.childNamed('blocks');
+ model.globalBlocks = model.scene.childNamed('blocks');
if (model.globalBlocks) {
- this.loadCustomBlocks(project.stage, model.globalBlocks, true);
+ this.loadCustomBlocks(scene.stage, model.globalBlocks, true);
this.populateCustomBlocks(
- project.stage,
+ scene.stage,
model.globalBlocks,
true
);
}
- this.loadObject(project.stage, model.stage);
+ this.loadObject(scene.stage, model.stage);
/* Sprites */
model.sprites = model.stage.require('sprites');
- project.sprites[project.stage.name] = project.stage;
-
+ if (model.sprites.attributes.select) {
+ scene.spriteIdx = +model.sprites.attributes.select;
+ }
+ scene.spritesDict[scene.stage.name] = scene.stage;
model.sprites.childrenNamed('sprite').forEach(
model => this.loadValue(model)
);
// restore inheritance and nesting associations
- this.project.stage.children.forEach(sprite => {
+ this.scene.stage.children.forEach(sprite => {
var exemplar, anchor;
if (sprite.inheritanceInfo) { // only sprites can inherit
- exemplar = this.project.sprites[
+ exemplar = this.scene.spritesDict[
sprite.inheritanceInfo.exemplar
];
if (exemplar) {
@@ -497,14 +515,14 @@ SnapSerializer.prototype.rawLoadProjectModel = function (xmlNode, remixID) {
sprite.updatePropagationCache();
}
if (sprite.nestingInfo) { // only sprites may have nesting info
- anchor = this.project.sprites[sprite.nestingInfo.anchor];
+ anchor = this.scene.spritesDict[sprite.nestingInfo.anchor];
if (anchor) {
anchor.attachPart(sprite);
}
sprite.rotatesWithAnchor = (sprite.nestingInfo.synch === 'true');
}
});
- this.project.stage.children.forEach(sprite => {
+ this.scene.stage.children.forEach(sprite => {
var costume;
if (sprite.nestingInfo) { // only sprites may have nesting info
sprite.nestingScale = +(sprite.nestingInfo.scale || sprite.scale);
@@ -541,7 +559,7 @@ SnapSerializer.prototype.rawLoadProjectModel = function (xmlNode, remixID) {
if (model.globalVariables) {
this.loadVariables(
- project.globalVariables,
+ scene.globalVariables,
model.globalVariables
);
}
@@ -557,7 +575,7 @@ SnapSerializer.prototype.rawLoadProjectModel = function (xmlNode, remixID) {
target = Object.prototype.hasOwnProperty.call(
model.attributes,
'scope'
- ) ? project.sprites[model.attributes.scope] : null;
+ ) ? scene.spritesDict[model.attributes.scope] : null;
// determine whether the watcher is hidden, slightly
// complicated to retain backward compatibility
@@ -575,7 +593,7 @@ SnapSerializer.prototype.rawLoadProjectModel = function (xmlNode, remixID) {
watcher = new WatcherMorph(
model.attributes['var'],
color,
- isNil(target) ? project.globalVariables
+ isNil(target) ? scene.globalVariables
: target.variables,
model.attributes['var'],
hidden
@@ -595,12 +613,12 @@ SnapSerializer.prototype.rawLoadProjectModel = function (xmlNode, remixID) {
watcher.setSliderMax(model.attributes.max || '100', true);
}
watcher.setPosition(
- project.stage.topLeft().add(new Point(
+ scene.stage.topLeft().add(new Point(
+model.attributes.x || 0,
+model.attributes.y || 0
))
);
- project.stage.add(watcher);
+ scene.stage.add(watcher);
watcher.onNextStep = function () {this.currentValue = null; };
// set watcher's contentsMorph's extent if it is showing a list and
@@ -621,25 +639,22 @@ SnapSerializer.prototype.rawLoadProjectModel = function (xmlNode, remixID) {
});
// clear sprites' inherited methods caches, if any
- this.project.stage.children.forEach(
+ this.scene.stage.children.forEach(
sprite => sprite.inheritedMethodsCache = []
);
this.objects = {};
- return project;
+ return scene.initialize();
};
SnapSerializer.prototype.loadBlocks = function (xmlString, targetStage) {
// public - answer a new Array of custom block definitions
// represented by the given XML String
- var stage = new StageMorph(),
- model;
+ var stage, model;
- this.project = {
- stage: stage,
- sprites: {},
- targetStage: targetStage // for secondary custom block def look-up
- };
+ this.scene = new Scene();
+ this.scene.targetStage = targetStage; // for secondary block def look-up
+ stage = this.scene.stage;
model = this.parse(xmlString);
if (+model.attributes.version > this.version) {
throw 'Module uses newer version of Serializer';
@@ -653,36 +668,33 @@ SnapSerializer.prototype.loadBlocks = function (xmlString, targetStage) {
this.objects = {};
stage.globalBlocks.forEach(def => def.receiver = null);
this.objects = {};
- this.project = {};
+ this.scene = new Scene();
this.mediaDict = {};
return stage.globalBlocks;
};
SnapSerializer.prototype.loadSprites = function (xmlString, ide) {
// public - import a set of sprites represented by xmlString
- // into the current project of the ide
- var model, project;
+ // into the current scene of the ide
+ var model, scene;
- project = this.project = {
- globalVariables: ide.globalVariables,
- stage: ide.stage,
- sprites: {}
- };
- project.sprites[project.stage.name] = project.stage;
+ this.scene = new Scene(ide.stage);
+ scene = this.scene;
+ scene.spritesDict[scene.stage.name] = scene.stage;
model = this.parse(xmlString);
if (+model.attributes.version > this.version) {
throw 'Module uses newer version of Serializer';
}
model.childrenNamed('sprite').forEach(model => {
- var sprite = new SpriteMorph(project.globalVariables);
+ var sprite = new SpriteMorph(scene.globalVariables);
if (model.attributes.id) {
this.objects[model.attributes.id] = sprite;
}
if (model.attributes.name) {
sprite.name = ide.newSpriteName(model.attributes.name);
- project.sprites[sprite.name] = sprite;
+ scene.spritesDict[sprite.name] = sprite;
}
if (model.attributes.color) {
sprite.color = this.loadColor(model.attributes.color);
@@ -697,7 +709,7 @@ SnapSerializer.prototype.loadSprites = function (xmlString, ide) {
if (model.attributes.pan) {
sprite.pan = +model.attributes.pan;
}
- project.stage.add(sprite);
+ scene.stage.add(sprite);
ide.sprites.add(sprite);
sprite.scale = parseFloat(model.attributes.scale || '1');
sprite.rotationStyle = parseFloat(
@@ -713,10 +725,10 @@ SnapSerializer.prototype.loadSprites = function (xmlString, ide) {
});
// restore inheritance and nesting associations
- project.stage.children.forEach(sprite => {
+ scene.stage.children.forEach(sprite => {
var exemplar, anchor;
if (sprite.inheritanceInfo) { // only sprites can inherit
- exemplar = project.sprites[
+ exemplar = scene.spritesDict[
sprite.inheritanceInfo.exemplar
];
if (exemplar) {
@@ -724,14 +736,14 @@ SnapSerializer.prototype.loadSprites = function (xmlString, ide) {
}
}
if (sprite.nestingInfo) { // only sprites may have nesting info
- anchor = project.sprites[sprite.nestingInfo.anchor];
+ anchor = scene.spritesDict[sprite.nestingInfo.anchor];
if (anchor) {
anchor.attachPart(sprite);
}
sprite.rotatesWithAnchor = (sprite.nestingInfo.synch === 'true');
}
});
- project.stage.children.forEach(sprite => {
+ scene.stage.children.forEach(sprite => {
delete sprite.inheritanceInfo;
if (sprite.nestingInfo) { // only sprites may have nesting info
sprite.nestingScale = +(sprite.nestingInfo.scale || sprite.scale);
@@ -740,7 +752,7 @@ SnapSerializer.prototype.loadSprites = function (xmlString, ide) {
});
this.objects = {};
- this.project = {};
+ this.scene = new Scene();
this.mediaDict = {};
ide.stage.fixLayout();
@@ -1111,10 +1123,10 @@ SnapSerializer.prototype.loadScript = function (model, object) {
var topBlock, block, nextBlock;
// Check whether we're importing a single script, not a script as part of a
- // whole project
- if (!this.project.stage) {
- this.project.stage = object.parentThatIsA(StageMorph);
- this.project.targetStage = this.project.stage;
+ // whole scene
+ if (!this.scene.stage) {
+ this.scene.stage = object.parentThatIsA(StageMorph);
+ this.scene.targetStage = this.scene.stage;
}
model.children.forEach(child => {
@@ -1174,15 +1186,15 @@ SnapSerializer.prototype.loadBlock = function (model, isReporter, object) {
}
} else if (model.tag === 'custom-block') {
isGlobal = model.attributes.scope ? false : true;
- receiver = isGlobal ? this.project.stage : object;
+ receiver = isGlobal ? this.scene.stage : object;
if (isGlobal) {
info = detect(
receiver.globalBlocks,
block => block.blockSpec() === model.attributes.s
);
- if (!info && this.project.targetStage) { // importing block files
+ if (!info && this.scene.targetStage) { // importing block files
info = detect(
- this.project.targetStage.globalBlocks,
+ this.scene.targetStage.globalBlocks,
block => block.blockSpec() === model.attributes.s
);
}
@@ -1399,13 +1411,13 @@ SnapSerializer.prototype.loadValue = function (model, object) {
});
return v;
case 'sprite':
- v = new SpriteMorph(this.project.globalVariables);
+ v = new SpriteMorph(this.scene.globalVariables);
if (model.attributes.id) {
this.objects[model.attributes.id] = v;
}
if (model.attributes.name) {
v.name = model.attributes.name;
- this.project.sprites[model.attributes.name] = v;
+ this.scene.spritesDict[model.attributes.name] = v;
}
if (model.attributes.idx) {
v.idx = +model.attributes.idx;
@@ -1423,7 +1435,7 @@ SnapSerializer.prototype.loadValue = function (model, object) {
if (model.attributes.pan) {
v.pan = +model.attributes.pan;
}
- this.project.stage.add(v);
+ this.scene.stage.add(v);
v.scale = parseFloat(model.attributes.scale || '1');
v.rotationStyle = parseFloat(
model.attributes.rotation || '1'
@@ -1605,48 +1617,6 @@ SnapSerializer.prototype.loadColor = function (colorString) {
);
};
-SnapSerializer.prototype.openProject = function (project, ide) {
- var stage = ide.stage,
- sprites = [],
- sprite;
- if (!project || !project.stage) {
- return;
- }
- ide.siblings().forEach(morph =>
- morph.destroy()
- );
- ide.projectName = project.name;
- ide.projectNotes = project.notes || '';
- if (ide.globalVariables) {
- ide.globalVariables = project.globalVariables;
- }
- if (stage) {
- stage.destroy();
- }
- ide.add(project.stage);
- ide.stage = project.stage;
- sprites = ide.stage.children.filter(
- child => child instanceof SpriteMorph
- );
- sprites.sort((x, y) => x.idx - y.idx);
-
- ide.sprites = new List(sprites);
- sprite = sprites[0] || project.stage;
-
- if (sizeOf(this.mediaDict) > 0) {
- ide.hasChangedMedia = false;
- this.mediaDict = {};
- } else {
- ide.hasChangedMedia = true;
- }
- project.stage.fixLayout();
- project.stage.pauseGenericHatBlocks();
- ide.createCorral();
- ide.selectSprite(sprite);
- ide.fixLayout();
- ide.world().keyboardFocus = project.stage;
-};
-
// SnapSerializer XML-representation of objects:
// Generics
@@ -1658,24 +1628,39 @@ Array.prototype.toXML = function (serializer) {
);
};
-// Sprites
+// Scenes & multi-scene projects
-StageMorph.prototype.toXML = function (serializer) {
- var thumbnail = normalizeCanvas(
- this.thumbnail(SnapSerializer.prototype.thumbnailSize),
- true
- ),
- thumbdata,
- costumeIdx = this.getCostumeIdx(),
- ide = this.parentThatIsA(IDE_Morph);
+Project.prototype.toXML = function (serializer) {
+ var thumbdata;
- // catch cross-origin tainting exception when using SVG costumes
+ // thumb data catch cross-origin tainting exception when using SVG costumes
try {
- thumbdata = thumbnail.toDataURL('image/png');
+ thumbdata = this.thumbnail.toDataURL('image/png');
} catch (error) {
thumbdata = null;
}
+ return serializer.format(
+ '' +
+ '$' +
+ '$' +
+ '%' +
+ '',
+ this.name || localize('Untitled'),
+ serializer.app,
+ serializer.version,
+ this.notes || '',
+ thumbdata,
+ this.scenes.asArray().indexOf(
+ this.currentScene) + 1,
+ serializer.store(this.scenes.itemsArray())
+ );
+};
+
+Scene.prototype.toXML = function (serializer) {
+ var tmp = new Scene(),
+ xml;
+
function code(key) {
var str = '';
Object.keys(StageMorph.prototype[key]).forEach(
@@ -1692,12 +1677,43 @@ StageMorph.prototype.toXML = function (serializer) {
return str;
}
+ tmp.captureGlobalSettings();
+ this.applyGlobalSettings();
+ xml = serializer.format(
+ '' +
+ '$' +
+ '$' +
+ '%' +
+ '%' +
+ '%' +
+ '%' +
+ '%' + // stage
+ '',
+ this.name || localize('Untitled'),
+ this.unifiedPalette,
+ this.notes || '',
+ Object.keys(StageMorph.prototype.hiddenPrimitives).reduce(
+ (a, b) => a + ' ' + b,
+ ''
+ ),
+ code('codeHeaders'),
+ code('codeMappings'),
+ serializer.store(this.stage.globalBlocks),
+ serializer.store(this.globalVariables),
+ serializer.store(this.stage)
+ );
+ tmp.applyGlobalSettings();
+ return xml;
+};
+
+// Sprites
+
+StageMorph.prototype.toXML = function (serializer) {
+ var costumeIdx = this.getCostumeIdx();
+
this.removeAllClones();
return serializer.format(
- '' +
- '$' +
- '$' +
- '%' +
'%' +
'%' +
- '%%' +
- '' +
- '$' +
- '%' +
- '%' +
- '%' +
- '%' +
- '',
- (ide && ide.projectName) ? ide.projectName : localize('Untitled'),
- serializer.app,
- serializer.version,
- (ide && ide.projectNotes) ? ide.projectNotes : '',
- thumbdata,
- this.name,
- StageMorph.prototype.dimensions.x,
- StageMorph.prototype.dimensions.y,
+ '%' +
+ '%' +
+ '',
+ this.dimensions.x,
+ this.dimensions.y,
costumeIdx,
this.color.r,
this.color.g,
@@ -1763,23 +1768,14 @@ StageMorph.prototype.toXML = function (serializer) {
serializer.store(this.variables),
serializer.store(this.customBlocks),
serializer.store(this.scripts),
- serializer.store(this.children),
- Object.keys(StageMorph.prototype.hiddenPrimitives).reduce(
- (a, b) => a + ' ' + b,
- ''
- ),
- code('codeHeaders'),
- code('codeMappings'),
- serializer.store(this.globalBlocks),
- (ide && ide.globalVariables) ?
- serializer.store(ide.globalVariables) : ''
+ serializer.root.sprites.asArray().indexOf(
+ serializer.root.currentSprite) + 1,
+ serializer.store(this.children)
);
};
SpriteMorph.prototype.toXML = function (serializer) {
- var stage = this.parentThatIsA(StageMorph),
- ide = stage ? stage.parentThatIsA(IDE_Morph) : null,
- idx = ide ? ide.sprites.asArray().indexOf(this) + 1 : 0,
+ var idx = serializer.root.sprites.asArray().indexOf(this) + 1,
costumeIdx = this.getCostumeIdx(),
noCostumes = this.inheritsAttribute('costumes'),
noSounds = this.inheritsAttribute('sounds'),
diff --git a/src/threads.js b/src/threads.js
index 7b3b5bc8..94626fcb 100644
--- a/src/threads.js
+++ b/src/threads.js
@@ -4657,6 +4657,55 @@ Process.prototype.goToLayer = function (name) {
}
};
+// Process scene primitives
+
+Process.prototype.doSwitchToScene = function (id) {
+ var rcvr = this.blockReceiver(),
+ idx = 0,
+ ide, scenes, num, scene;
+
+ this.assertAlive(rcvr);
+ ide = rcvr.parentThatIsA(IDE_Morph);
+ scenes = ide.scenes;
+
+ if (id instanceof Array) { // special named indices
+ switch (this.inputOption(id)) {
+ case 'next':
+ idx = scenes.indexOf(ide.scene) + 1;
+ if (idx > scenes.length()) {
+ idx = 1;
+ }
+ break;
+ case 'previous':
+ idx = scenes.indexOf(ide.scene) - 1;
+ if (idx < 1) {
+ idx = scenes.length();
+ }
+ break;
+ case 'last':
+ idx = scenes.length();
+ break;
+ case 'random':
+ idx = this.reportBasicRandom(1, scenes.length());
+ break;
+ }
+ ide.switchToScene(scenes.at(idx));
+ return;
+ }
+
+ scene = detect(scenes.itemsArray(), scn => scn.name === id);
+ if (scene === null) {
+ num = parseFloat(id);
+ if (isNaN(num)) {
+ return;
+ }
+ scene = scenes.at(num);
+ }
+
+ ide.switchToScene(scene);
+};
+
+
// Process color primitives
Process.prototype.setHSVA = function (name, num) {