/* widgets.js additional GUI elements for morphic.js written by Jens Mönig jens@moenig.org Copyright (C) 2015 by Jens Mönig This file is part of Snap!. Snap! is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see . prerequisites: -------------- needs blocks.js and objects.js I. hierarchy ------------- the following tree lists all constructors hierarchically, indentation indicating inheritance. Refer to this list to get a contextual overview: Morph* AlignmentMorph DialogBoxMorph InputFieldMorph TriggerMorph* PushButtonMorph ToggleButtonMorph TabMorph ToggleMorph ToggleElementMorph * from Morphic.js II. toc ------- the following list shows the order in which all constructors are defined. Use this list to locate code in this document: PushButtonMorph ToggleButtonMorph TabMorph ToggleMorph ToggleElementMorph DialogBoxMorph AlignmentMorph InputFieldMorph */ // Global settings ///////////////////////////////////////////////////// /*global TriggerMorph, modules, Color, Point, BoxMorph, radians, newCanvas, StringMorph, Morph, TextMorph, nop, detect, StringFieldMorph, HTMLCanvasElement, fontHeight, SymbolMorph, localize, SpeechBubbleMorph, ArrowMorph, MenuMorph, isString, isNil, SliderMorph, MorphicPreferences, ScrollFrameMorph*/ modules.widgets = '2015-July-27'; var PushButtonMorph; var ToggleButtonMorph; var TabMorph; var ToggleMorph; var ToggleElementMorph; var DialogBoxMorph; var AlignmentMorph; var InputFieldMorph; // PushButtonMorph ///////////////////////////////////////////////////// // I am a Button with rounded corners and 3D-ish graphical effects // PushButtonMorph inherits from TriggerMorph: PushButtonMorph.prototype = new TriggerMorph(); PushButtonMorph.prototype.constructor = PushButtonMorph; PushButtonMorph.uber = TriggerMorph.prototype; // PushButtonMorph preferences settings: PushButtonMorph.prototype.fontSize = 10; PushButtonMorph.prototype.fontStyle = 'sans-serif'; PushButtonMorph.prototype.labelColor = new Color(0, 0, 0); PushButtonMorph.prototype.labelShadowColor = new Color(255, 255, 255); PushButtonMorph.prototype.labelShadowOffset = new Point(1, 1); PushButtonMorph.prototype.color = new Color(220, 220, 220); PushButtonMorph.prototype.pressColor = new Color(115, 180, 240); PushButtonMorph.prototype.highlightColor = PushButtonMorph.prototype.pressColor.lighter(50); PushButtonMorph.prototype.outlineColor = new Color(30, 30, 30); PushButtonMorph.prototype.outlineGradient = false; PushButtonMorph.prototype.contrast = 60; PushButtonMorph.prototype.edge = 2; PushButtonMorph.prototype.corner = 5; PushButtonMorph.prototype.outline = 1.00001; PushButtonMorph.prototype.padding = 3; // PushButtonMorph instance creation: function PushButtonMorph( target, action, labelString, environment, hint, template ) { this.init( target, action, labelString, environment, hint, template ); } PushButtonMorph.prototype.init = function ( target, action, labelString, environment, hint, template ) { // additional properties: this.is3D = false; // for "flat" design exceptions this.target = target || null; this.action = action || null; this.environment = environment || null; this.labelString = labelString || null; this.label = null; this.labelMinExtent = new Point(0, 0); this.hint = hint || null; this.template = template || null; // for pre-computed backbrounds // if a template is specified, its background images are used as cache // initialize inherited properties: TriggerMorph.uber.init.call(this); // override inherited properites: this.color = PushButtonMorph.prototype.color; this.drawNew(); this.fixLayout(); }; // PushButtonMorph layout: PushButtonMorph.prototype.fixLayout = function () { // make sure I at least encompass my label if (this.label !== null) { var padding = this.padding * 2 + this.outline * 2 + this.edge * 2; this.setExtent(new Point( Math.max(this.label.width(), this.labelMinExtent.x) + padding, Math.max(this.label instanceof StringMorph ? this.label.rawHeight() : this.label.height(), this.labelMinExtent.y) + padding )); this.label.setCenter(this.center()); } }; // PushButtonMorph events PushButtonMorph.prototype.mouseDownLeft = function () { PushButtonMorph.uber.mouseDownLeft.call(this); if (this.label) { this.label.setCenter(this.center().add(1)); } }; PushButtonMorph.prototype.mouseClickLeft = function () { PushButtonMorph.uber.mouseClickLeft.call(this); if (this.label) { this.label.setCenter(this.center()); } }; PushButtonMorph.prototype.mouseLeave = function () { PushButtonMorph.uber.mouseLeave.call(this); if (this.label) { this.label.setCenter(this.center()); } }; // PushButtonMorph drawing: PushButtonMorph.prototype.outlinePath = BoxMorph.prototype.outlinePath; PushButtonMorph.prototype.drawOutline = function (context) { var outlineStyle, isFlat = MorphicPreferences.isFlat && !this.is3D; if (!this.outline || isFlat) {return null; } if (this.outlineGradient) { outlineStyle = context.createLinearGradient( 0, 0, 0, this.height() ); outlineStyle.addColorStop(1, 'white'); outlineStyle.addColorStop(0, this.outlineColor.darker().toString()); } else { outlineStyle = this.outlineColor.toString(); } context.fillStyle = outlineStyle; context.beginPath(); this.outlinePath( context, isFlat ? 0 : this.corner, 0 ); context.closePath(); context.fill(); }; PushButtonMorph.prototype.drawBackground = function (context, color) { var isFlat = MorphicPreferences.isFlat && !this.is3D; context.fillStyle = color.toString(); context.beginPath(); this.outlinePath( context, isFlat ? 0 : Math.max(this.corner - this.outline, 0), this.outline ); context.closePath(); context.fill(); context.lineWidth = this.outline; }; PushButtonMorph.prototype.drawEdges = function ( context, color, topColor, bottomColor ) { if (MorphicPreferences.isFlat && !this.is3D) {return; } var minInset = Math.max(this.corner, this.outline + this.edge), w = this.width(), h = this.height(), gradient; // top: gradient = context.createLinearGradient( 0, this.outline, 0, this.outline + this.edge ); gradient.addColorStop(0, topColor.toString()); gradient.addColorStop(1, color.toString()); context.strokeStyle = gradient; context.lineCap = 'round'; context.lineWidth = this.edge; context.beginPath(); context.moveTo(minInset, this.outline + this.edge / 2); context.lineTo(w - minInset, this.outline + this.edge / 2); context.stroke(); // top-left corner: gradient = context.createRadialGradient( this.corner, this.corner, Math.max(this.corner - this.outline - this.edge, 0), this.corner, this.corner, Math.max(this.corner - this.outline, 0) ); gradient.addColorStop(1, topColor.toString()); gradient.addColorStop(0, color.toString()); context.strokeStyle = gradient; context.lineCap = 'round'; context.lineWidth = this.edge; context.beginPath(); context.arc( this.corner, this.corner, Math.max(this.corner - this.outline - this.edge / 2, 0), radians(180), radians(270), false ); context.stroke(); // left: gradient = context.createLinearGradient( this.outline, 0, this.outline + this.edge, 0 ); gradient.addColorStop(0, topColor.toString()); gradient.addColorStop(1, color.toString()); context.strokeStyle = gradient; context.lineCap = 'round'; context.lineWidth = this.edge; context.beginPath(); context.moveTo(this.outline + this.edge / 2, minInset); context.lineTo(this.outline + this.edge / 2, h - minInset); context.stroke(); // bottom: gradient = context.createLinearGradient( 0, h - this.outline, 0, h - this.outline - this.edge ); gradient.addColorStop(0, bottomColor.toString()); gradient.addColorStop(1, color.toString()); context.strokeStyle = gradient; context.lineCap = 'round'; context.lineWidth = this.edge; context.beginPath(); context.moveTo(minInset, h - this.outline - this.edge / 2); context.lineTo(w - minInset, h - this.outline - this.edge / 2); context.stroke(); // bottom-right corner: gradient = context.createRadialGradient( w - this.corner, h - this.corner, Math.max(this.corner - this.outline - this.edge, 0), w - this.corner, h - this.corner, Math.max(this.corner - this.outline, 0) ); gradient.addColorStop(1, bottomColor.toString()); gradient.addColorStop(0, color.toString()); context.strokeStyle = gradient; context.lineCap = 'round'; context.lineWidth = this.edge; context.beginPath(); context.arc( w - this.corner, h - this.corner, Math.max(this.corner - this.outline - this.edge / 2, 0), radians(0), radians(90), false ); context.stroke(); // right: gradient = context.createLinearGradient( w - this.outline, 0, w - this.outline - this.edge, 0 ); gradient.addColorStop(0, bottomColor.toString()); gradient.addColorStop(1, color.toString()); context.strokeStyle = gradient; context.lineCap = 'round'; context.lineWidth = this.edge; context.beginPath(); context.moveTo(w - this.outline - this.edge / 2, minInset); context.lineTo(w - this.outline - this.edge / 2, h - minInset); context.stroke(); }; PushButtonMorph.prototype.createBackgrounds = function () { var context, ext = this.extent(); if (this.template) { // take the backgrounds images from the template this.image = this.template.image; this.normalImage = this.template.normalImage; this.highlightImage = this.template.highlightImage; this.pressImage = this.template.pressImage; return null; } this.normalImage = newCanvas(ext); context = this.normalImage.getContext('2d'); this.drawOutline(context); this.drawBackground(context, this.color); this.drawEdges( context, this.color, this.color.lighter(this.contrast), this.color.darker(this.contrast) ); this.highlightImage = newCanvas(ext); context = this.highlightImage.getContext('2d'); this.drawOutline(context); this.drawBackground(context, this.highlightColor); this.drawEdges( context, this.highlightColor, this.highlightColor.lighter(this.contrast), this.highlightColor.darker(this.contrast) ); this.pressImage = newCanvas(ext); context = this.pressImage.getContext('2d'); this.drawOutline(context); this.drawBackground(context, this.pressColor); this.drawEdges( context, this.pressColor, this.pressColor.darker(this.contrast), this.pressColor.lighter(this.contrast) ); this.image = this.normalImage; }; PushButtonMorph.prototype.createLabel = function () { var shading = !MorphicPreferences.isFlat || this.is3D; if (this.label !== null) { this.label.destroy(); } if (this.labelString instanceof SymbolMorph) { this.label = this.labelString.fullCopy(); if (shading) { this.label.shadowOffset = this.labelShadowOffset; this.label.shadowColor = this.labelShadowColor; } this.label.color = this.labelColor; this.label.drawNew(); } else { this.label = new StringMorph( localize(this.labelString), this.fontSize, this.fontStyle, true, false, false, shading ? this.labelShadowOffset : null, this.labelShadowColor, this.labelColor ); } this.add(this.label); }; // ToggleButtonMorph /////////////////////////////////////////////////////// /* I am a two-state PushButton. When my state is "true" I keep my "pressed" background color. I can also be set to not auto-layout my bounds, in which case my label will left-align. */ // ToggleButtonMorph inherits from PushButtonMorph: ToggleButtonMorph.prototype = new PushButtonMorph(); ToggleButtonMorph.prototype.constructor = ToggleButtonMorph; ToggleButtonMorph.uber = PushButtonMorph.prototype; // ToggleButton settings ToggleButtonMorph.prototype.contrast = 30; // ToggleButtonMorph instance creation: function ToggleButtonMorph( colors, // color overrides, : [normal, highlight, pressed] target, action, // a toggle function labelString, query, // predicate/selector environment, hint, template, // optional, for cached background images minWidth, // optional, if specified label will left-align hasPreview, // show press color on left edge (e.g. category) isPicture // treat label as picture, i.e. don't apply typography ) { this.init( colors, target, action, labelString, query, environment, hint, template, minWidth, hasPreview, isPicture ); } ToggleButtonMorph.prototype.init = function ( colors, target, action, labelString, query, environment, hint, template, minWidth, hasPreview, isPicture ) { // additional properties: this.state = false; this.query = query || function () {return true; }; this.minWidth = minWidth || null; this.hasPreview = hasPreview || false; this.isPicture = isPicture || false; this.trueStateLabel = null; // initialize inherited properties: ToggleButtonMorph.uber.init.call( this, target, action, labelString, environment, hint, template ); // override default colors if others are specified if (colors) { this.color = colors[0]; this.highlightColor = colors[1]; this.pressColor = colors[2]; } this.refresh(); this.drawNew(); }; // ToggleButtonMorph events ToggleButtonMorph.prototype.mouseEnter = function () { var contents = this.hint instanceof Function ? this.hint() : this.hint; if (!this.state) { this.image = this.highlightImage; this.changed(); } if (contents) { this.bubbleHelp(contents); } }; ToggleButtonMorph.prototype.mouseLeave = function () { if (!this.state) { this.image = this.normalImage; this.changed(); } if (this.hint) { this.world().hand.destroyTemporaries(); } }; ToggleButtonMorph.prototype.mouseDownLeft = function () { if (!this.state) { this.image = this.pressImage; this.changed(); } }; ToggleButtonMorph.prototype.mouseClickLeft = function () { if (!this.state) { this.image = this.highlightImage; this.changed(); } this.trigger(); // allow me to be triggered again to force-update others }; // ToggleButtonMorph action ToggleButtonMorph.prototype.trigger = function () { ToggleButtonMorph.uber.trigger.call(this); this.refresh(); }; ToggleButtonMorph.prototype.refresh = function () { /* if query is a function: execute the query with target as environment (can be null) for lambdafied (inline) actions else if query is a String: treat it as function property of target and execute it for selector-like queries */ if (typeof this.query === 'function') { this.state = this.query.call(this.target); } else { // assume it's a String this.state = this.target[this.query](); } if (this.state) { this.image = this.pressImage; if (this.trueStateLabel) { this.label.hide(); this.trueStateLabel.show(); } } else { this.image = this.normalImage; if (this.trueStateLabel) { this.label.show(); this.trueStateLabel.hide(); } } this.changed(); }; // ToggleButtonMorph layout: ToggleButtonMorph.prototype.fixLayout = function () { if (this.label !== null) { var lw = Math.max(this.label.width(), this.labelMinExtent.x), padding = this.padding * 2 + this.outline * 2 + this.edge * 2; this.setExtent(new Point( (this.minWidth ? Math.max(this.minWidth, lw) + padding : lw + padding), Math.max(this.label instanceof StringMorph ? this.label.rawHeight() : this.label.height(), this.labelMinExtent.y) + padding )); this.label.setCenter(this.center()); if (this.trueStateLabel) { this.trueStateLabel.setCenter(this.center()); } if (this.minWidth) { // left-align along my corner this.label.setLeft( this.left() + this.outline + this.edge + this.corner + this.padding ); } } }; // ToggleButtonMorph drawing ToggleButtonMorph.prototype.createBackgrounds = function () { /* basically the same as inherited from PushButtonMorph, except for not inverting the pressImage 3D-ish border (because it stays that way), and optionally coloring the left edge in the press-color, previewing the selection color (e.g. in the case of Scratch palette-category selector. the latter is done in the drawEdges() method. */ var context, ext = this.extent(); if (this.template) { // take the backgrounds images from the template this.image = this.template.image; this.normalImage = this.template.normalImage; this.highlightImage = this.template.highlightImage; this.pressImage = this.template.pressImage; return null; } this.normalImage = newCanvas(ext); context = this.normalImage.getContext('2d'); this.drawOutline(context); this.drawBackground(context, this.color); this.drawEdges( context, this.color, this.color.lighter(this.contrast), this.color.darker(this.contrast) ); this.highlightImage = newCanvas(ext); context = this.highlightImage.getContext('2d'); this.drawOutline(context); this.drawBackground(context, this.highlightColor); this.drawEdges( context, this.highlightColor, this.highlightColor.lighter(this.contrast), this.highlightColor.darker(this.contrast) ); // note: don't invert the 3D-ish edges for pressedImage, because // it will stay that way, and should not look inverted (or should it?) this.pressImage = newCanvas(ext); context = this.pressImage.getContext('2d'); this.drawOutline(context); this.drawBackground(context, this.pressColor); this.drawEdges( context, this.pressColor, this.pressColor.lighter(40), this.pressColor.darker(40) ); this.image = this.normalImage; }; ToggleButtonMorph.prototype.drawEdges = function ( context, color, topColor, bottomColor ) { var gradient; ToggleButtonMorph.uber.drawEdges.call( this, context, color, topColor, bottomColor ); if (this.hasPreview) { // indicate the possible selection color if (MorphicPreferences.isFlat && !this.is3D) { context.fillStyle = this.pressColor.toString(); context.fillRect( this.outline, this.outline, this.corner, this.height() - this.outline * 2 ); return; } gradient = context.createLinearGradient( 0, 0, this.corner, 0 ); gradient.addColorStop(0, this.pressColor.lighter(40).toString()); gradient.addColorStop(1, this.pressColor.darker(40).toString()); context.fillStyle = gradient; // this.pressColor.toString(); context.beginPath(); this.previewPath( context, Math.max(this.corner - this.outline, 0), this.outline ); context.closePath(); context.fill(); } }; ToggleButtonMorph.prototype.previewPath = function (context, radius, inset) { var offset = radius + inset, h = this.height(); // top left: context.arc( offset, offset, radius, radians(-180), radians(-90), false ); // bottom left: context.arc( offset, h - offset, radius, radians(90), radians(180), false ); }; ToggleButtonMorph.prototype.createLabel = function () { var shading = !MorphicPreferences.isFlat || this.is3D, none = new Point(); if (this.label !== null) { this.label.destroy(); } if (this.trueStateLabel !== null) { this.trueStateLabel.destroy(); } if (this.labelString instanceof Array && this.labelString.length === 2) { if (this.labelString[0] instanceof SymbolMorph) { this.label = this.labelString[0].fullCopy(); this.trueStateLabel = this.labelString[1].fullCopy(); if (!this.isPicture) { this.label.shadowOffset = shading ? this.labelShadowOffset : none; this.label.shadowColor = this.labelShadowColor; this.label.color = this.labelColor; this.label.drawNew(); this.trueStateLabel.shadowOffset = shading ? this.labelShadowOffset : none; this.trueStateLabel.shadowColor = this.labelShadowColor; this.trueStateLabel.color = this.labelColor; this.trueStateLabel.drawNew(); } } else if (this.labelString[0] instanceof Morph) { this.label = this.labelString[0].fullCopy(); this.trueStateLabel = this.labelString[1].fullCopy(); } else { this.label = new StringMorph( localize(this.labelString[0]), this.fontSize, this.fontStyle, true, false, false, shading ? this.labelShadowOffset : null, this.labelShadowColor, this.labelColor ); this.trueStateLabel = new StringMorph( localize(this.labelString[1]), this.fontSize, this.fontStyle, true, false, false, shading ? this.labelShadowOffset : null, this.labelShadowColor, this.labelColor ); } } else { if (this.labelString instanceof SymbolMorph) { this.label = this.labelString.fullCopy(); if (!this.isPicture) { this.label.shadowOffset = shading ? this.labelShadowOffset : none; this.label.shadowColor = this.labelShadowColor; this.label.color = this.labelColor; this.label.drawNew(); } } else if (this.labelString instanceof Morph) { this.label = this.labelString.fullCopy(); } else { this.label = new StringMorph( localize(this.labelString), this.fontSize, this.fontStyle, true, false, false, shading ? this.labelShadowOffset : none, this.labelShadowColor, this.labelColor ); } } this.add(this.label); if (this.trueStateLabel) { this.add(this.trueStateLabel); } }; // ToggleButtonMorph hiding and showing: /* override the inherited behavior to recursively hide/show all children, so that my instances get restored correctly when hiding/showing my parent. */ ToggleButtonMorph.prototype.hide = function () { this.isVisible = false; this.changed(); }; ToggleButtonMorph.prototype.show = function () { this.isVisible = true; this.changed(); }; // TabMorph /////////////////////////////////////////////////////// // TabMorph inherits from ToggleButtonMorph: TabMorph.prototype = new ToggleButtonMorph(); TabMorph.prototype.constructor = TabMorph; TabMorph.uber = ToggleButtonMorph.prototype; // TabMorph instance creation: function TabMorph( colors, // color overrides, : [normal, highlight, pressed] target, action, // a toggle function labelString, query, // predicate/selector environment, hint ) { this.init( colors, target, action, labelString, query, environment, hint ); } // TabMorph layout: TabMorph.prototype.fixLayout = function () { if (this.label !== null) { this.setExtent(new Point( this.label.width() + this.padding * 2 + this.corner * 3 + this.edge * 2, (this.label instanceof StringMorph ? this.label.rawHeight() : this.label.height()) + this.padding * 2 + this.edge )); this.label.setCenter(this.center()); } }; // TabMorph action: TabMorph.prototype.refresh = function () { if (this.state) { // bring to front if (this.parent) { this.parent.add(this); } } TabMorph.uber.refresh.call(this); }; // TabMorph drawing: TabMorph.prototype.drawBackground = function (context, color) { var w = this.width(), h = this.height(), c = this.corner; context.fillStyle = color.toString(); context.beginPath(); context.moveTo(0, h); context.bezierCurveTo(c, h, c, 0, c * 2, 0); context.lineTo(w - c * 2, 0); context.bezierCurveTo(w - c, 0, w - c, h, w, h); context.closePath(); context.fill(); }; TabMorph.prototype.drawOutline = function () { nop(); }; TabMorph.prototype.drawEdges = function ( context, color, topColor, bottomColor ) { if (MorphicPreferences.isFlat && !this.is3D) {return; } var w = this.width(), h = this.height(), c = this.corner, e = this.edge, eh = e / 2, gradient; nop(color); // argument not needed here gradient = context.createLinearGradient(0, 0, w, 0); gradient.addColorStop(0, topColor.toString()); gradient.addColorStop(1, bottomColor.toString()); context.strokeStyle = gradient; context.lineCap = 'round'; context.lineWidth = e; context.beginPath(); context.moveTo(0, h + eh); context.bezierCurveTo(c, h, c, 0, c * 2, eh); context.lineTo(w - c * 2, eh); context.bezierCurveTo(w - c, 0, w - c, h, w, h + eh); context.stroke(); }; // ToggleMorph /////////////////////////////////////////////////////// /* I am a PushButton which toggles a check mark ( becoming check box) or a bullet (becoming a radio button). I can have both or either an additional label and an additional pictogram, whereas the pictogram can be either an instance of (any) Morph, in which case the pictogram will be an interactive toggle itself or a Canvas, in which case it is just going to be a picture. */ // ToggleMorph inherits from PushButtonMorph: ToggleMorph.prototype = new PushButtonMorph(); ToggleMorph.prototype.constructor = ToggleMorph; ToggleMorph.uber = PushButtonMorph.prototype; // ToggleMorph instance creation: function ToggleMorph( style, // 'checkbox' or 'radiobutton' target, action, // a toggle function labelString, query, // predicate/selector environment, hint, template, element, // optional Morph or Canvas to display builder // method which constructs the element (only for Morphs) ) { this.init( style, target, action, labelString, query, environment, hint, template, element, builder ); } ToggleMorph.prototype.init = function ( style, target, action, labelString, query, environment, hint, template, element, builder ) { // additional properties: this.padding = 1; style = style || 'checkbox'; this.corner = (style === 'checkbox' ? 0 : fontHeight(this.fontSize) / 2 + this.outline + this.padding); this.state = false; this.query = query || function () {return true; }; this.tick = null; this.captionString = labelString || null; this.labelAlignment = 'right'; this.element = element || null; this.builder = builder || null; this.toggleElement = null; // initialize inherited properties: ToggleMorph.uber.init.call( this, target, action, (style === 'checkbox' ? '\u2713' : '\u25CF'), environment, hint, template ); this.refresh(); this.drawNew(); }; // ToggleMorph layout: ToggleMorph.prototype.fixLayout = function () { var padding = this.padding * 2 + this.outline * 2, y; if (this.tick !== null) { this.silentSetHeight(this.tick.rawHeight() + padding); this.silentSetWidth(this.tick.width() + padding); this.setExtent(new Point( Math.max(this.width(), this.height()), Math.max(this.width(), this.height()) )); this.tick.setCenter(this.center()); } if (this.state) { this.tick.show(); } else { this.tick.hide(); } if (this.toggleElement && (this.labelAlignment === 'right')) { y = this.top() + (this.height() - this.toggleElement.height()) / 2; this.toggleElement.setPosition(new Point( this.right() + padding, y )); } if (this.label !== null) { y = this.top() + (this.height() - this.label.height()) / 2; if (this.labelAlignment === 'right') { this.label.setPosition(new Point( this.toggleElement ? this.toggleElement instanceof ToggleElementMorph ? this.toggleElement.right() : this.toggleElement.right() + padding : this.right() + padding, y )); } else { this.label.setPosition(new Point( this.left() - this.label.width() - padding, y )); } } }; ToggleMorph.prototype.createLabel = function () { var shading = !MorphicPreferences.isFlat || this.is3D; if (this.label === null) { if (this.captionString) { this.label = new TextMorph( localize(this.captionString), this.fontSize, this.fontStyle, true ); this.add(this.label); } } if (this.tick === null) { this.tick = new StringMorph( localize(this.labelString), this.fontSize, this.fontStyle, true, false, false, shading ? new Point(1, 1) : null, new Color(240, 240, 240) ); this.add(this.tick); } if (this.toggleElement === null) { if (this.element) { if (this.element instanceof Morph) { this.toggleElement = new ToggleElementMorph( this.target, this.action, this.element, this.query, this.environment, this.hint, this.builder ); } else if (this.element instanceof HTMLCanvasElement) { this.toggleElement = new Morph(); this.toggleElement.silentSetExtent(new Point( this.element.width, this.element.height )); this.toggleElement.image = this.element; } this.add(this.toggleElement); } } }; // ToggleMorph action: ToggleMorph.prototype.trigger = function () { ToggleMorph.uber.trigger.call(this); this.refresh(); }; ToggleMorph.prototype.refresh = function () { /* if query is a function: execute the query with target as environment (can be null) for lambdafied (inline) actions else if query is a String: treat it as function property of target and execute it for selector-like queries */ if (typeof this.query === 'function') { this.state = this.query.call(this.target); } else { // assume it's a String this.state = this.target[this.query](); } if (this.state) { this.tick.show(); } else { this.tick.hide(); } if (this.toggleElement && this.toggleElement.refresh) { this.toggleElement.refresh(); } }; // ToggleMorph events ToggleMorph.prototype.mouseDownLeft = function () { PushButtonMorph.uber.mouseDownLeft.call(this); if (this.tick) { this.tick.setCenter(this.center().add(1)); } }; ToggleMorph.prototype.mouseClickLeft = function () { PushButtonMorph.uber.mouseClickLeft.call(this); if (this.tick) { this.tick.setCenter(this.center()); } }; ToggleMorph.prototype.mouseLeave = function () { PushButtonMorph.uber.mouseLeave.call(this); if (this.tick) { this.tick.setCenter(this.center()); } }; // ToggleMorph hiding and showing: /* override the inherited behavior to recursively hide/show all children, so that my instances get restored correctly when hiding/showing my parent. */ ToggleMorph.prototype.hide = ToggleButtonMorph.prototype.hide; ToggleMorph.prototype.show = ToggleButtonMorph.prototype.show; // ToggleElementMorph ///////////////////////////////////////////////////// /* I am a picture of a Morph ("element") which acts as a toggle button. I am different from ToggleButton in that I neither create a label nor draw button outlines. Instead I display my element morph in specified contrasts of a given color, symbolizing whether it is selected or not */ // ToggleElementMorph inherits from TriggerMorph: ToggleElementMorph.prototype = new TriggerMorph(); ToggleElementMorph.prototype.constructor = ToggleElementMorph; ToggleElementMorph.uber = TriggerMorph.prototype; // ToggleElementMorph preferences settings ToggleElementMorph.prototype.contrast = 50; ToggleElementMorph.prototype.shadowOffset = new Point(2, 2); ToggleElementMorph.prototype.shadowAlpha = 0.6; ToggleElementMorph.prototype.fontSize = 10; // only for (optional) labels ToggleElementMorph.prototype.inactiveColor = new Color(180, 180, 180); // ToggleElementMorph instance creation: function ToggleElementMorph( target, action, element, query, environment, hint, builder, labelString ) { this.init( target, action, element, query, environment, hint, builder, labelString ); } ToggleElementMorph.prototype.init = function ( target, action, element, // mandatory query, environment, hint, builder, // optional function name that rebuilds the element labelString ) { // additional properties: this.target = target || null; this.action = action || null; this.element = element; this.query = query || function () {return true; }; this.environment = environment || null; this.hint = hint || null; this.builder = builder || 'nop'; this.captionString = labelString || null; this.labelAlignment = 'right'; this.state = false; // initialize inherited properties: TriggerMorph.uber.init.call(this); // override inherited properties: this.color = element.color; this.createLabel(); }; // ToggleElementMorph drawing: ToggleElementMorph.prototype.createBackgrounds = function () { var shading = !MorphicPreferences.isFlat || this.is3D; this.color = this.element.color; this.element.removeShadow(); this.element[this.builder](); if (shading) { this.element.addShadow(this.shadowOffset, this.shadowAlpha); } this.silentSetExtent(this.element.fullBounds().extent()); // w/ shadow this.pressImage = this.element.fullImage(); this.element.removeShadow(); this.element.setColor(this.inactiveColor); this.element[this.builder](this.contrast); if (shading) { this.element.addShadow(this.shadowOffset, 0); } this.normalImage = this.element.fullImage(); this.element.removeShadow(); this.element.setColor(this.color.lighter(this.contrast)); this.element[this.builder](this.contrast); if (shading) { this.element.addShadow(this.shadowOffset, this.shadowAlpha); } this.highlightImage = this.element.fullImage(); this.element.removeShadow(); this.element.setColor(this.color); this.element[this.builder](); this.image = this.normalImage; }; ToggleElementMorph.prototype.setColor = function (aColor) { this.element.setColor(aColor); this.createBackgrounds(); this.refresh(); }; // ToggleElementMorph layout: ToggleElementMorph.prototype.createLabel = function () { var y; if (this.captionString) { this.label = new StringMorph( this.captionString, this.fontSize, this.fontStyle, true ); this.add(this.label); y = this.top() + (this.height() - this.label.height()) / 2; if (this.labelAlignment === 'right') { this.label.setPosition(new Point( this.right(), y )); } else { this.label.setPosition(new Point( this.left() - this.label.width(), y )); } } }; // ToggleElementMorph action ToggleElementMorph.prototype.trigger = ToggleButtonMorph.prototype.trigger; ToggleElementMorph.prototype.refresh = ToggleButtonMorph.prototype.refresh; // ToggleElementMorph events ToggleElementMorph.prototype.mouseEnter = ToggleButtonMorph.prototype.mouseEnter; ToggleElementMorph.prototype.mouseLeave = ToggleButtonMorph.prototype.mouseLeave; ToggleElementMorph.prototype.mouseDownLeft = ToggleButtonMorph.prototype.mouseDownLeft; ToggleElementMorph.prototype.mouseClickLeft = ToggleButtonMorph.prototype.mouseClickLeft; // DialogBoxMorph ///////////////////////////////////////////////////// /* I am a DialogBox frame. Note: ----- my key property keeps track of my purpose to prevent multiple instances on the same or similar objects */ // DialogBoxMorph inherits from Morph: DialogBoxMorph.prototype = new Morph(); DialogBoxMorph.prototype.constructor = DialogBoxMorph; DialogBoxMorph.uber = Morph.prototype; // DialogBoxMorph preferences settings: DialogBoxMorph.prototype.fontSize = 12; DialogBoxMorph.prototype.titleFontSize = 14; DialogBoxMorph.prototype.fontStyle = 'sans-serif'; DialogBoxMorph.prototype.color = PushButtonMorph.prototype.color; DialogBoxMorph.prototype.titleTextColor = new Color(255, 255, 255); DialogBoxMorph.prototype.titleBarColor = PushButtonMorph.prototype.pressColor; DialogBoxMorph.prototype.contrast = 40; DialogBoxMorph.prototype.corner = 12; DialogBoxMorph.prototype.padding = 14; DialogBoxMorph.prototype.titlePadding = 6; DialogBoxMorph.prototype.buttonContrast = 50; DialogBoxMorph.prototype.buttonFontSize = 12; DialogBoxMorph.prototype.buttonCorner = 12; DialogBoxMorph.prototype.buttonEdge = 6; DialogBoxMorph.prototype.buttonPadding = 0; DialogBoxMorph.prototype.buttonOutline = 3; DialogBoxMorph.prototype.buttonOutlineColor = PushButtonMorph.prototype.color; DialogBoxMorph.prototype.buttonOutlineGradient = true; DialogBoxMorph.prototype.instances = {}; // prevent multiple instances // DialogBoxMorph instance creation: function DialogBoxMorph(target, action, environment) { this.init(target, action, environment); } DialogBoxMorph.prototype.init = function (target, action, environment) { // additional properties: this.is3D = false; // for "flat" design exceptions this.target = target || null; this.action = action || null; this.environment = environment || null; this.key = null; // keep track of my purpose to prevent mulitple instances this.labelString = null; this.label = null; this.head = null; this.body = null; this.buttons = null; // initialize inherited properties: DialogBoxMorph.uber.init.call(this); // override inherited properites: this.isDraggable = true; this.color = PushButtonMorph.prototype.color; this.createLabel(); this.createButtons(); this.setExtent(new Point(300, 150)); }; // DialogBoxMorph ops DialogBoxMorph.prototype.inform = function ( title, textString, world, pic ) { var txt = new TextMorph( textString, this.fontSize, this.fontStyle, true, false, 'center', null, null, MorphicPreferences.isFlat ? null : new Point(1, 1), new Color(255, 255, 255) ); if (!this.key) { this.key = 'inform' + title + textString; } this.labelString = title; this.createLabel(); if (pic) {this.setPicture(pic); } if (textString) { this.addBody(txt); } this.addButton('ok', 'OK'); this.drawNew(); this.fixLayout(); this.popUp(world); }; DialogBoxMorph.prototype.askYesNo = function ( title, textString, world, pic ) { var txt = new TextMorph( textString, this.fontSize, this.fontStyle, true, false, 'center', null, null, MorphicPreferences.isFlat ? null : new Point(1, 1), new Color(255, 255, 255) ); if (!this.key) { this.key = 'decide' + title + textString; } this.labelString = title; this.createLabel(); if (pic) {this.setPicture(pic); } this.addBody(txt); this.addButton('ok', 'Yes'); this.addButton('cancel', 'No'); this.fixLayout(); this.drawNew(); this.fixLayout(); this.popUp(world); }; DialogBoxMorph.prototype.prompt = function ( title, defaultString, world, pic, choices, // optional dictionary for drop-down of choices isReadOnly, // optional when using choices isNumeric, // optional sliderMin, // optional for numeric sliders sliderMax, // optional for numeric sliders sliderAction // optional single-arg function for numeric slider ) { var sld, head, txt = new InputFieldMorph( defaultString, isNumeric || false, // numeric? choices || null, // drop-down dict, optional choices ? isReadOnly || false : false ); txt.setWidth(250); if (isNumeric) { if (pic) { head = new AlignmentMorph('column', this.padding); pic.setPosition(head.position()); head.add(pic); } if (!isNil(sliderMin) && !isNil(sliderMax)) { sld = new SliderMorph( sliderMin * 100, sliderMax * 100, parseFloat(defaultString) * 100, (sliderMax - sliderMin) / 10 * 100, 'horizontal' ); sld.alpha = 1; sld.color = this.color.lighter(50); sld.setHeight(txt.height() * 0.7); sld.setWidth(txt.width()); sld.action = function (num) { if (sliderAction) { sliderAction(num / 100); } txt.setContents(num / 100); txt.edit(); }; if (!head) { head = new AlignmentMorph('column', this.padding); } head.add(sld); } if (head) { head.fixLayout(); this.setPicture(head); head.fixLayout(); } } else { if (pic) {this.setPicture(pic); } } this.reactToChoice = function (inp) { if (sld) { sld.value = inp * 100; sld.drawNew(); sld.changed(); } if (sliderAction) { sliderAction(inp); } }; txt.reactToKeystroke = function () { var inp = txt.getValue(); if (sld) { inp = Math.max(inp, sliderMin); sld.value = inp * 100; sld.drawNew(); sld.changed(); } if (sliderAction) { sliderAction(inp); } }; this.labelString = title; this.createLabel(); if (!this.key) { this.key = 'prompt' + title + defaultString; } this.addBody(txt); txt.drawNew(); this.addButton('ok', 'OK'); this.addButton('cancel', 'Cancel'); this.fixLayout(); this.drawNew(); this.fixLayout(); this.popUp(world); }; DialogBoxMorph.prototype.promptCode = function ( title, defaultString, world, pic, instructions ) { var frame = new ScrollFrameMorph(), text = new TextMorph(defaultString || ''), bdy = new AlignmentMorph('column', this.padding), size = pic ? Math.max(pic.width, 400) : 400; this.getInput = function () { return text.text; }; function remarkText(string) { return new TextMorph( localize(string), 10, null, // style false, // bold null, // italic null, // alignment null, // width null, // font name MorphicPreferences.isFlat ? null : new Point(1, 1), new Color(255, 255, 255) // shadowColor ); } frame.padding = 6; frame.setWidth(size); frame.acceptsDrops = false; frame.contents.acceptsDrops = false; text.fontName = 'monospace'; text.fontStyle = 'monospace'; text.fontSize = 11; text.setPosition(frame.topLeft().add(frame.padding)); text.enableSelecting(); text.isEditable = true; frame.setHeight(size / 4); frame.fixLayout = nop; frame.edge = InputFieldMorph.prototype.edge; frame.fontSize = InputFieldMorph.prototype.fontSize; frame.typeInPadding = InputFieldMorph.prototype.typeInPadding; frame.contrast = InputFieldMorph.prototype.contrast; frame.drawNew = InputFieldMorph.prototype.drawNew; frame.drawRectBorder = InputFieldMorph.prototype.drawRectBorder; frame.addContents(text); text.drawNew(); if (pic) {this.setPicture(pic); } this.labelString = title; this.createLabel(); if (!this.key) { this.key = 'promptCode' + title + defaultString; } bdy.setColor(this.color); bdy.add(frame); if (instructions) { bdy.add(remarkText(instructions)); } bdy.fixLayout(); this.addBody(bdy); frame.drawNew(); bdy.drawNew(); this.addButton('ok', 'OK'); this.addButton('cancel', 'Cancel'); this.fixLayout(); this.drawNew(); this.fixLayout(); this.popUp(world); text.edit(); }; DialogBoxMorph.prototype.promptVector = function ( title, point, deflt, xLabel, yLabel, world, pic, msg ) { var vec = new AlignmentMorph('row', 4), xInp = new InputFieldMorph(point.x.toString(), true), yInp = new InputFieldMorph(point.y.toString(), true), xCol = new AlignmentMorph('column', 2), yCol = new AlignmentMorph('column', 2), inp = new AlignmentMorph('column', 2), bdy = new AlignmentMorph('column', this.padding); function labelText(string) { return new TextMorph( localize(string), 10, null, // style false, // bold null, // italic null, // alignment null, // width null, // font name MorphicPreferences.isFlat ? null : new Point(1, 1), new Color(255, 255, 255) // shadowColor ); } inp.alignment = 'left'; inp.setColor(this.color); bdy.setColor(this.color); xCol.alignment = 'left'; xCol.setColor(this.color); yCol.alignment = 'left'; yCol.setColor(this.color); xCol.add(labelText(xLabel)); xCol.add(xInp); yCol.add(labelText(yLabel)); yCol.add(yInp); vec.add(xCol); vec.add(yCol); inp.add(vec); if (msg) { bdy.add(labelText(msg)); } bdy.add(inp); vec.fixLayout(); xCol.fixLayout(); yCol.fixLayout(); inp.fixLayout(); bdy.fixLayout(); this.labelString = title; this.createLabel(); if (pic) {this.setPicture(pic); } this.addBody(bdy); vec.drawNew(); xCol.drawNew(); xInp.drawNew(); yCol.drawNew(); yInp.drawNew(); bdy.fixLayout(); this.addButton('ok', 'OK'); if (deflt instanceof Point) { this.addButton( function () { xInp.setContents(deflt.x.toString()); yInp.setContents(deflt.y.toString()); }, 'Default' ); } this.addButton('cancel', 'Cancel'); this.fixLayout(); this.drawNew(); this.fixLayout(); this.edit = function () { xInp.edit(); }; this.getInput = function () { return new Point(xInp.getValue(), yInp.getValue()); }; if (!this.key) { this.key = 'vector' + title; } this.popUp(world); }; DialogBoxMorph.prototype.promptCredentials = function ( title, purpose, tosURL, tosLabel, prvURL, prvLabel, checkBoxLabel, world, pic, msg ) { var usr = new InputFieldMorph(), bmn, byr, emlLabel, eml = new InputFieldMorph(), pw1 = new InputFieldMorph(), pw2 = new InputFieldMorph(), opw = new InputFieldMorph(), agree = false, chk, dof = new AlignmentMorph('row', 4), mCol = new AlignmentMorph('column', 2), yCol = new AlignmentMorph('column', 2), inp = new AlignmentMorph('column', 2), lnk = new AlignmentMorph('row', 4), bdy = new AlignmentMorph('column', this.padding), years = {}, currentYear = new Date().getFullYear(), firstYear = currentYear - 20, myself = this; function labelText(string) { return new TextMorph( localize(string), 10, null, // style false, // bold null, // italic null, // alignment null, // width null, // font name MorphicPreferences.isFlat ? null : new Point(1, 1), new Color(255, 255, 255) // shadowColor ); } function linkButton(label, url) { var btn = new PushButtonMorph( myself, function () { window.open(url); }, ' ' + localize(label) + ' ' ); btn.fontSize = 10; btn.corner = myself.buttonCorner; btn.edge = myself.buttonEdge; btn.outline = myself.buttonOutline; btn.outlineColor = myself.buttonOutlineColor; btn.outlineGradient = myself.buttonOutlineGradient; btn.padding = myself.buttonPadding; btn.contrast = myself.buttonContrast; btn.drawNew(); btn.fixLayout(); return btn; } function age() { var today = new Date().getFullYear() + new Date().getMonth() / 12, year = +byr.getValue() || 0, monthName = bmn.getValue(), month, birthday; if (monthName instanceof Array) { // translatable monthName = monthName[0]; } if (isNaN(year)) { year = 0; } month = [ 'January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December' ].indexOf(monthName); if (isNaN(month)) { month = 0; } birthday = year + month / 12; return today - birthday; } bmn = new InputFieldMorph( null, // text false, // numeric? { 'January' : ['January'], 'February' : ['February'], 'March' : ['March'], 'April' : ['April'], 'May' : ['May'], 'June' : ['June'], 'July' : ['July'], 'August' : ['August'], 'September' : ['September'], 'October' : ['October'], 'November' : ['November'], 'December' : ['December'] }, true // read-only ); for (currentYear; currentYear > firstYear; currentYear -= 1) { years[currentYear.toString() + ' '] = currentYear; } years[firstYear + ' ' + localize('or before')] = '< ' + currentYear; byr = new InputFieldMorph( null, // text false, // numeric? years, true // read-only ); inp.alignment = 'left'; inp.setColor(this.color); bdy.setColor(this.color); mCol.alignment = 'left'; mCol.setColor(this.color); yCol.alignment = 'left'; yCol.setColor(this.color); usr.setWidth(200); bmn.setWidth(100); byr.contents().minWidth = 80; byr.setWidth(80); eml.setWidth(200); pw1.setWidth(200); pw2.setWidth(200); opw.setWidth(200); pw1.contents().text.toggleIsPassword(); pw2.contents().text.toggleIsPassword(); opw.contents().text.toggleIsPassword(); if (purpose === 'login') { inp.add(labelText('User name:')); inp.add(usr); } if (purpose === 'signup') { inp.add(labelText('User name:')); inp.add(usr); mCol.add(labelText('Birth date:')); mCol.add(bmn); yCol.add(labelText('year:')); yCol.add(byr); dof.add(mCol); dof.add(yCol); inp.add(dof); emlLabel = labelText('foo'); inp.add(emlLabel); inp.add(eml); } if (purpose === 'login') { inp.add(labelText('Password:')); inp.add(pw1); } if (purpose === 'changePassword') { inp.add(labelText('Old password:')); inp.add(opw); inp.add(labelText('New password:')); inp.add(pw1); inp.add(labelText('Repeat new password:')); inp.add(pw2); } if (purpose === 'resetPassword') { inp.add(labelText('User name:')); inp.add(usr); } if (msg) { bdy.add(labelText(msg)); } bdy.add(inp); if (tosURL || prvURL) { bdy.add(lnk); } if (tosURL) { lnk.add(linkButton(tosLabel, tosURL)); } if (prvURL) { lnk.add(linkButton(prvLabel, prvURL)); } if (checkBoxLabel) { chk = new ToggleMorph( 'checkbox', this, function () {agree = !agree; }, // action, checkBoxLabel, function () {return agree; } //query ); chk.edge = this.buttonEdge / 2; chk.outline = this.buttonOutline / 2; chk.outlineColor = this.buttonOutlineColor; chk.outlineGradient = this.buttonOutlineGradient; chk.contrast = this.buttonContrast; chk.drawNew(); chk.fixLayout(); bdy.add(chk); } dof.fixLayout(); mCol.fixLayout(); yCol.fixLayout(); inp.fixLayout(); lnk.fixLayout(); bdy.fixLayout(); this.labelString = title; this.createLabel(); if (pic) {this.setPicture(pic); } this.addBody(bdy); usr.drawNew(); dof.drawNew(); mCol.drawNew(); bmn.drawNew(); yCol.drawNew(); byr.drawNew(); pw1.drawNew(); pw2.drawNew(); opw.drawNew(); eml.drawNew(); bdy.fixLayout(); this.addButton('ok', 'OK'); this.addButton('cancel', 'Cancel'); this.fixLayout(); this.drawNew(); this.fixLayout(); function validInputs() { var checklist, empty, em = eml.getValue(); function indicate(morph, string) { var bubble = new SpeechBubbleMorph(localize(string)); bubble.isPointingRight = false; bubble.drawNew(); bubble.popUp( world, morph.leftCenter().subtract(new Point(bubble.width() + 2, 0)) ); if (morph.edit) { morph.edit(); } } if (purpose === 'login') { checklist = [usr, pw1]; } else if (purpose === 'signup') { checklist = [usr, bmn, byr, eml]; } else if (purpose === 'changePassword') { checklist = [opw, pw1, pw2]; } else if (purpose === 'resetPassword') { checklist = [usr]; } empty = detect( checklist, function (inp) { return !inp.getValue(); } ); if (empty) { indicate(empty, 'please fill out\nthis field'); return false; } if (purpose === 'signup') { if (usr.getValue().length < 4) { indicate(usr, 'User name must be four\ncharacters or longer'); return false; } if (em.indexOf(' ') > -1 || em.indexOf('@') === -1 || em.indexOf('.') === -1) { indicate(eml, 'please provide a valid\nemail address'); return false; } } if (purpose === 'changePassword') { if (pw1.getValue().length < 6) { indicate(pw1, 'password must be six\ncharacters or longer'); return false; } if (pw1.getValue() !== pw2.getValue()) { indicate(pw2, 'passwords do\nnot match'); return false; } } if (purpose === 'signup') { if (!agree) { indicate(chk, 'please agree to\nthe TOS'); return false; } } return true; } this.accept = function () { if (validInputs()) { DialogBoxMorph.prototype.accept.call(myself); } }; this.edit = function () { if (purpose === 'changePassword') { opw.edit(); } else { // 'signup', 'login', 'resetPassword' usr.edit(); } }; this.getInput = function () { return { username: usr.getValue(), email: eml.getValue(), oldpassword: opw.getValue(), password: pw1.getValue(), choice: agree }; }; this.reactToChoice = function () { if (purpose === 'signup') { emlLabel.changed(); emlLabel.text = age() <= 13 ? 'E-mail address of parent or guardian:' : 'E-mail address:'; emlLabel.text = localize(emlLabel.text); emlLabel.drawNew(); emlLabel.changed(); } }; this.reactToChoice(); // initialize e-mail label if (!this.key) { this.key = 'credentials' + title + purpose; } this.popUp(world); }; DialogBoxMorph.prototype.accept = function () { /* if target is a function, use it as callback: execute target as callback function with action as argument in the environment as optionally specified. Note: if action is also a function, instead of becoming the argument itself it will be called to answer the argument. for selections, Yes/No Choices etc: else (if target is not a function): if action is a function: execute the action with target as environment (can be null) for lambdafied (inline) actions else if action is a String: treat it as function property of target and execute it for selector-like actions */ if (this.action) { if (typeof this.target === 'function') { if (typeof this.action === 'function') { this.target.call(this.environment, this.action.call()); } else { this.target.call(this.environment, this.action); } } else { if (typeof this.action === 'function') { this.action.call(this.target, this.getInput()); } else { // assume it's a String this.target[this.action](this.getInput()); } } } this.destroy(); }; DialogBoxMorph.prototype.withKey = function (key) { this.key = key; return this; }; DialogBoxMorph.prototype.popUp = function (world) { if (world) { if (this.key) { if (this.instances[world.stamp]) { if (this.instances[world.stamp][this.key]) { this.instances[world.stamp][this.key].destroy(); } this.instances[world.stamp][this.key] = this; } else { this.instances[world.stamp] = {}; this.instances[world.stamp][this.key] = this; } } world.add(this); world.keyboardReceiver = this; this.setCenter(world.center()); this.edit(); } }; DialogBoxMorph.prototype.destroy = function () { DialogBoxMorph.uber.destroy.call(this); if (this.key) { delete this.instances[this.key]; } }; DialogBoxMorph.prototype.ok = function () { this.accept(); }; DialogBoxMorph.prototype.cancel = function () { this.destroy(); }; DialogBoxMorph.prototype.edit = function () { this.children.forEach(function (c) { if (c.edit) { return c.edit(); } }); }; DialogBoxMorph.prototype.getInput = function () { if (this.body instanceof InputFieldMorph) { return this.body.getValue(); } return null; }; DialogBoxMorph.prototype.justDropped = function (hand) { hand.world.keyboardReceiver = this; this.edit(); }; DialogBoxMorph.prototype.destroy = function () { var world = this.world(); world.keyboardReceiver = null; world.hand.destroyTemporaries(); DialogBoxMorph.uber.destroy.call(this); }; DialogBoxMorph.prototype.normalizeSpaces = function (string) { var ans = '', i, c, flag = false; for (i = 0; i < string.length; i += 1) { c = string[i]; if (c === ' ') { if (flag) { ans += c; flag = false; } } else { ans += c; flag = true; } } return ans.trim(); }; // DialogBoxMorph submorph construction DialogBoxMorph.prototype.createLabel = function () { var shading = !MorphicPreferences.isFlat || this.is3D; if (this.label) { this.label.destroy(); } if (this.labelString) { this.label = new StringMorph( localize(this.labelString), this.titleFontSize, this.fontStyle, true, false, false, shading ? new Point(2, 1) : null, this.titleBarColor.darker(this.contrast) ); this.label.color = this.titleTextColor; this.label.drawNew(); this.add(this.label); } }; DialogBoxMorph.prototype.createButtons = function () { if (this.buttons) { this.buttons.destroy(); } this.buttons = new AlignmentMorph('row', this.padding); this.add(this.buttons); }; DialogBoxMorph.prototype.addButton = function (action, label) { var button = new PushButtonMorph( this, action || 'ok', ' ' + localize((label || 'OK')) + ' ' ); button.fontSize = this.buttonFontSize; button.corner = this.buttonCorner; button.edge = this.buttonEdge; button.outline = this.buttonOutline; button.outlineColor = this.buttonOutlineColor; button.outlineGradient = this.buttonOutlineGradient; button.padding = this.buttonPadding; button.contrast = this.buttonContrast; button.drawNew(); button.fixLayout(); this.buttons.add(button); return button; }; DialogBoxMorph.prototype.setPicture = function (aMorphOrCanvas) { var morph; if (aMorphOrCanvas instanceof Morph) { morph = aMorphOrCanvas; } else { morph = new Morph(); morph.image = aMorphOrCanvas; morph.silentSetWidth(aMorphOrCanvas.width); morph.silentSetHeight(aMorphOrCanvas.height); } this.addHead(morph); }; DialogBoxMorph.prototype.addHead = function (aMorph) { if (this.head) { this.head.destroy(); } this.head = aMorph; this.add(this.head); }; DialogBoxMorph.prototype.addBody = function (aMorph) { if (this.body) { this.body.destroy(); } this.body = aMorph; this.add(this.body); }; // DialogBoxMorph layout DialogBoxMorph.prototype.addShadow = function () {nop(); }; DialogBoxMorph.prototype.removeShadow = function () {nop(); }; DialogBoxMorph.prototype.fixLayout = function () { var th = fontHeight(this.titleFontSize) + this.titlePadding * 2, w; if (this.head) { this.head.setPosition(this.position().add(new Point( this.padding, th + this.padding ))); this.silentSetWidth(this.head.width() + this.padding * 2); this.silentSetHeight( this.head.height() + this.padding * 2 + th ); } if (this.body) { if (this.head) { this.body.setPosition(this.head.bottomLeft().add(new Point( 0, this.padding ))); this.silentSetWidth(Math.max( this.width(), this.body.width() + this.padding * 2 )); this.silentSetHeight( this.height() + this.body.height() + this.padding ); w = this.width(); this.head.setLeft( this.left() + Math.round((w - this.head.width()) / 2) ); this.body.setLeft( this.left() + Math.round((w - this.body.width()) / 2) ); } else { this.body.setPosition(this.position().add(new Point( this.padding, th + this.padding ))); this.silentSetWidth(this.body.width() + this.padding * 2); this.silentSetHeight( this.body.height() + this.padding * 2 + th ); } } if (this.label) { this.label.setCenter(this.center()); this.label.setTop(this.top() + (th - this.label.height()) / 2); } if (this.buttons && (this.buttons.children.length > 0)) { this.buttons.fixLayout(); this.silentSetHeight( this.height() + this.buttons.height() + this.padding ); this.buttons.setCenter(this.center()); this.buttons.setBottom(this.bottom() - this.padding); } }; // DialogBoxMorph shadow /* only take the 'plain' image, so the box rounding doesn't become conflicted by the scrolling scripts pane */ DialogBoxMorph.prototype.shadowImage = function (off, color) { // fallback for Windows Chrome-Shadow bug var fb, img, outline, sha, ctx, offset = off || new Point(7, 7), clr = color || new Color(0, 0, 0); fb = this.extent(); img = this.image; outline = newCanvas(fb); ctx = outline.getContext('2d'); ctx.drawImage(img, 0, 0); ctx.globalCompositeOperation = 'destination-out'; ctx.drawImage( img, -offset.x, -offset.y ); sha = newCanvas(fb); ctx = sha.getContext('2d'); ctx.drawImage(outline, 0, 0); ctx.globalCompositeOperation = 'source-atop'; ctx.fillStyle = clr.toString(); ctx.fillRect(0, 0, fb.x, fb.y); return sha; }; DialogBoxMorph.prototype.shadowImageBlurred = function (off, color) { var fb, img, sha, ctx, offset = off || new Point(7, 7), blur = this.shadowBlur, clr = color || new Color(0, 0, 0); fb = this.extent().add(blur * 2); img = this.image; sha = newCanvas(fb); ctx = sha.getContext('2d'); ctx.shadowOffsetX = offset.x; ctx.shadowOffsetY = offset.y; ctx.shadowBlur = blur; ctx.shadowColor = clr.toString(); ctx.drawImage( img, blur - offset.x, blur - offset.y ); ctx.shadowOffsetX = 0; ctx.shadowOffsetY = 0; ctx.shadowBlur = 0; ctx.globalCompositeOperation = 'destination-out'; ctx.drawImage( img, blur - offset.x, blur - offset.y ); return sha; }; // DialogBoxMorph keyboard events DialogBoxMorph.prototype.processKeyPress = function () {nop(); }; DialogBoxMorph.prototype.processKeyDown = function (event) { // this.inspectKeyEvent(event); switch (event.keyCode) { case 13: this.ok(); break; case 27: this.cancel(); break; default: nop(); // this.inspectKeyEvent(event); } }; // DialogBoxMorph drawing DialogBoxMorph.prototype.drawNew = function () { this.fullChanged(); Morph.prototype.trackChanges = false; DialogBoxMorph.uber.removeShadow.call(this); this.fixLayout(); var context, gradient, w = this.width(), h = this.height(), th = Math.floor( fontHeight(this.titleFontSize) + this.titlePadding * 2 ), shift = this.corner / 2, x, y, isFlat = MorphicPreferences.isFlat && !this.is3D; // this.alpha = isFlat ? 0.9 : 1; this.image = newCanvas(this.extent()); context = this.image.getContext('2d'); // title bar if (isFlat) { context.fillStyle = this.titleBarColor.toString(); } else { gradient = context.createLinearGradient(0, 0, 0, th); gradient.addColorStop( 0, this.titleBarColor.lighter(this.contrast / 2).toString() ); gradient.addColorStop( 1, this.titleBarColor.darker(this.contrast).toString() ); context.fillStyle = gradient; } context.beginPath(); this.outlinePathTitle( context, isFlat ? 0 : this.corner ); context.closePath(); context.fill(); // flat shape // body context.fillStyle = this.color.toString(); context.beginPath(); this.outlinePathBody( context, isFlat ? 0 : this.corner ); context.closePath(); context.fill(); if (isFlat) { DialogBoxMorph.uber.addShadow.call(this); Morph.prototype.trackChanges = true; this.fullChanged(); return; } // 3D-effect // bottom left corner gradient = context.createLinearGradient( 0, h - this.corner, 0, h ); gradient.addColorStop(0, this.color.toString()); gradient.addColorStop(1, this.color.darker(this.contrast.toString())); context.lineWidth = this.corner; context.lineCap = 'round'; context.strokeStyle = gradient; context.beginPath(); context.moveTo(this.corner, h - shift); context.lineTo(this.corner + 1, h - shift); context.stroke(); // bottom edge gradient = context.createLinearGradient( 0, h - this.corner, 0, h ); gradient.addColorStop(0, this.color.toString()); gradient.addColorStop(1, this.color.darker(this.contrast.toString())); context.lineWidth = this.corner; context.lineCap = 'butt'; context.strokeStyle = gradient; context.beginPath(); context.moveTo(this.corner, h - shift); context.lineTo(w - this.corner, h - shift); context.stroke(); // right body edge gradient = context.createLinearGradient( w - this.corner, 0, w, 0 ); gradient.addColorStop(0, this.color.toString()); gradient.addColorStop(1, this.color.darker(this.contrast).toString()); context.lineWidth = this.corner; context.lineCap = 'butt'; context.strokeStyle = gradient; context.beginPath(); context.moveTo(w - shift, th); context.lineTo(w - shift, h - this.corner); context.stroke(); // bottom right corner x = w - this.corner; y = h - this.corner; gradient = context.createRadialGradient( x, y, 0, x, y, this.corner ); gradient.addColorStop(0, this.color.toString()); gradient.addColorStop(1, this.color.darker(this.contrast.toString())); context.lineCap = 'butt'; context.strokeStyle = gradient; context.beginPath(); context.arc( x, y, shift, radians(90), radians(0), true ); context.stroke(); // left body edge gradient = context.createLinearGradient( 0, 0, this.corner, 0 ); gradient.addColorStop(1, this.color.toString()); gradient.addColorStop( 0, this.color.lighter(this.contrast).toString() ); context.lineCap = 'butt'; context.strokeStyle = gradient; context.beginPath(); context.moveTo(shift, th); context.lineTo(shift, h - this.corner * 2); context.stroke(); // left vertical bottom corner gradient = context.createLinearGradient( 0, 0, this.corner, 0 ); gradient.addColorStop(1, this.color.toString()); gradient.addColorStop( 0, this.color.lighter(this.contrast).toString() ); context.lineCap = 'round'; context.strokeStyle = gradient; context.beginPath(); context.moveTo(shift, h - this.corner * 2); context.lineTo(shift, h - this.corner - shift); context.stroke(); DialogBoxMorph.uber.addShadow.call(this); Morph.prototype.trackChanges = true; this.fullChanged(); }; DialogBoxMorph.prototype.outlinePathTitle = function (context, radius) { var w = this.width(), h = Math.ceil(fontHeight(this.titleFontSize)) + this.titlePadding * 2; // top left: context.arc( radius, radius, radius, radians(-180), radians(-90), false ); // top right: context.arc( w - radius, radius, radius, radians(-90), radians(-0), false ); // bottom right: context.lineTo(w, h); // bottom left: context.lineTo(0, h); }; DialogBoxMorph.prototype.outlinePathBody = function (context, radius) { var w = this.width(), h = this.height(), th = Math.floor(fontHeight(this.titleFontSize)) + this.titlePadding * 2; // top left: context.moveTo(0, th); // top right: context.lineTo(w, th); // bottom right: context.arc( w - radius, h - radius, radius, radians(0), radians(90), false ); // bottom left: context.arc( radius, h - radius, radius, radians(90), radians(180), false ); }; // AlignmentMorph ///////////////////////////////////////////////////// // I am a reified layout, either a row or a column of submorphs // AlignmentMorph inherits from Morph: AlignmentMorph.prototype = new Morph(); AlignmentMorph.prototype.constructor = AlignmentMorph; AlignmentMorph.uber = Morph.prototype; // AlignmentMorph instance creation: function AlignmentMorph(orientation, padding) { this.init(orientation, padding); } AlignmentMorph.prototype.init = function (orientation, padding) { // additional properties: this.orientation = orientation || 'row'; // or 'column' this.alignment = 'center'; // or 'left' in a column this.padding = padding || 0; this.respectHiddens = false; // initialize inherited properties: AlignmentMorph.uber.init.call(this); // override inherited properites: }; // AlignmentMorph displaying and layout AlignmentMorph.prototype.drawNew = function () { this.image = newCanvas(new Point(1, 1)); this.fixLayout(); }; AlignmentMorph.prototype.fixLayout = function () { var myself = this, last = null, newBounds; if (this.children.length === 0) { return null; } this.children.forEach(function (c) { var cfb = c.fullBounds(), lfb; if (c.isVisible || myself.respectHiddens) { if (last) { lfb = last.fullBounds(); if (myself.orientation === 'row') { c.setPosition( lfb.topRight().add(new Point( myself.padding, (lfb.height() - cfb.height()) / 2 )) ); } else { // orientation === 'column' c.setPosition( lfb.bottomLeft().add(new Point( myself.alignment === 'center' ? (lfb.width() - cfb.width()) / 2 : 0, myself.padding )) ); } newBounds = newBounds.merge(cfb); } else { newBounds = cfb; } last = c; } }); this.bounds = newBounds; }; // InputFieldMorph ////////////////////////////////////////////////////// // InputFieldMorph inherits from Morph: InputFieldMorph.prototype = new Morph(); InputFieldMorph.prototype.constructor = InputFieldMorph; InputFieldMorph.uber = Morph.prototype; // InputFieldMorph settings InputFieldMorph.prototype.edge = 2; InputFieldMorph.prototype.fontSize = 12; InputFieldMorph.prototype.typeInPadding = 2; InputFieldMorph.prototype.contrast = 65; // InputFieldMorph instance creation: function InputFieldMorph(text, isNumeric, choiceDict, isReadOnly) { this.init(text, isNumeric, choiceDict, isReadOnly); } InputFieldMorph.prototype.init = function ( text, isNumeric, choiceDict, isReadOnly ) { var contents = new StringFieldMorph(text || ''), arrow = new ArrowMorph( 'down', 0, Math.max(Math.floor(this.fontSize / 6), 1) ); this.choices = choiceDict || null; // object, function or selector this.isReadOnly = isReadOnly || false; this.isNumeric = isNumeric || false; contents.alpha = 0; contents.fontSize = this.fontSize; contents.drawNew(); this.oldContentsExtent = contents.extent(); this.isNumeric = isNumeric || false; InputFieldMorph.uber.init.call(this); this.color = new Color(255, 255, 255); this.add(contents); this.add(arrow); contents.isDraggable = false; this.drawNew(); }; // InputFieldMorph accessing: InputFieldMorph.prototype.contents = function () { return detect( this.children, function (child) { return (child instanceof StringFieldMorph); } ); }; InputFieldMorph.prototype.arrow = function () { return detect( this.children, function (child) { return (child instanceof ArrowMorph); } ); }; InputFieldMorph.prototype.setChoice = function (aStringOrFloat) { this.setContents(aStringOrFloat); this.escalateEvent('reactToChoice', aStringOrFloat); }; InputFieldMorph.prototype.setContents = function (aStringOrFloat) { var cnts = this.contents(); cnts.text.text = aStringOrFloat; if (aStringOrFloat === undefined) { return null; } if (aStringOrFloat === null) { cnts.text.text = ''; } else if (aStringOrFloat.toString) { cnts.text.text = aStringOrFloat.toString(); } cnts.drawNew(); cnts.changed(); }; InputFieldMorph.prototype.edit = function () { var c = this.contents(); c.text.edit(); c.text.selectAll(); }; InputFieldMorph.prototype.setIsNumeric = function (bool) { var value; this.isNumeric = bool; this.contents().isNumeric = bool; this.contents().text.isNumeric = bool; // adjust my shown value to conform with the numeric flag value = this.getValue(); if (this.isNumeric) { value = parseFloat(value); if (isNaN(value)) { value = null; } } this.setContents(value); }; // InputFieldMorph drop-down menu: InputFieldMorph.prototype.dropDownMenu = function () { var choices = this.choices, key, menu = new MenuMorph( this.setChoice, null, this, this.fontSize ); if (choices instanceof Function) { choices = choices.call(this); } else if (isString(choices)) { choices = this[choices](); } if (!choices) { return null; } menu.addItem(' ', null); if (choices instanceof Array) { choices.forEach(function (choice) { menu.addItem(choice[0], choice[1]); }); } else { // assuming a dictionary for (key in choices) { if (Object.prototype.hasOwnProperty.call(choices, key)) { if (key[0] === '~') { menu.addLine(); } else { menu.addItem(key, choices[key]); } } } } if (menu.items.length > 0) { menu.popUpAtHand(this.world()); } else { return null; } }; // InputFieldMorph layout: InputFieldMorph.prototype.fixLayout = function () { var contents = this.contents(), arrow = this.arrow(); if (!contents) {return null; } contents.isNumeric = this.isNumeric; contents.isEditable = (!this.isReadOnly); if (this.choices) { arrow.setSize(this.fontSize); arrow.show(); } else { arrow.setSize(0); arrow.hide(); } this.silentSetHeight( contents.height() + this.edge * 2 + this.typeInPadding * 2 ); this.silentSetWidth(Math.max( contents.minWidth + this.edge * 2 + this.typeInPadding * 2, this.width() )); contents.setWidth( this.width() - this.edge - this.typeInPadding - (this.choices ? arrow.width() + this.typeInPadding : 0) ); contents.silentSetPosition(new Point( this.edge, this.edge ).add(this.typeInPadding).add(this.position())); arrow.silentSetPosition(new Point( this.right() - arrow.width() - this.edge, contents.top() )); }; // InputFieldMorph events: InputFieldMorph.prototype.mouseClickLeft = function (pos) { if (this.arrow().bounds.containsPoint(pos)) { this.dropDownMenu(); } else if (this.isReadOnly) { this.dropDownMenu(); } else { this.escalateEvent('mouseClickLeft', pos); } }; // InputFieldMorph retrieving: InputFieldMorph.prototype.getValue = function () { /* answer my content's text string. If I am numerical convert that string to a number. If the conversion fails answer the string otherwise the numerical value. */ var num, contents = this.contents(); if (this.isNumeric) { num = parseFloat(contents.text); if (!isNaN(num)) { return num; } } return this.normalizeSpaces(contents.string()); }; InputFieldMorph.prototype.normalizeSpaces = DialogBoxMorph.prototype.normalizeSpaces; // InputFieldMorph drawing: InputFieldMorph.prototype.drawNew = function () { var context, borderColor; this.fixLayout(); // initialize my surface property this.image = newCanvas(this.extent()); context = this.image.getContext('2d'); if (this.parent) { if (this.parent.color.eq(new Color(255, 255, 255))) { this.color = this.parent.color.darker(this.contrast * 0.1); } else { this.color = this.parent.color.lighter(this.contrast * 0.75); } borderColor = this.parent.color; } else { borderColor = new Color(120, 120, 120); } context.fillStyle = this.color.toString(); // cache my border colors this.cachedClr = borderColor.toString(); this.cachedClrBright = borderColor.lighter(this.contrast) .toString(); this.cachedClrDark = borderColor.darker(this.contrast).toString(); context.fillRect( this.edge, this.edge, this.width() - this.edge * 2, this.height() - this.edge * 2 ); this.drawRectBorder(context); }; InputFieldMorph.prototype.drawRectBorder = function (context) { var shift = this.edge * 0.5, gradient; if (MorphicPreferences.isFlat && !this.is3D) {return; } context.lineWidth = this.edge; context.lineJoin = 'round'; context.lineCap = 'round'; context.shadowOffsetY = shift; context.shadowBlur = this.edge * 4; context.shadowColor = this.cachedClrDark; gradient = context.createLinearGradient( 0, 0, 0, this.edge ); gradient.addColorStop(0, this.cachedClr); gradient.addColorStop(1, this.cachedClrDark); context.strokeStyle = gradient; context.beginPath(); context.moveTo(this.edge, shift); context.lineTo(this.width() - this.edge - shift, shift); context.stroke(); context.shadowOffsetY = 0; gradient = context.createLinearGradient( 0, 0, this.edge, 0 ); gradient.addColorStop(0, this.cachedClr); gradient.addColorStop(1, this.cachedClrDark); context.strokeStyle = gradient; context.beginPath(); context.moveTo(shift, this.edge); context.lineTo(shift, this.height() - this.edge - shift); context.stroke(); context.shadowOffsetX = 0; context.shadowOffsetY = 0; context.shadowBlur = 0; gradient = context.createLinearGradient( 0, this.height() - this.edge, 0, this.height() ); gradient.addColorStop(0, this.cachedClrBright); gradient.addColorStop(1, this.cachedClr); context.strokeStyle = gradient; context.beginPath(); context.moveTo(this.edge, this.height() - shift); context.lineTo(this.width() - this.edge, this.height() - shift); context.stroke(); gradient = context.createLinearGradient( this.width() - this.edge, 0, this.width(), 0 ); gradient.addColorStop(0, this.cachedClrBright); gradient.addColorStop(1, this.cachedClr); context.strokeStyle = gradient; context.beginPath(); context.moveTo(this.width() - shift, this.edge); context.lineTo(this.width() - shift, this.height() - this.edge); context.stroke(); };