diff options
| author | jmoenig <jens@moenig.org> | 2013-03-16 09:02:16 +0100 |
|---|---|---|
| committer | jmoenig <jens@moenig.org> | 2013-03-16 09:02:16 +0100 |
| commit | 0b510366d230399a38f18a8db92dd396a0dad42e (patch) | |
| tree | ffb841d64bca04799ebc470701b71e59e726017e /widgets.js | |
| download | snap-yow-0b510366d230399a38f18a8db92dd396a0dad42e.tar.gz snap-yow-0b510366d230399a38f18a8db92dd396a0dad42e.zip | |
Creation
Diffstat (limited to 'widgets.js')
| -rw-r--r-- | widgets.js | 2912 |
1 files changed, 2912 insertions, 0 deletions
diff --git a/widgets.js b/widgets.js new file mode 100644 index 0000000..6740ef5 --- /dev/null +++ b/widgets.js @@ -0,0 +1,2912 @@ +/* + + widgets.js + + additional GUI elements for morphic.js + + written by Jens Mšnig + jens@moenig.org + + Copyright (C) 2013 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 <http://www.gnu.org/licenses/>. + + + 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*/ + +modules.widgets = '2013-February-26'; + +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.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; + + if (!this.outline) {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, + this.corner, + 0 + ); + context.closePath(); + context.fill(); +}; + +PushButtonMorph.prototype.drawBackground = function (context, color) { + context.fillStyle = color.toString(); + context.beginPath(); + this.outlinePath( + context, + 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 +) { + 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 () { + if (this.label !== null) { + this.label.destroy(); + } + if (this.labelString instanceof SymbolMorph) { + this.label = this.labelString.fullCopy(); + 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, + this.labelShadowOffset, + 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, <array>: [normal, highlight, pressed] + target, + action, // a toggle function + labelString, + query, // predicate/selector + environment, + hint, + template, // optional, for cached background images + minWidth, // <num> optional, if specified label will left-align + hasPreview, // <bool> 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 () { + if (!this.state) { + this.image = this.highlightImage; + this.changed(); + } + if (this.hint) { + this.bubbleHelp(this.hint); + } +}; + +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 + 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 () { + 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 = this.labelShadowOffset; + this.label.shadowColor = this.labelShadowColor; + this.label.color = this.labelColor; + this.label.drawNew(); + + this.trueStateLabel.shadowOffset = this.labelShadowOffset; + 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, + this.labelShadowOffset, + this.labelShadowColor, + this.labelColor + ); + this.trueStateLabel = new StringMorph( + localize(this.labelString[1]), + this.fontSize, + this.fontStyle, + true, + false, + false, + this.labelShadowOffset, + this.labelShadowColor, + this.labelColor + ); + } + } else { + if (this.labelString instanceof SymbolMorph) { + this.label = this.labelString.fullCopy(); + if (!this.isPicture) { + this.label.shadowOffset = this.labelShadowOffset; + 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, + this.labelShadowOffset, + 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, <array>: [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 +) { + 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 () { + 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, + new Point(1, 1), + 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 () { + this.color = this.element.color; + this.element.removeShadow(); + this.element[this.builder](); + 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); + 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); + 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 + +// 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 instance creation: + +function DialogBoxMorph(target, action, environment) { + this.init(target, action, environment); +} + +DialogBoxMorph.prototype.init = function (target, action, environment) { + // additional properties: + this.target = target || null; + this.action = action || null; + this.environment = environment || null; + + 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, + new Point(1, 1), + new Color(255, 255, 255) + ); + this.labelString = title; + this.createLabel(); + if (pic) {this.setPicture(pic); } + if (textString) { + this.addBody(txt); + } + this.addButton('ok', 'OK'); + this.drawNew(); + this.fixLayout(); + if (world) { + world.add(this); + world.keyboardReceiver = this; + this.setCenter(world.center()); + } +}; + +DialogBoxMorph.prototype.askYesNo = function ( + title, + textString, + world, + pic +) { + var txt = new TextMorph( + textString, + this.fontSize, + this.fontStyle, + true, + false, + 'center', + null, + null, + new Point(1, 1), + new Color(255, 255, 255) + ); + 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(); + if (world) { + world.add(this); + world.keyboardReceiver = this; + this.setCenter(world.center()); + } +}; + +DialogBoxMorph.prototype.prompt = function ( + title, + defaultString, + world, + pic, + choices, // optional dictionary for drop-down of choices + isReadOnly // optional when using choices +) { + var txt = new InputFieldMorph( + defaultString, + false, // numeric? + choices || null, // drop-down dict, optional + choices ? isReadOnly || false : false + ); + txt.setWidth(250); + this.labelString = title; + this.createLabel(); + if (pic) {this.setPicture(pic); } + this.addBody(txt); + txt.drawNew(); + this.addButton('ok', 'OK'); + this.addButton('cancel', 'Cancel'); + this.fixLayout(); + this.drawNew(); + this.fixLayout(); + if (world) { + world.add(this); + this.setCenter(world.center()); + this.edit(); + } +}; + +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( + string, + 10, + null, // style + false, // bold + null, // italic + null, // alignment + null, // width + null, // font name + new Point(1, 1), // shadow offset + 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 + ' 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); + inp.add(emlLabel = labelText('foo')); + 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 (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(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]; + } + + 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' + 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.drawNew(); + emlLabel.changed(); + } + }; + + this.reactToChoice(); // initialize e-mail label + + if (world) { + world.add(this); + this.setCenter(world.center()); + this.edit(); + } +}; + +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.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 () { + if (this.label) { + this.label.destroy(); + } + if (this.labelString) { + this.label = new StringMorph( + localize(this.labelString), + this.titleFontSize, + this.fontStyle, + true, + false, + false, + new Point(2, 1), + 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: + // 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; + + this.image = newCanvas(this.extent()); + context = this.image.getContext('2d'); + + // title bar + 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, + this.corner + ); + context.closePath(); + context.fill(); + + // flat shape + // body + context.fillStyle = this.color.toString(); + context.beginPath(); + this.outlinePathBody( + context, + this.corner + ); + context.closePath(); + context.fill(); + + // 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); + for (key in choices) { + if (choices.hasOwnProperty(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) { + 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; + + 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(); +};
\ No newline at end of file |
