diff --git a/blocks.js b/blocks.js index 8847aaa7..9487ac5f 100644 --- a/blocks.js +++ b/blocks.js @@ -144,11 +144,11 @@ fontHeight, TableFrameMorph, SpriteMorph, Context, ListWatcherMorph, CellMorph, DialogBoxMorph, BlockInputFragmentMorph, PrototypeHatBlockMorph, Costume, IDE_Morph, BlockDialogMorph, BlockEditorMorph, localize, isNil, isSnapObject, PushButtonMorph, SpriteIconMorph, Process, AlignmentMorph, -CustomCommandBlockMorph, SymbolMorph, ToggleButtonMorph*/ +CustomCommandBlockMorph, SymbolMorph, ToggleButtonMorph, DialMorph*/ // Global stuff //////////////////////////////////////////////////////// -modules.blocks = '2018-January-22'; +modules.blocks = '2018-January-25'; var SyntaxElementMorph; var BlockMorph; @@ -934,6 +934,7 @@ SyntaxElementMorph.prototype.labelPart = function (spec) { null, true, { + '§_dir': null, '(90) right' : 90, '(-90) left' : -90, '(0) up' : '0', @@ -4067,6 +4068,9 @@ BlockMorph.prototype.situation = function () { BlockMorph.prototype.prepareToBeGrabbed = function (hand) { var myself = this; + this.allInputs().forEach(function (input) { + delete input.bindingID; + }); this.allComments().forEach(function (comment) { comment.startFollowing(myself, hand.world); }); @@ -8257,7 +8261,8 @@ InputSlotMorph.prototype.menuFromDict = function ( noEmptyOption, enableKeyboard) { - var key, + var key, dial, + myself = this, menu = new MenuMorph( this.userSetContents, null, @@ -8265,6 +8270,11 @@ InputSlotMorph.prototype.menuFromDict = function ( this.fontSize ); + function update (num) { + myself.setContents(num); + myself.reactToSliderEdit(); + } + if (choices instanceof Function) { choices = choices.call(this); } else if (isString(choices)) { @@ -8282,6 +8292,17 @@ InputSlotMorph.prototype.menuFromDict = function ( menu.addLine(); } else if (key.indexOf('§_def') === 0) { menu.addItem(choices[key], choices[key]); + } else if (key.indexOf('§_dir') === 0) { + dial = new DialMorph(); + dial.rootForGrab = function () {return this; }; + dial.target = this; + dial.action = update; + dial.fillColor = this.parent.color; + dial.setRadius(this.fontSize * 3); + dial.setValue(this.evaluate(), false, true); + menu.addLine(); + menu.items.push(dial); + menu.addLine(); } else if (choices[key] instanceof Object && !(choices[key] instanceof Array) && (typeof choices[key] !== 'function')) { @@ -9199,10 +9220,6 @@ InputSlotMorph.prototype.drawRoundBorder = function (context) { context.stroke(); }; - - - - // TemplateSlotMorph /////////////////////////////////////////////////// /* diff --git a/gui.js b/gui.js index bc3db6c9..12dcd8ec 100644 --- a/gui.js +++ b/gui.js @@ -75,7 +75,7 @@ isRetinaSupported, SliderMorph, Animation*/ // Global stuff //////////////////////////////////////////////////////// -modules.gui = '2018-January-23'; +modules.gui = '2018-January-25'; // Declarations @@ -301,7 +301,8 @@ IDE_Morph.prototype.openIn = function (world) { // prevent non-DialogBoxMorphs from being dropped // onto the World in user-mode world.reactToDropOf = function (morph) { - if (!(morph instanceof DialogBoxMorph)) { + if (!(morph instanceof DialogBoxMorph || + (morph instanceof MenuMorph))) { if (world.hand.grabOrigin) { morph.slideBackTo(world.hand.grabOrigin); } else { @@ -4454,7 +4455,8 @@ IDE_Morph.prototype.switchToUserMode = function () { // prevent non-DialogBoxMorphs from being dropped // onto the World in user-mode world.reactToDropOf = function (morph) { - if (!(morph instanceof DialogBoxMorph)) { + if (!(morph instanceof DialogBoxMorph || + (morph instanceof MenuMorph))) { if (world.hand.grabOrigin) { morph.slideBackTo(world.hand.grabOrigin); } else { diff --git a/history.txt b/history.txt index 874c82d0..4347554b 100755 --- a/history.txt +++ b/history.txt @@ -3868,6 +3868,14 @@ Fixes: * fixed #1972, thanks, Joan! * Objects, GUI: When deleting a temporary clone, detach all its parts and delete the temporary ones +180125 +------ +* Morphic: new DialMorph widget +* Blocks: added dial widget to POINT IN DIRECTION's drop-down menu +* Objects: added "rotate" option to Sprite context menu +* Threads, Blocks: fixed Joan's fix for #1972, because it broke HOFs + + v4.1.1 New Features: * translation support for custom blocks @@ -3875,6 +3883,8 @@ v4.1.1 New Features: * included local methods in the OF-block's left drop-down menu * added "width" and "height" selectors to Pixels library * added scroll events, thanks, Bernat! +* new dial widget POINT IN DIRECTION's drop-down menu +* new "rotate" option for sprite context menu Notable Changes: * global and local variables are now separat in the palette, each sorted alphabetically, local vars marked with location pin (only in palette) diff --git a/lang-de.js b/lang-de.js index aae54b32..e82f6b86 100644 --- a/lang-de.js +++ b/lang-de.js @@ -185,7 +185,7 @@ SnapTranslator.dict.de = { 'translator_e-mail': 'jens@moenig.org', // optional 'last_changed': - '2018-01-22', // this, too, will appear in the Translators tab + '2018-01-25', // this, too, will appear in the Translators tab // GUI // control bar: @@ -969,6 +969,8 @@ SnapTranslator.dict.de = { 'Angelpunkt', 'edit the costume\'s\nrotation center': 'Drehpunkt des Kostüms\nanzeigen und verschieben', + 'rotate': + 'Drehen', 'detach from': 'Abtrennen von', 'detach all parts': diff --git a/locale.js b/locale.js index ec091bce..fac9eebc 100644 --- a/locale.js +++ b/locale.js @@ -42,7 +42,7 @@ /*global modules, contains*/ -modules.locale = '2018-January-22'; +modules.locale = '2018-January-25'; // Global stuff @@ -160,7 +160,7 @@ SnapTranslator.dict.de = { 'translator_e-mail': 'jens@moenig.org', 'last_changed': - '2018-01-22' + '2018-01-25' }; SnapTranslator.dict.it = { diff --git a/morphic.js b/morphic.js index 64eab5c7..2ba0214f 100644 --- a/morphic.js +++ b/morphic.js @@ -85,6 +85,7 @@ ColorPaletteMorph GrayPaletteMorph ColorPickerMorph + DialMorph FrameMorph ScrollFrameMorph ListMorph @@ -126,6 +127,7 @@ CursorMorph BoxMorph SpeechBubbleMorph + DialMorph CircleBoxMorph SliderButtonMorph SliderMorph @@ -1161,7 +1163,7 @@ /*global window, HTMLCanvasElement, FileReader, Audio, FileList*/ -var morphicVersion = '2018-January-22'; +var morphicVersion = '2018-January-25'; var modules = {}; // keep track of additional loaded modules var useBlurredShadows = getBlurredShadowSupport(); // check for Chrome-bug @@ -4843,6 +4845,52 @@ PenMorph.prototype.setHeading = function (degrees) { this.changed(); }; +PenMorph.prototype.numericalSetters = function () { + // for context menu demo purposes + return [ + 'setLeft', + 'setTop', + 'setWidth', + 'setHeight', + 'setAlphaScaled', + 'setHeading' + ]; +}; + +// PenMorph menu: + +PenMorph.prototype.developersMenu = function () { + var menu = PenMorph.uber.developersMenu.call(this); + menu.addLine(); + menu.addItem( + 'set rotation', + "setRotation", + 'interactively turn this morph\nusing a dial widget' + ); + return menu; +}; + +PenMorph.prototype.setRotation = function () { + var menu, dial, + name = this.name || this.constructor.name; + if (name.length > 10) { + name = name.slice(0, 9) + '...'; + } + menu = new MenuMorph(this, name); + dial = new DialMorph(null, null, this.heading); + dial.rootForGrab = function () {return this; }; + dial.target = this; + dial.action = 'setHeading'; + menu.items.push(dial); + menu.addLine(); + menu.addItem('(90) right', function () {this.setHeading(90); }); + menu.addItem('(-90) left', function () {this.setHeading(-90); }); + menu.addItem('(0) up', function () {this.setHeading(0); }); + menu.addItem('(180) down', function () {this.setHeading(180); }); + menu.isDraggable = true; + menu.popUpAtHand(this.world()); +}; + // PenMorph drawing: PenMorph.prototype.drawLine = function (start, dest) { @@ -6245,6 +6293,315 @@ SpeechBubbleMorph.prototype.fixLayout = function () { this.addShadow(new Point(2, 2), 80); }; +// DialMorph ////////////////////////////////////////////////////// + +// I am a knob than can be turned to select a number + +var DialMorph; + +// DialMorph inherits from Morph: + +DialMorph.prototype = new Morph(); +DialMorph.prototype.constructor = DialMorph; +DialMorph.uber = Morph.prototype; + +function DialMorph(min, max, value, tick, radius) { + this.init(min, max, value, tick, radius); +} + +DialMorph.prototype.init = function (min, max, value, tick, radius) { + this.target = null; + this.action = null; + this.min = min || 0; + this.max = max || 360; + this.value = Math.max(this.min, (value || 0) % this.max); + this.tick = tick || 15; + this.fillColor = null; + + DialMorph.uber.init.call(this); + + this.color = new Color(230, 230, 230); + this.noticesTransparentClick = true; + this.setRadius(radius || MorphicPreferences.menuFontSize * 4); +}; + +DialMorph.prototype.setRadius = function (radius) { + this.radius = radius; + this.setExtent(new Point(this.radius * 2, this.radius * 2)); +}; + +DialMorph.prototype.setValue = function (value, snapToTick, noUpdate) { + var range = this.max - this.min; + value = value || 0; + this.value = this.min + (((+value % range) + range) % range); + if (snapToTick) { + if (this.value < this.tick) { + this.value = this.min; + } else { + this.value -= this.value % this.tick % this.value; + } + } + this.drawNew(); + this.changed(); + if (noUpdate) {return; } + this.updateTarget(); +}; + +DialMorph.prototype.getValueOf = function (point) { + var range = this.max - this.min, + center = this.center(), + deltaX = point.x - center.x, + deltaY = center.y - point.y, + angle = Math.abs(deltaX) < 0.001 ? (deltaY < 0 ? 90 : 270) + : Math.round( + (deltaX >= 0 ? 0 : 180) + - (Math.atan(deltaY / deltaX) * 57.2957795131) + ), + value = angle + 90 % 360, + ratio = value / 360; + return range * ratio + this.min; +}; + +DialMorph.prototype.setExtent = function (aPoint) { + var size = Math.min(aPoint.x, aPoint.y); + this.radius = size / 2; + DialMorph.uber.setExtent.call(this, new Point(size, size)); +}; + +DialMorph.prototype.drawNew = function () { + var ctx, i, angle, x1, y1, x2, y2, + light = this.color.lighter().toString(), + range = this.max - this.min, + ticks = range / this.tick, + face = this.radius * 0.75, + inner = face * 0.85, + outer = face * 0.95; + + this.image = newCanvas(this.extent()); + ctx = this.image.getContext('2d'); + + // draw a light border: + ctx.fillStyle = light; + ctx.beginPath(); + ctx.arc( + this.radius, + this.radius, + face + Math.min(1, this.radius - face), + 0, + 2 * Math.PI, + false + ); + ctx.closePath(); + ctx.fill(); + + // fill circle: + ctx.fillStyle = this.color.toString(); + ctx.beginPath(); + ctx.arc( + this.radius, + this.radius, + face, + 0, + 2 * Math.PI, + false + ); + ctx.closePath(); + ctx.fill(); + + // fill value + angle = (this.value - this.min) * (Math.PI * 2) / range - Math.PI / 2; + ctx.fillStyle = (this.fillColor || this.color.darker()).toString(); + ctx.beginPath(); + ctx.arc( + this.radius, + this.radius, + face, + Math.PI / -2, + angle, + false + ); + ctx.lineTo(this.radius, this.radius); + ctx.closePath(); + ctx.fill(); + + // draw ticks: + ctx.strokeStyle = new Color(35, 35, 35).toString(); + ctx.lineWidth = 1; + for (i = 0; i < ticks; i += 1) { + angle = (i - 3) * (Math.PI * 2) / ticks - Math.PI / 2; + ctx.beginPath(); + x1 = this.radius + Math.cos(angle) * inner; + y1 = this.radius + Math.sin(angle) * inner; + x2 = this.radius + Math.cos(angle) * outer; + y2 = this.radius + Math.sin(angle) * outer; + ctx.moveTo(x1, y1); + ctx.lineTo(x2, y2); + ctx.stroke(); + } + + // draw a filled center: + inner = face * 0.05; + ctx.fillStyle = 'black'; + ctx.beginPath(); + ctx.arc( + this.radius, + this.radius, + inner, + 0, + 2 * Math.PI, + false + ); + ctx.closePath(); + ctx.fill(); + + // draw the inner hand: + ctx.strokeStyle = 'black'; + ctx.lineWidth = 1; + angle = (this.value - this.min) * (Math.PI * 2) / range - Math.PI / 2; + outer = face * 0.8; + x1 = this.radius + Math.cos(angle) * inner; + y1 = this.radius + Math.sin(angle) * inner; + x2 = this.radius + Math.cos(angle) * outer; + y2 = this.radius + Math.sin(angle) * outer; + ctx.beginPath(); + ctx.moveTo(x1, y1); + ctx.lineTo(x2, y2); + ctx.stroke(); + + // draw a read-out circle: + inner = inner * 2; + x2 = this.radius + Math.cos(angle) * (outer + inner); + y2 = this.radius + Math.sin(angle) * (outer + inner); + ctx.fillStyle = 'black'; + ctx.beginPath(); + ctx.arc( + x2, + y2, + inner, + 0, + 2 * Math.PI, + false + ); + ctx.closePath(); + ctx.stroke(); + + // draw the outer hand: + angle = (this.value - this.min) * (Math.PI * 2) / range - Math.PI / 2; + x1 = this.radius + Math.cos(angle) * face; + y1 = this.radius + Math.sin(angle) * face; + x2 = this.radius + Math.cos(angle) * (this.radius - 1); + y2 = this.radius + Math.sin(angle) * (this.radius - 1); + ctx.beginPath(); + ctx.moveTo(x1, y1); + ctx.lineTo(x2, y2); + ctx.lineWidth = 3; + ctx.strokeStyle = light; + ctx.stroke(); + ctx.lineWidth = 1; + ctx.strokeStyle = 'black'; + ctx.stroke(); + + // draw arrow tip: + angle = radians(degrees(angle) - 4); + x1 = this.radius + Math.cos(angle) * this.radius * 0.9; + y1 = this.radius + Math.sin(angle) * this.radius * 0.9; + ctx.beginPath(); + ctx.moveTo(x1, y1); + angle = radians(degrees(angle) + 8); + x1 = this.radius + Math.cos(angle) * this.radius * 0.9; + y1 = this.radius + Math.sin(angle) * this.radius * 0.9; + ctx.lineTo(x1, y1); + ctx.lineTo(x2, y2); + ctx.closePath(); + ctx.lineWidth = 3; + ctx.strokeStyle = light; + ctx.stroke(); + ctx.lineWidth = 1; + ctx.strokeStyle = 'black'; + ctx.stroke(); + ctx.fill(); +}; + +// DialMorph stepping: + +DialMorph.prototype.step = null; + +DialMorph.prototype.mouseDownLeft = function (pos) { + var world, myself = this; + world = this.root(); + this.step = function () { + if (world.hand.mouseButton) { + myself.setValue( + myself.getValueOf(world.hand.bounds.origin), + world.currentKey !== 16 // snap to tick + ); + } else { + this.step = null; + } + }; +}; + +// DialMorph menu: + +DialMorph.prototype.developersMenu = function () { + var menu = DialMorph.uber.developersMenu.call(this); + menu.addLine(); + menu.addItem( + 'set target', + "setTarget", + 'select another morph\nwhose numerical property\nwill be ' + + 'controlled by this one' + ); + return menu; +}; + +DialMorph.prototype.setTarget = function () { + var choices = this.overlappedMorphs(), + menu = new MenuMorph(this, 'choose target:'), + myself = this; + + choices.push(this.world()); + choices.forEach(function (each) { + menu.addItem(each.toString().slice(0, 50), function () { + myself.target = each; + myself.setTargetSetter(); + }); + }); + if (choices.length === 1) { + this.target = choices[0]; + this.setTargetSetter(); + } else if (choices.length > 0) { + menu.popUpAtHand(this.world()); + } +}; + +DialMorph.prototype.setTargetSetter = function () { + var choices = this.target.numericalSetters(), + menu = new MenuMorph(this, 'choose target property:'), + myself = this; + + choices.forEach(function (each) { + menu.addItem(each, function () { + myself.action = each; + }); + }); + if (choices.length === 1) { + this.action = choices[0]; + } else if (choices.length > 0) { + menu.popUpAtHand(this.world()); + } +}; + +DialMorph.prototype.updateTarget = function () { + if (this.action) { + if (typeof this.action === 'function') { + this.action.call(this.target, this.value); + } else { // assume it's a String + this.target[this.action](this.value); + } + } +}; + // CircleBoxMorph ////////////////////////////////////////////////////// // I can be used for sliders @@ -7672,7 +8029,8 @@ MenuMorph.prototype.drawNew = function () { isLine = false; if (tuple instanceof StringFieldMorph || tuple instanceof ColorPickerMorph || - tuple instanceof SliderMorph) { + tuple instanceof SliderMorph || + tuple instanceof DialMorph) { item = tuple; } else if (tuple[0] === 0) { isLine = true; @@ -7729,7 +8087,8 @@ MenuMorph.prototype.maxWidth = function () { ); } else if ((item instanceof StringFieldMorph) || (item instanceof ColorPickerMorph) || - (item instanceof SliderMorph)) { + (item instanceof SliderMorph) || + (item instanceof DialMorph)) { w = Math.max(w, item.width()); } }); @@ -7744,7 +8103,9 @@ MenuMorph.prototype.adjustWidths = function () { isSelected, myself = this; this.children.forEach(function (item) { - item.silentSetWidth(w); + if (!(item instanceof DialMorph)) { + item.silentSetWidth(w); + } if (item instanceof MenuItemMorph) { item.fixLayout(); isSelected = (item.image === item.pressImage); @@ -10745,7 +11106,9 @@ HandMorph.prototype.grab = function (aMorph) { if (this.children.length === 0) { this.world.stopEditing(); this.grabOrigin = aMorph.situation(); - aMorph.addShadow(); + if (!(aMorph instanceof MenuMorph)) { + aMorph.addShadow(); + } if (aMorph.prepareToBeGrabbed) { aMorph.prepareToBeGrabbed(this); } @@ -10770,7 +11133,9 @@ HandMorph.prototype.drop = function () { morphToDrop.cachedFullImage = null; morphToDrop.cachedFullBounds = null; morphToDrop.changed(); - morphToDrop.removeShadow(); + if (!(morphToDrop instanceof MenuMorph)) { + morphToDrop.removeShadow(); + } this.children = []; this.setExtent(new Point()); if (morphToDrop.justDropped) { @@ -11895,6 +12260,10 @@ WorldMorph.prototype.userCreateMorph = function () { menu.addItem('slider', function () { create(new SliderMorph()); }); + menu.addItem('dial', function () { + newMorph = new DialMorph(); + newMorph.pickUp(this); + }); menu.addItem('frame', function () { newMorph = new FrameMorph(); newMorph.setExtent(new Point(350, 250)); diff --git a/objects.js b/objects.js index 9490dc34..4c21477e 100644 --- a/objects.js +++ b/objects.js @@ -83,7 +83,7 @@ BlockEditorMorph, BlockDialogMorph, PrototypeHatBlockMorph, localize, TableMorph, TableFrameMorph, normalizeCanvas, BooleanSlotMorph, HandleMorph, AlignmentMorph*/ -modules.objects = '2018-January-23'; +modules.objects = '2018-January-25'; var SpriteMorph; var StageMorph; @@ -3227,6 +3227,7 @@ SpriteMorph.prototype.userMenu = function () { } menu.addItem("delete", 'remove'); menu.addItem("move", 'moveCenter'); + menu.addItem("rotate", 'setRotation'); if (this.costume) { menu.addItem( "pivot", diff --git a/threads.js b/threads.js index 29f630a9..d8db1cc4 100644 --- a/threads.js +++ b/threads.js @@ -61,7 +61,7 @@ StageMorph, SpriteMorph, StagePrompterMorph, Note, modules, isString, copy, isNil, WatcherMorph, List, ListWatcherMorph, alert, console, TableMorph, TableFrameMorph, ColorSlotMorph, isSnapObject*/ -modules.threads = '2018-January-23'; +modules.threads = '2018-January-25'; var ThreadManager; var Process; @@ -855,7 +855,6 @@ Process.prototype.evaluateInput = function (input) { } else { ans = this.context.variables.getVar(input.bindingID); } - delete input.bindingID; } else { ans = input.evaluate(); if (ans) {