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) {