summaryrefslogtreecommitdiff
path: root/morphic.js
diff options
context:
space:
mode:
Diffstat (limited to 'morphic.js')
-rw-r--r--morphic.js10565
1 files changed, 10565 insertions, 0 deletions
diff --git a/morphic.js b/morphic.js
new file mode 100644
index 0000000..9bbeaa0
--- /dev/null
+++ b/morphic.js
@@ -0,0 +1,10565 @@
+/*
+
+ morphic.js
+
+ a lively Web-GUI
+ inspired by Squeak
+
+ 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/>.
+
+
+ documentation contents
+ ----------------------
+ I. inheritance hierarchy
+ II. object definition toc
+ III. yet to implement
+ IV. open issues
+ V. browser compatibility
+ VI. the big picture
+ VII. programming guide
+ (1) setting up a web page
+ (a) single world
+ (b) multiple worlds
+ (c) an application
+ (2) manipulating morphs
+ (3) events
+ (a) mouse events
+ (b) context menu
+ (c) dragging
+ (d) dropping
+ (e) keyboard events
+ (f) resize event
+ (g) combined mouse-keyboard events
+ (h) text editing events
+ (4) stepping
+ (5) creating new kinds of morphs
+ (6) development and user modes
+ (7) turtle graphics
+ (8) damage list housekeeping
+ (9) minifying morphic.js
+ VIII. acknowledgements
+ IX. contributors
+
+
+ I. hierarchy
+ -------------
+ the following tree lists all constructors hierarchically,
+ indentation indicating inheritance. Refer to this list to get a
+ contextual overview:
+
+ Color
+ Node
+ Morph
+ BlinkerMorph
+ CursorMorph
+ BouncerMorph*
+ BoxMorph
+ InspectorMorph
+ MenuMorph
+ MouseSensorMorph*
+ SpeechBubbleMorph
+ CircleBoxMorph
+ SliderButtonMorph
+ SliderMorph
+ ColorPaletteMorph
+ GrayPaletteMorph
+ ColorPickerMorph
+ FrameMorph
+ ScrollFrameMorph
+ ListMorph
+ StringFieldMorph
+ WorldMorph
+ HandleMorph
+ HandMorph
+ PenMorph
+ ShadowMorph
+ StringMorph
+ TextMorph
+ TriggerMorph
+ MenuItemMorph
+ Point
+ Rectangle
+
+
+ II. toc
+ -------
+ the following list shows the order in which all constructors are
+ defined. Use this list to locate code in this document:
+
+
+ Global settings
+ Global functions
+
+ Color
+ Point
+ Rectangle
+ Node
+ Morph
+ ShadowMorph
+ HandleMorph
+ PenMorph
+ ColorPaletteMorph
+ GrayPaletteMorph
+ ColorPickerMorph
+ BlinkerMorph
+ CursorMorph
+ BoxMorph
+ SpeechBubbleMorph
+ CircleBoxMorph
+ SliderButtonMorph
+ SliderMorph
+ MouseSensorMorph*
+ InspectorMorph
+ MenuMorph
+ StringMorph
+ TextMorph
+ TriggerMorph
+ MenuItemMorph
+ FrameMorph
+ ScrollFrameMorph
+ ListMorph
+ StringFieldMorph
+ BouncerMorph*
+ HandMorph
+ WorldMorph
+
+ * included only for demo purposes
+
+
+ III. yet to implement
+ ---------------------
+ - keyboard support for scroll frames and lists
+
+
+ IV. open issues
+ ----------------
+ - blurry shadows don't work well in Chrome
+
+
+ V. browser compatibility
+ ------------------------
+ I have taken great care and considerable effort to make morphic.js
+ runnable and appearing exactly the same on all current browsers
+ available to me:
+
+ - Firefox for Windows
+ - Firefox for Mac
+ - Chrome for Windows (blurry shadows have some issues)
+ - Chrome for Mac
+ - Safari for Windows
+ - safari for Mac
+ - Safari for iOS (mobile)
+ - IE for Windows
+ - Opera for Windows
+ - Opera for Mac
+
+
+ VI. the big picture
+ -------------------
+ Morphic.js is completely based on Canvas and JavaScript, it is just
+ Morphic, nothing else. Morphic.js is very basic and covers only the
+ bare essentials:
+
+ * a stepping mechanism (a time-sharing multiplexer for lively
+ user interaction ontop of a single OS/browser thread)
+ * progressive display updates (only dirty rectangles are
+ redrawn in each display cycle)
+ * a tree structure
+ * a single World per Canvas element (although you can have
+ multiple worlds in multiple Canvas elements on the same web
+ page)
+ * a single Hand per World (but you can support multi-touch
+ events)
+ * a single text entry focus per World
+
+ In its current state morphic.js doesn't support Transforms (you
+ cannot rotate Morphs), but with PenMorph there already is a simple
+ LOGO-like turtle that you can use to draw onto any Morph it is
+ attached to. I'm planning to add special Morphs that support these
+ operations later on, but not for every Morph in the system.
+ Therefore these additions ("sprites" etc.) are likely to be part of
+ other libraries ("microworld.js") in separate files.
+
+ the purpose of morphic.js is to provide a malleable framework that
+ will let me experiment with lively GUIs for my hobby horse, which
+ is drag-and-drop, blocks based programming languages. Those things
+ (BYOB4 - http://byob.berkeley.edu) will be written using morphic.js
+ as a library.
+
+
+ VII. programming guide
+ ----------------------
+ Morphic.js provides a library for lively GUIs inside single HTML
+ Canvas elements. Each such canvas element functions as a "world" in
+ which other visible shapes ("morphs") can be positioned and
+ manipulated, often directly and interactively by the user. Morphs
+ are tree nodes and may contain any number of submorphs ("children").
+
+ All things visible in a morphic World are morphs themselves, i.e.
+ all text rendering, blinking cursors, entry fields, menus, buttons,
+ sliders, windows and dialog boxes etc. are created with morphic.js
+ rather than using HTML DOM elements, and as a consequence can be
+ changed and adjusted by the programmer regardless of proprietary
+ browser behavior.
+
+ Each World has an - invisible - "Hand" resembling the mouse cursor
+ (or the user's finger on touch screens) which handles mouse events,
+ and may also have a keyboardReceiver to handle key events.
+
+ The basic idea of Morphic is to continuously run display cycles and
+ to incrementally update the screen by only redrawing those World
+ regions which have been "dirtied" since the last redraw. Before
+ each shape is processed for redisplay it gets the chance to perform
+ a "step" procedure, thus allowing for an illusion of concurrency.
+
+
+ (1) setting up a web page
+ -------------------------
+ Setting up a web page for Morphic always involves three steps:
+ adding one or more Canvas elements, defining one or more worlds,
+ initializing and starting the main loop.
+
+
+ (a) single world
+ -----------------
+ Most commonly you will want your World to fill the browsers's whole
+ client area. This default situation is easiest and most straight
+ forward.
+
+ example html file:
+
+ <!DOCTYPE html>
+ <html>
+ <head>
+ <title>Morphic!</title>
+ <script type="text/javascript" src="morphic.js"></script>
+ <script type="text/javascript">
+ var world;
+
+ window.onload = function () {
+ world = new WorldMorph(
+ document.getElementById('world'));
+ setInterval(loop, 50);
+ };
+
+ function loop() {
+ world.doOneCycle();
+ }
+ </script>
+ </head>
+ <body>
+ <canvas id="world" tabindex="1" width="800" height="600">
+ <p>Your browser doesn't support canvas.</p>
+ </canvas>
+ </body>
+ </html>
+
+ if you use ScrollFrames or otherwise plan to support mouse wheel
+ scrolling events, you might also add the following inline-CSS
+ attribute to the Canvas element:
+
+ style="position: absolute;"
+
+ which will prevent the World to be scrolled around instead of the
+ elements inside of it in some browsers.
+
+
+ (b) multiple worlds
+ -------------------
+ If you wish to create a web page with more than one world, make
+ sure to prevent each world from auto-filling the whole page and
+ include it in the main loop. It's also a good idea to give each
+ world its own tabindex:
+
+ example html file:
+
+ <!DOCTYPE html>
+ <html>
+ <head>
+ <title>Morphic!</title>
+ <script type="text/javascript" src="morphic.js"></script>
+ <script type="text/javascript">
+ var world1, world2;
+
+ window.onload = function () {
+ world1 = new WorldMorph(
+ document.getElementById('world1'), false);
+ world2 = new WorldMorph(
+ document.getElementById('world2'), false);
+ setInterval(loop, 50);
+ };
+
+ function loop() {
+ world1.doOneCycle();
+ world2.doOneCycle();
+ }
+ </script>
+ </head>
+ <body>
+ <p>first world:</p>
+ <canvas id="world1" tabindex="1" width="600" height="400">
+ <p>Your browser doesn't support canvas.</p>
+ </canvas>
+ <p>second world:</p>
+ <canvas id="world2" tabindex="2" width="400" height="600">
+ <p>Your browser doesn't support canvas.</p>
+ </canvas>
+ </body>
+ </html>
+
+
+ (c) an application
+ -------------------
+ Of course, most of the time you don't want to just plain use the
+ standard Morhic World "as is" out of the box, but write your own
+ application (something like Scratch!) in it. For such an
+ application you'll create your own morph prototypes, perhaps
+ assemble your own "window frame" and bring it all to life in a
+ customized World state. the following example creates a simple
+ snake-like mouse drawing game.
+
+ example html file:
+
+ <!DOCTYPE html>
+ <html>
+ <head>
+ <title>touch me!</title>
+ <script type="text/javascript" src="morphic.js"></script>
+ <script type="text/javascript">
+ var worldCanvas, sensor;
+
+ window.onload = function () {
+ var x, y, w, h;
+
+ worldCanvas = document.getElementById('world');
+ world = new WorldMorph(worldCanvas);
+ world.isDevMode = false;
+ world.color = new Color();
+
+ w = 100;
+ h = 100;
+
+ x = 0;
+ y = 0;
+
+ while ((y * h) < world.height()) {
+ while ((x * w) < world.width()) {
+ sensor = new MouseSensorMorph();
+ sensor.setPosition(new Point(x * w, y * h));
+ sensor.alpha = 0;
+ sensor.setExtent(new Point(w, h));
+ world.add(sensor);
+ x += 1;
+ }
+ x = 0;
+ y += 1;
+ }
+ setInterval(loop, 50);
+ };
+
+ function loop() {
+ world.doOneCycle();
+ }
+ </script>
+ </head>
+ <body bgcolor='black'>
+ <canvas id="world" width="800" height="600">
+ <p>Your browser doesn't support canvas.</p>
+ </canvas>
+ </body>
+ </html>
+
+ To get an idea how you can craft your own custom morph prototypes
+ I've included two examples which should give you an idea how to add
+ properties, override inherited methods and use the stepping
+ mechanism for "livelyness":
+
+ BouncerMorph
+ MouseSensorMorph
+
+ For the sake of sharing a single file I've included those examples
+ in morphic.js itself. Usually you'll define your additions in a
+ separate file and keep morphic.js untouched.
+
+
+ (2) manipulating morphs
+ -----------------------
+ There are many methods to programmatically manipulate morphs. Among
+ the most important and common ones among all morphs are the
+ following nine:
+
+ * hide()
+ * show()
+
+ * setPosition(aPoint)
+ * setExtent(aPoint)
+ * setColor(aColor)
+
+ * add(submorph) - attaches submorph ontop
+ * addBack(submorph) - attaches submorph underneath
+
+ * fullCopy() - duplication
+ * destroy() - deletion
+
+
+ (3) events
+ ----------
+ All user (and system) interaction is triggered by events, which are
+ passed on from the root element - the World - to its submorphs. The
+ World contains a list of system (browser) events it reacts to in its
+
+ initEventListeners()
+
+ method. Currently there are
+
+ - mouse
+ - drop
+ - keyboard
+ - (window) resize
+
+ events.
+
+ These system events are dispatched within the morphic World by the
+ World's Hand and its keyboardReceiver (usually the active text
+ cursor).
+
+
+ (a) mouse events:
+ -----------------
+ The Hand dispatches the following mouse events to relevant morphs:
+
+ mouseDownLeft
+ mouseDownRight
+ mouseClickLeft
+ mouseClickRight
+ mouseEnter
+ mouseLeave
+ mouseEnterDragging
+ mouseLeaveDragging
+ mouseMove
+ mouseScroll
+
+ If you wish your morph to react to any such event, simply add a
+ method of the same name as the event, e.g:
+
+ MyMorph.prototype.mouseMove = function(pos) {};
+
+ The only optional parameter of such a method is a Point object
+ indicating the current position of the Hand inside the World's
+ coordinate system.
+
+ Events may be "bubbled" up a morph's owner chain by calling
+
+ this.escalateEvent(functionName, arg)
+
+ in the event handler method's code.
+
+ Likewise, removing the event handler method will render your morph
+ passive to the event in question.
+
+
+ (b) context menu:
+ -----------------
+ By default right-clicking (or single-finger tap-and-hold) on a morph
+ also invokes its context menu (in addition to firing the
+ mouseClickRight event). A morph's context menu can be customized by
+ assigning a Menu instance to its
+
+ customContextMenu
+
+ property, or altogether suppressed by overriding its inherited
+
+ contextMenu()
+
+ method.
+
+
+ (c) dragging:
+ -------------
+ Dragging a morph is initiated when the left mouse button is pressed,
+ held and the mouse is moved.
+
+ You can control whether a morph is draggable by setting its
+
+ isDraggable
+
+ property either to false or true. If a morph isn't draggable itself
+ it will pass the pick-up request up its owner chain. This lets you
+ create draggable composite morphs like Windows, DialogBoxes,
+ Sliders etc.
+
+ Sometimes it is desireable to make "template" shapes which cannot be
+ moved themselves, but from which instead duplicates can be peeled
+ off. This is especially useful for building blocks in construction
+ kits, e.g. the MIT-Scratch palette. Morphic.js lets you control this
+ functionality by setting the
+
+ isTemplate
+
+ property flag to true for any morph whose "isDraggable" property is
+ turned off. When dragging such a Morph the hand will instead grab
+ a duplicate of the template whose "isDraggable" flag is true and
+ whose "isTemplate" flag is false, in other words: a non-template.
+
+ Dragging is indicated by adding a drop shadow to the morph in hand.
+ If a morph follows the hand without displaying a drop shadow it is
+ merely being moved about without changing its parent (owner morph),
+ e.g. when "dragging" a morph handle to resize its owner, or when
+ "dragging" a slider button.
+
+ Right before a morph is picked up its
+
+ prepareToBeGrabbed(handMorph)
+
+ method is invoked, if it is present. Immediately after the pick-up
+ the former parent's
+
+ reactToGrabOf(grabbedMorph)
+
+ method is called, again only if it exists.
+
+ Similar to events, these methods are optional and don't exist by
+ default. For a simple example of how they can be used to adjust
+ scroll bars in a scroll frame please have a look at their
+ implementation in FrameMorph.
+
+
+ (d) dropping:
+ -------------
+ Dropping is triggered when the left mouse button is either pressed
+ or released while the Hand is dragging a morph.
+
+ Dropping a morph causes it to become embedded in a new owner morph.
+ You can control this embedding behavior by setting the prospective
+ drop target's
+
+ acceptsDrops
+
+ property to either true or false, or by overriding its inherited
+
+ wantsDropOf(aMorph)
+
+ method.
+
+ Right after a morph has been dropped its
+
+ justDropped(handMorph)
+
+ method is called, and its new parent's
+
+ reactToDropOf(droppedMorph, handMorph)
+
+ method is invoked, again only if each method exists.
+
+ Similar to events, these methods are optional and by default are
+ not present in morphs by default (watch out for inheritance,
+ though!). For a simple example of how they can be used to adjust
+ scroll bars in a scroll frame please have a look at their
+ implementation in FrameMorph.
+
+ Drops of image elements from outside the world canvas are dispatched as
+
+ droppedImage(aCanvas, name)
+ droppedSVG(anImage, name)
+
+ events to interested Morphs at the mouse pointer. If you want you Morph
+ to e.g. import outside images you can add the droppedImage() and / or the
+ droppedSVG() methods to it. The parameter passed to the event handles is
+ a new offscreen canvas element representing a copy of the original image
+ element which can be directly used, e.g. by assigning it to another
+ Morph's image property. In the case of a dropped SVG it is an image
+ element (not a canvas), which has to be rasterized onto a canvas before
+ it can be used. The benefit of handling SVGs as image elements is that
+ rasterization can be deferred until the destination scale is known, taking
+ advantage of SVG's ability for smooth scaling. If instead SVGs are to be
+ rasterized right away, you can set the
+
+ MorphicPreferences.rasterizeSVGs
+
+ preference to <true>. In this case dropped SVGs also trigger the
+ droppedImage() event with a canvas containing a rasterized version of the
+ SVG.
+
+ The same applies to drops of audio or text files from outside the world
+ canvas.
+
+ Those are dispatched as
+
+ droppedAudio(anAudio, name)
+ droppedText(aString, name)
+
+ events to interested Morphs at the mouse pointer.
+
+ if none of the above content types can be determined, the file contents
+ is dispatched as an ArrayBuffer to interested Morphs:
+
+ droppedBinary(anArrayBuffer, name)
+
+
+ (e) keyboard events
+ -------------------
+ The World dispatches the following key events to its active
+ keyboardReceiver:
+
+ keypress
+ keydown
+ keyup
+
+ Currently the only morph which acts as keyboard receiver is
+ CursorMorph, the basic text editing widget. If you wish to add
+ keyboard support to your morph you need to add event handling
+ methods for
+
+ processKeyPress(event)
+ processKeyDown(event)
+ processKeyUp(event)
+
+ and activate them by assigning your morph to the World's
+
+ keyboardReceiver
+
+ property.
+
+ Note that processKeyUp() is optional and doesn't have to be present
+ if your morph doesn't require it.
+
+
+ (f) resize event
+ ----------------
+ The Window resize event is handled by the World and allows the
+ World's extent to be adjusted so that it always completely fills
+ the browser's visible page. You can turn off this default behavior
+ by setting the World's
+
+ useFillPage
+
+ property to false.
+
+ Alternatively you can also initialize the World with the
+ useFillPage switch turned off from the beginning by passing the
+ false value as second parameter to the World's constructor:
+
+ world = new World(aCanvas, false);
+
+ Use this when creating a web page with multiple Worlds.
+
+ if "useFillPage" is turned on the World dispatches an
+
+ reactToWorldResize(newBounds)
+
+ events to all of its children (toplevel only), allowing each to
+ adjust to the new World bounds by implementing a corresponding
+ method, the passed argument being the World's new dimensions after
+ completing the resize. By default, the "reactToWorldResize" Method
+ does not exist.
+
+ Example:
+
+ Add the following method to your Morph to let it automatically
+ fill the whole World, but leave a 10 pixel border uncovered:
+
+ MyMorph.prototype.reactToWorldResize = function (rect) {
+ this.changed();
+ this.bounds = rect.insetBy(10);
+ this.drawNew();
+ this.changed();
+ };
+
+
+ (g) combined mouse-keyboard events
+ ----------------------------------
+ Occasionally you'll want an object to react differently to a mouse
+ click or to some other mouse event while the user holds down a key
+ on the keyboard. Such "shift-click", "ctl-click", or "alt-click"
+ events can be implemented by querying the World's
+
+ currentKey
+
+ property inside the function that reacts to the mouse event. This
+ property stores the keyCode of the key that's currently pressed.
+ Once the key is released by the user it reverts to null.
+
+
+ (h) text editing events
+ -----------------------
+ Much of Morphic's "liveliness" comes out of allowing text elements
+ (instances of either single-lined StringMorph or multi-lined TextMorph)
+ to be directly manipulated and edited by users. This requires other
+ objects which may have an interest in the text element's state to react
+ appropriately. Therefore text elements and their manipulators emit
+ a stream of events, mostly by "bubbling" them up the text element's
+ owner chain. Text elements' parents are notified about the following
+ events:
+
+ Whenever the user presses a key on the keyboard while a text element
+ is being edited, a
+
+ reactToKeystroke(event)
+
+ is escalated up its parent chain, the "event" parameter being the
+ original one received by the World.
+
+ Once the user has completed the edit, the following events are
+ dispatched:
+
+ accept() - <enter> was pressed on a single line of text
+ cancel() - <esc> was pressed on any text element
+
+ Note that "accept" only gets triggered by single-line texte elements,
+ as the <enter> key is used to insert line breaks in multi-line
+ elements. Therefore, whenever a text edit is terminated by the user
+ (accepted, cancelled or otherwise),
+
+ reactToEdit(StringOrTextMorph)
+
+ is triggered.
+
+ If the MorphicPreference's
+
+ useSliderForInput
+
+ setting is turned on, a slider is popped up underneath the currently
+ edited text element letting the user insert numbers out of the given
+ slider range. Whenever this happens, i.e. whenever the slider is moved
+ or while the slider button is pressed, a stream of
+
+ reactToSliderEdit(StringOrTextMorph)
+
+ events is dispatched, allowing for "Bret-Victor" style "live coding"
+ applications.
+
+ In addition to user-initiated events text elements also emit
+ change notifications to their direct parents whenever their drawNew()
+ method is invoked. That way complex Morphs containing text elements
+ get a chance to react if something about the embedded text has been
+ modified programmatically. These events are:
+
+ layoutChanged() - sent from instances of TextMorph
+ fixLayout() - sent from instances of StringMorph
+
+ they are different so that Morphs which contain both multi-line and
+ single-line text elements can hold them apart.
+
+
+ (4) stepping
+ ------------
+ Stepping is what makes Morphic "magical". Two properties control
+ a morph's stepping behavior: the fps attribute and the step()
+ method.
+
+ By default the
+
+ step()
+
+ method does nothing. As you can see in the examples of BouncerMorph
+ and MouseSensorMorph you can easily override this inherited method
+ to suit your needs.
+
+ By default the step() method is called once per display cycle.
+ Depending on the number of actively stepping morphs and the
+ complexity of your step() methods this can cause quite a strain on
+ your CPU, and also result in your application behaving differently
+ on slower computers than on fast ones.
+
+ setting
+
+ myMorph.fps
+
+ to a number lower than the interval for the main loop lets you free
+ system resources (albeit at the cost of a less responsive or slower
+ behavior for this particular morph).
+
+
+ (5) creating new kinds of morphs
+ --------------------------------
+ The real fun begins when you start to create new kinds of morphs
+ with customized shapes. Imagine, e.g. jigsaw puzzle pieces or
+ musical notes. For this you have to override the default
+
+ drawNew()
+
+ method.
+
+ This method creates a new offscreen Canvas and stores it in
+ the morph's
+
+ image
+
+ property.
+
+ Use the following template for a start:
+
+ MyMorph.prototype.drawNew = function() {
+ var context;
+ this.image = newCanvas(this.extent());
+ context = this.image.getContext('2d');
+ // use context to paint stuff here
+ };
+
+ If your new morph stores or references other morphs outside of the
+ submorph tree in other properties, be sure to also override the
+ default
+
+ copyRecordingReferences()
+
+ method accordingly if you want it to support duplication.
+
+
+ (6) development and user modes
+ ------------------------------
+ When working with Squeak on Scratch or BYOB among the features I
+ like the best and use the most is inspecting what's going on in
+ the World while it is up and running. That's what development mode
+ is for (you could also call it debug mode). In essence development
+ mode controls which context menu shows up. In user mode right
+ clicking (or double finger tapping) a morph invokes its
+
+ customContextMenu
+
+ property, whereas in development mode only the general
+
+ developersMenu()
+
+ method is called and the resulting menu invoked. The developers'
+ menu features Gui-Builder-wise functionality to directly inspect,
+ take apart, reassamble and otherwise manipulate morphs and their
+ contents.
+
+ Instead of using the "customContextMenu" property you can also
+ assign a more dynamic contextMenu by overriding the general
+
+ userMenu()
+
+ method with a customized menu constructor. The difference between
+ the customContextMenu property and the userMenu() method is that
+ the former is also present in development mode and overrides the
+ developersMenu() result. For an example of how to use the
+ customContextMenu property have a look at TextMorph's evaluation
+ menu, which is used for the Inspector's evaluation pane.
+
+ When in development mode you can inspect every Morph's properties
+ with the inspector, including all of its methods. The inspector
+ also lets you add, remove and rename properties, and even edit
+ their values at runtime. Like in a Smalltalk environment the inspect
+ features an evaluation pane into which you can type in arbitrary
+ JavaScript code and evaluate it in the context of the inspectee.
+
+ Use switching between user and development modes while you are
+ developing an application and disable switching to development once
+ you're done and deploying, because generally you don't want to
+ confuse end-users with inspectors and meta-level stuff.
+
+
+ (7) turtle graphics
+ -------------------
+
+ The basic Morphic kernel features a simple LOGO turtle constructor
+ called
+
+ PenMorph
+
+ which you can use to draw onto its parent Morph. By default every
+ Morph in the system (including the World) is able to act as turtle
+ canvas and can display pen trails. Pen trails will be lost whenever
+ the trails morph (the pen's parent) performs a "drawNew()"
+ operation. If you want to create your own pen trails canvas, you
+ may wish to modify its
+
+ penTrails()
+
+ property, so that it keeps a separate offscreen canvas for pen
+ trails (and doesn't loose these on redraw).
+
+ the following properties of PenMorph are relevant for turtle
+ graphics:
+
+ color - a Color
+ size - line width of pen trails
+ heading - degrees
+ isDown - drawing state
+
+ the following commands can be used to actually draw something:
+
+ up() - lift the pen up, further movements leave no trails
+ down() - set down, further movements leave trails
+ clear() - remove all trails from the current parent
+ forward(n) - move n steps in the current direction (heading)
+ turn(n) - turn right n degrees
+
+ Turtle graphics can best be explored interactively by creating a
+ new PenMorph object and by manipulating it with the inspector
+ widget.
+
+ NOTE: PenMorph has a special optimization for recursive operations
+ called
+
+ warp(function)
+
+ You can significantly speed up recursive ops and increase the depth
+ of recursion that's displayable by wrapping WARP around your
+ recursive function call:
+
+ example:
+
+ myPen.warp(function () {
+ myPen.tree(12, 120, 20);
+ })
+
+ will be much faster than just invoking the tree function, because it
+ prevents the parent's parent from keeping track of every single line
+ segment and instead redraws the outcome in a single pass.
+
+
+ (8) damage list housekeeping
+ ----------------------------
+ Morphic's progressive display update comes at the cost of having to
+ cycle through a list of "broken rectangles" every display cycle. If
+ this list gets very long working this damage list can lead to a
+ seemingly dramatic slow-down of the Morphic system. Typically this
+ occurs when updating the layout of complex Morphs with very many
+ submorphs, e.g. when resizing an inspector window.
+
+ An effective strategy to cope with this is to use the inherited
+
+ trackChanges
+
+ property of the Morph prototype for damage list housekeeping.
+
+ The trackChanges property of the Morph prototype is a Boolean switch
+ that determines whether the World's damage list ('broken' rectangles)
+ tracks changes. By default the switch is always on. If set to false
+ changes are not stored. This can be very useful for housekeeping of
+ the damage list in situations where a large number of (sub-) morphs
+ are changed more or less at once. Instead of keeping track of every
+ single submorph's changes tremendous performance improvements can be
+ achieved by setting the trackChanges flag to false before propagating
+ the layout changes, setting it to true again and then storing the full
+ bounds of the surrounding morph. An an example refer to the
+
+ moveBy()
+
+ method of HandMorph, and to the
+
+ fixLayout()
+
+ method of InspectorMorph, or the
+
+ startLayout()
+ endLayout()
+
+ methods of SyntaxElementMorph in the Snap application.
+
+
+ (9) minifying morphic.js
+ ------------------------
+ Coming from Smalltalk and being a Squeaker at heart I am a huge fan
+ of browsing the code itself to make sense of it. Therefore I have
+ included this documentation and (too little) inline comments so all
+ you need to get going is this very file.
+
+ Nowadays with live streaming HD video even on mobile phones 250 KB
+ shouldn't be a big strain on bandwith, still minifying and even
+ compressing morphic.js down do about 100 KB may sometimes improve
+ performance in production use.
+
+ Being an attorney-at-law myself you programmer folk keep harassing
+ me with rabulistic nitpickings about free software licenses. I'm
+ releasing morphic.js under an AGPL license. Therefore please make
+ sure to adhere to that license in any minified or compressed version.
+
+
+ VIII. acknowledgements
+ ----------------------
+ The original Morphic was designed and written by Randy Smith and
+ John Maloney for the SELF programming language, and later ported to
+ Squeak (Smalltalk) by John Maloney and Dan Ingalls, who has also
+ ported it to JavaScript (the Lively Kernel), once again setting
+ a "Gold Standard" for self sustaining systems which morphic.js
+ cannot and does not aspire to meet.
+
+ This Morphic implementation for JavaScript is not a direct port of
+ Squeak's Morphic, but still many individual functions have been
+ ported almost literally from Squeak, sometimes even including their
+ comments, e.g. the morph duplication mechanism fullCopy(). Squeak
+ has been a treasure trove, and if morphic.js looks, feels and
+ smells a lot like Squeak, I'll take it as a compliment.
+
+ Evelyn Eastmond has inspired and encouraged me with her wonderful
+ implementation of DesignBlocksJS. Thanks for sharing code, ideas
+ and enthusiasm for programming.
+
+ John Maloney has been my mentor and my source of inspiration for
+ these Morphic experiments. Thanks for the critique, the suggestions
+ and explanations for all things Morphic and for being my all time
+ programming hero.
+
+ I have originally written morphic.js in Florian Balmer's Notepad2
+ editor for Windows and later switched to Apple's Dashcode. I've also
+ come to depend on both Douglas Crockford's JSLint, Mozilla's Firebug
+ and Google's Chrome to get it right.
+
+
+ IX. contributors
+ ----------------------
+ Joe Otto found and fixed many early bugs and taught me some tricks.
+ Nathan Dinsmore contributed mouse wheel scrolling, cached
+ background texture handling and countless bug fixes.
+ Ian Reynolds contributed backspace key handling for Chrome.
+ Davide Della Casa contributed performance optimizations for Firefox.
+
+ - Jens Mšnig
+*/
+
+// Global settings /////////////////////////////////////////////////////
+
+/*global window, HTMLCanvasElement, getMinimumFontHeight, FileReader, Audio,
+FileList, getBlurredShadowSupport*/
+
+var morphicVersion = '2013-March-11';
+var modules = {}; // keep track of additional loaded modules
+var useBlurredShadows = getBlurredShadowSupport(); // check for Chrome-bug
+
+var standardSettings = {
+ minimumFontHeight: getMinimumFontHeight(), // browser settings
+ globalFontFamily: '',
+ menuFontName: 'sans-serif',
+ menuFontSize: 12,
+ bubbleHelpFontSize: 10,
+ prompterFontName: 'sans-serif',
+ prompterFontSize: 12,
+ prompterSliderSize: 10,
+ handleSize: 15,
+ scrollBarSize: 12,
+ mouseScrollAmount: 40,
+ useSliderForInput: false,
+ useVirtualKeyboard: true,
+ rasterizeSVGs: false
+};
+
+var touchScreenSettings = {
+ minimumFontHeight: standardSettings.minimumFontHeight,
+ globalFontFamily: '',
+ menuFontName: 'sans-serif',
+ menuFontSize: 24,
+ bubbleHelpFontSize: 18,
+ prompterFontName: 'sans-serif',
+ prompterFontSize: 24,
+ prompterSliderSize: 20,
+ handleSize: 26,
+ scrollBarSize: 24,
+ mouseScrollAmount: 40,
+ useSliderForInput: true,
+ useVirtualKeyboard: true,
+ rasterizeSVGs: false
+};
+
+var MorphicPreferences = standardSettings;
+
+// Global Functions ////////////////////////////////////////////////////
+
+function nop() {
+ // do explicitly nothing
+ return null;
+}
+
+function localize(string) {
+ // override this function with custom localizations
+ return string;
+}
+
+function isNil(thing) {
+ return thing === undefined || thing === null;
+}
+
+function contains(list, element) {
+ // answer true if element is a member of list
+ return list.some(function (any) {
+ return any === element;
+ });
+}
+
+function detect(list, predicate) {
+ // answer the first element of list for which predicate evaluates
+ // true, otherwise answer null
+ var i, size = list.length;
+ for (i = 0; i < size; i += 1) {
+ if (predicate.call(null, list[i])) {
+ return list[i];
+ }
+ }
+ return null;
+}
+
+function sizeOf(object) {
+ // answer the number of own properties
+ var size = 0, key;
+ for (key in object) {
+ if (object.hasOwnProperty(key)) {
+ size += 1;
+ }
+ }
+ return size;
+}
+
+function isString(target) {
+ return typeof target === 'string' || target instanceof String;
+}
+
+function isObject(target) {
+ return target !== null &&
+ (typeof target === 'object' || target instanceof Object);
+}
+
+function radians(degrees) {
+ return degrees * Math.PI / 180;
+}
+
+function degrees(radians) {
+ return radians * 180 / Math.PI;
+}
+
+function fontHeight(height) {
+ var minHeight = Math.max(height, MorphicPreferences.minimumFontHeight);
+ return minHeight * 1.2; // assuming 1/5 font size for ascenders
+}
+
+function newCanvas(extentPoint) {
+ // answer a new empty instance of Canvas, don't display anywhere
+ var canvas, ext;
+ ext = extentPoint || {x: 0, y: 0};
+ canvas = document.createElement('canvas');
+ canvas.width = ext.x;
+ canvas.height = ext.y;
+ return canvas;
+}
+
+function getMinimumFontHeight() {
+ // answer the height of the smallest font renderable in pixels
+ var str = 'I',
+ size = 50,
+ canvas = document.createElement('canvas'),
+ ctx,
+ maxX,
+ data,
+ x,
+ y;
+ canvas.width = size;
+ canvas.height = size;
+ ctx = canvas.getContext('2d');
+ ctx.font = '1px serif';
+ maxX = ctx.measureText(str).width;
+ ctx.fillStyle = 'black';
+ ctx.textBaseline = 'bottom';
+ ctx.fillText(str, 0, size);
+ for (y = 0; y < size; y += 1) {
+ for (x = 0; x < maxX; x += 1) {
+ data = ctx.getImageData(x, y, 1, 1);
+ if (data.data[3] !== 0) {
+ return size - y + 1;
+ }
+ }
+ }
+ return 0;
+}
+
+function getBlurredShadowSupport() {
+ // check for Chrome issue 90001
+ // http://code.google.com/p/chromium/issues/detail?id=90001
+ var source, target, ctx;
+ source = document.createElement('canvas');
+ source.width = 10;
+ source.height = 10;
+ ctx = source.getContext('2d');
+ ctx.fillStyle = 'rgb(255, 0, 0)';
+ ctx.beginPath();
+ ctx.arc(5, 5, 5, 0, Math.PI * 2, true);
+ ctx.closePath();
+ ctx.fill();
+ target = document.createElement('canvas');
+ target.width = 10;
+ target.height = 10;
+ ctx = target.getContext('2d');
+ ctx.shadowBlur = 10;
+ ctx.shadowColor = 'rgba(0, 0, 255, 1)';
+ ctx.drawImage(source, 0, 0);
+ return ctx.getImageData(0, 0, 1, 1).data[3] ? true : false;
+}
+
+function getDocumentPositionOf(aDOMelement) {
+ // answer the absolute coordinates of a DOM element in the document
+ var pos, offsetParent;
+ if (aDOMelement === null) {
+ return {x: 0, y: 0};
+ }
+ pos = {x: aDOMelement.offsetLeft, y: aDOMelement.offsetTop};
+ offsetParent = aDOMelement.offsetParent;
+ while (offsetParent !== null) {
+ pos.x += offsetParent.offsetLeft;
+ pos.y += offsetParent.offsetTop;
+ if (offsetParent !== document.body &&
+ offsetParent !== document.documentElement) {
+ pos.x -= offsetParent.scrollLeft;
+ pos.y -= offsetParent.scrollTop;
+ }
+ offsetParent = offsetParent.offsetParent;
+ }
+ return pos;
+}
+
+function clone(target) {
+ // answer a new instance of target's type
+ if (typeof target === 'object') {
+ var Clone = function () {};
+ Clone.prototype = target;
+ return new Clone();
+ }
+ return target;
+}
+
+function copy(target) {
+ // answer a shallow copy of target
+ var value, c, property;
+
+ if (typeof target !== 'object') {
+ return target;
+ }
+ value = target.valueOf();
+ if (target !== value) {
+ return new target.constructor(value);
+ }
+ if (target instanceof target.constructor &&
+ target.constructor !== Object) {
+ c = clone(target.constructor.prototype);
+ for (property in target) {
+ if (target.hasOwnProperty(property)) {
+ c[property] = target[property];
+ }
+ }
+ } else {
+ c = {};
+ for (property in target) {
+ if (!c[property]) {
+ c[property] = target[property];
+ }
+ }
+ }
+ return c;
+}
+
+// Colors //////////////////////////////////////////////////////////////
+
+// Color instance creation:
+
+function Color(r, g, b, a) {
+ // all values are optional, just (r, g, b) is fine
+ this.r = r || 0;
+ this.g = g || 0;
+ this.b = b || 0;
+ this.a = a || ((a === 0) ? 0 : 1);
+}
+
+// Color string representation: e.g. 'rgba(255,165,0,1)'
+
+Color.prototype.toString = function () {
+ return 'rgba(' +
+ Math.round(this.r) + ',' +
+ Math.round(this.g) + ',' +
+ Math.round(this.b) + ',' +
+ this.a + ')';
+};
+
+// Color copying:
+
+Color.prototype.copy = function () {
+ return new Color(
+ this.r,
+ this.g,
+ this.b,
+ this.a
+ );
+};
+
+// Color comparison:
+
+Color.prototype.eq = function (aColor) {
+ // ==
+ return aColor &&
+ this.r === aColor.r &&
+ this.g === aColor.g &&
+ this.b === aColor.b;
+};
+
+// Color conversion (hsv):
+
+Color.prototype.hsv = function () {
+ // ignore alpha
+ var max, min, h, s, v, d,
+ rr = this.r / 255,
+ gg = this.g / 255,
+ bb = this.b / 255;
+ max = Math.max(rr, gg, bb);
+ min = Math.min(rr, gg, bb);
+ h = max;
+ s = max;
+ v = max;
+ d = max - min;
+ s = max === 0 ? 0 : d / max;
+ if (max === min) {
+ h = 0;
+ } else {
+ switch (max) {
+ case rr:
+ h = (gg - bb) / d + (gg < bb ? 6 : 0);
+ break;
+ case gg:
+ h = (bb - rr) / d + 2;
+ break;
+ case bb:
+ h = (rr - gg) / d + 4;
+ break;
+ }
+ h /= 6;
+ }
+ return [h, s, v];
+};
+
+Color.prototype.set_hsv = function (h, s, v) {
+ // ignore alpha, h, s and v are to be within [0, 1]
+ var i, f, p, q, t;
+ i = Math.floor(h * 6);
+ f = h * 6 - i;
+ p = v * (1 - s);
+ q = v * (1 - f * s);
+ t = v * (1 - (1 - f) * s);
+ switch (i % 6) {
+ case 0:
+ this.r = v;
+ this.g = t;
+ this.b = p;
+ break;
+ case 1:
+ this.r = q;
+ this.g = v;
+ this.b = p;
+ break;
+ case 2:
+ this.r = p;
+ this.g = v;
+ this.b = t;
+ break;
+ case 3:
+ this.r = p;
+ this.g = q;
+ this.b = v;
+ break;
+ case 4:
+ this.r = t;
+ this.g = p;
+ this.b = v;
+ break;
+ case 5:
+ this.r = v;
+ this.g = p;
+ this.b = q;
+ break;
+ }
+
+ this.r *= 255;
+ this.g *= 255;
+ this.b *= 255;
+
+};
+
+// Color mixing:
+
+Color.prototype.mixed = function (proportion, otherColor) {
+ // answer a copy of this color mixed with another color, ignore alpha
+ var frac1 = Math.min(Math.max(proportion, 0), 1),
+ frac2 = 1 - frac1;
+ return new Color(
+ this.r * frac1 + otherColor.r * frac2,
+ this.g * frac1 + otherColor.g * frac2,
+ this.b * frac1 + otherColor.b * frac2
+ );
+};
+
+Color.prototype.darker = function (percent) {
+ // return an rgb-interpolated darker copy of me, ignore alpha
+ var fract = 0.8333;
+ if (percent) {
+ fract = (100 - percent) / 100;
+ }
+ return this.mixed(fract, new Color(0, 0, 0));
+};
+
+Color.prototype.lighter = function (percent) {
+ // return an rgb-interpolated lighter copy of me, ignore alpha
+ var fract = 0.8333;
+ if (percent) {
+ fract = (100 - percent) / 100;
+ }
+ return this.mixed(fract, new Color(255, 255, 255));
+};
+
+Color.prototype.dansDarker = function () {
+ // return an hsv-interpolated darker copy of me, ignore alpha
+ var hsv = this.hsv(),
+ result = new Color(),
+ vv = Math.max(hsv[2] - 0.16, 0);
+ result.set_hsv(hsv[0], hsv[1], vv);
+ return result;
+};
+
+// Points //////////////////////////////////////////////////////////////
+
+// Point instance creation:
+
+function Point(x, y) {
+ this.x = x || 0;
+ this.y = y || 0;
+}
+
+// Point string representation: e.g. '12@68'
+
+Point.prototype.toString = function () {
+ return Math.round(this.x.toString()) +
+ '@' + Math.round(this.y.toString());
+};
+
+// Point copying:
+
+Point.prototype.copy = function () {
+ return new Point(this.x, this.y);
+};
+
+// Point comparison:
+
+Point.prototype.eq = function (aPoint) {
+ // ==
+ return this.x === aPoint.x && this.y === aPoint.y;
+};
+
+Point.prototype.lt = function (aPoint) {
+ // <
+ return this.x < aPoint.x && this.y < aPoint.y;
+};
+
+Point.prototype.gt = function (aPoint) {
+ // >
+ return this.x > aPoint.x && this.y > aPoint.y;
+};
+
+Point.prototype.ge = function (aPoint) {
+ // >=
+ return this.x >= aPoint.x && this.y >= aPoint.y;
+};
+
+Point.prototype.le = function (aPoint) {
+ // <=
+ return this.x <= aPoint.x && this.y <= aPoint.y;
+};
+
+Point.prototype.max = function (aPoint) {
+ return new Point(Math.max(this.x, aPoint.x),
+ Math.max(this.y, aPoint.y));
+};
+
+Point.prototype.min = function (aPoint) {
+ return new Point(Math.min(this.x, aPoint.x),
+ Math.min(this.y, aPoint.y));
+};
+
+// Point conversion:
+
+Point.prototype.round = function () {
+ return new Point(Math.round(this.x), Math.round(this.y));
+};
+
+Point.prototype.abs = function () {
+ return new Point(Math.abs(this.x), Math.abs(this.y));
+};
+
+Point.prototype.neg = function () {
+ return new Point(-this.x, -this.y);
+};
+
+Point.prototype.mirror = function () {
+ return new Point(this.y, this.x);
+};
+
+Point.prototype.floor = function () {
+ return new Point(
+ Math.max(Math.floor(this.x), 0),
+ Math.max(Math.floor(this.y), 0)
+ );
+};
+
+Point.prototype.ceil = function () {
+ return new Point(Math.ceil(this.x), Math.ceil(this.y));
+};
+
+// Point arithmetic:
+
+Point.prototype.add = function (other) {
+ if (other instanceof Point) {
+ return new Point(this.x + other.x, this.y + other.y);
+ }
+ return new Point(this.x + other, this.y + other);
+};
+
+Point.prototype.subtract = function (other) {
+ if (other instanceof Point) {
+ return new Point(this.x - other.x, this.y - other.y);
+ }
+ return new Point(this.x - other, this.y - other);
+};
+
+Point.prototype.multiplyBy = function (other) {
+ if (other instanceof Point) {
+ return new Point(this.x * other.x, this.y * other.y);
+ }
+ return new Point(this.x * other, this.y * other);
+};
+
+Point.prototype.divideBy = function (other) {
+ if (other instanceof Point) {
+ return new Point(this.x / other.x, this.y / other.y);
+ }
+ return new Point(this.x / other, this.y / other);
+};
+
+Point.prototype.floorDivideBy = function (other) {
+ if (other instanceof Point) {
+ return new Point(Math.floor(this.x / other.x),
+ Math.floor(this.y / other.y));
+ }
+ return new Point(Math.floor(this.x / other),
+ Math.floor(this.y / other));
+};
+
+// Point polar coordinates:
+
+Point.prototype.r = function () {
+ var t = (this.multiplyBy(this));
+ return Math.sqrt(t.x + t.y);
+};
+
+Point.prototype.degrees = function () {
+/*
+ answer the angle I make with origin in degrees.
+ Right is 0, down is 90
+*/
+ var tan, theta;
+
+ if (this.x === 0) {
+ if (this.y >= 0) {
+ return 90;
+ }
+ return 270;
+ }
+ tan = this.y / this.x;
+ theta = Math.atan(tan);
+ if (this.x >= 0) {
+ if (this.y >= 0) {
+ return degrees(theta);
+ }
+ return 360 + (degrees(theta));
+ }
+ return 180 + degrees(theta);
+};
+
+Point.prototype.theta = function () {
+/*
+ answer the angle I make with origin in radians.
+ Right is 0, down is 90
+*/
+ var tan, theta;
+
+ if (this.x === 0) {
+ if (this.y >= 0) {
+ return radians(90);
+ }
+ return radians(270);
+ }
+ tan = this.y / this.x;
+ theta = Math.atan(tan);
+ if (this.x >= 0) {
+ if (this.y >= 0) {
+ return theta;
+ }
+ return radians(360) + theta;
+ }
+ return radians(180) + theta;
+};
+
+// Point functions:
+
+Point.prototype.crossProduct = function (aPoint) {
+ return this.multiplyBy(aPoint.mirror());
+};
+
+Point.prototype.distanceTo = function (aPoint) {
+ return (aPoint.subtract(this)).r();
+};
+
+Point.prototype.rotate = function (direction, center) {
+ // direction must be 'right', 'left' or 'pi'
+ var offset = this.subtract(center);
+ if (direction === 'right') {
+ return new Point(-offset.y, offset.y).add(center);
+ }
+ if (direction === 'left') {
+ return new Point(offset.y, -offset.y).add(center);
+ }
+ // direction === 'pi'
+ return center.subtract(offset);
+};
+
+Point.prototype.flip = function (direction, center) {
+ // direction must be 'vertical' or 'horizontal'
+ if (direction === 'vertical') {
+ return new Point(this.x, center.y * 2 - this.y);
+ }
+ // direction === 'horizontal'
+ return new Point(center.x * 2 - this.x, this.y);
+};
+
+Point.prototype.distanceAngle = function (dist, angle) {
+ var deg = angle, x, y;
+ if (deg > 270) {
+ deg = deg - 360;
+ } else if (deg < -270) {
+ deg = deg + 360;
+ }
+ if (-90 <= deg && deg <= 90) {
+ x = Math.sin(radians(deg)) * dist;
+ y = Math.sqrt((dist * dist) - (x * x));
+ return new Point(x + this.x, this.y - y);
+ }
+ x = Math.sin(radians(180 - deg)) * dist;
+ y = Math.sqrt((dist * dist) - (x * x));
+ return new Point(x + this.x, this.y + y);
+};
+
+// Point transforming:
+
+Point.prototype.scaleBy = function (scalePoint) {
+ return this.multiplyBy(scalePoint);
+};
+
+Point.prototype.translateBy = function (deltaPoint) {
+ return this.add(deltaPoint);
+};
+
+Point.prototype.rotateBy = function (angle, centerPoint) {
+ var center = centerPoint || new Point(0, 0),
+ p = this.subtract(center),
+ r = p.r(),
+ theta = angle - p.theta();
+ return new Point(
+ center.x + (r * Math.cos(theta)),
+ center.y - (r * Math.sin(theta))
+ );
+};
+
+// Point conversion:
+
+Point.prototype.asArray = function () {
+ return [this.x, this.y];
+};
+
+// Rectangles //////////////////////////////////////////////////////////
+
+// Rectangle instance creation:
+
+function Rectangle(left, top, right, bottom) {
+ this.init(new Point((left || 0), (top || 0)),
+ new Point((right || 0), (bottom || 0)));
+}
+
+Rectangle.prototype.init = function (originPoint, cornerPoint) {
+ this.origin = originPoint;
+ this.corner = cornerPoint;
+};
+
+// Rectangle string representation: e.g. '[0@0 | 160@80]'
+
+Rectangle.prototype.toString = function () {
+ return '[' + this.origin.toString() + ' | ' +
+ this.extent().toString() + ']';
+};
+
+// Rectangle copying:
+
+Rectangle.prototype.copy = function () {
+ return new Rectangle(
+ this.left(),
+ this.top(),
+ this.right(),
+ this.bottom()
+ );
+};
+
+// creating Rectangle instances from Points:
+
+Point.prototype.corner = function (cornerPoint) {
+ // answer a new Rectangle
+ return new Rectangle(
+ this.x,
+ this.y,
+ cornerPoint.x,
+ cornerPoint.y
+ );
+};
+
+Point.prototype.rectangle = function (aPoint) {
+ // answer a new Rectangle
+ var org, crn;
+ org = this.min(aPoint);
+ crn = this.max(aPoint);
+ return new Rectangle(org.x, org.y, crn.x, crn.y);
+};
+
+Point.prototype.extent = function (aPoint) {
+ //answer a new Rectangle
+ var crn = this.add(aPoint);
+ return new Rectangle(this.x, this.y, crn.x, crn.y);
+};
+
+// Rectangle accessing - setting:
+
+Rectangle.prototype.setTo = function (left, top, right, bottom) {
+ // note: all inputs are optional and can be omitted
+
+ this.origin = new Point(
+ left || ((left === 0) ? 0 : this.left()),
+ top || ((top === 0) ? 0 : this.top())
+ );
+
+ this.corner = new Point(
+ right || ((right === 0) ? 0 : this.right()),
+ bottom || ((bottom === 0) ? 0 : this.bottom())
+ );
+};
+
+// Rectangle accessing - getting:
+
+Rectangle.prototype.area = function () {
+ //requires width() and height() to be defined
+ var w = this.width();
+ if (w < 0) {
+ return 0;
+ }
+ return Math.max(w * this.height(), 0);
+};
+
+Rectangle.prototype.bottom = function () {
+ return this.corner.y;
+};
+
+Rectangle.prototype.bottomCenter = function () {
+ return new Point(this.center().x, this.bottom());
+};
+
+Rectangle.prototype.bottomLeft = function () {
+ return new Point(this.origin.x, this.corner.y);
+};
+
+Rectangle.prototype.bottomRight = function () {
+ return this.corner.copy();
+};
+
+Rectangle.prototype.boundingBox = function () {
+ return this;
+};
+
+Rectangle.prototype.center = function () {
+ return this.origin.add(
+ this.corner.subtract(this.origin).floorDivideBy(2)
+ );
+};
+
+Rectangle.prototype.corners = function () {
+ return [this.origin,
+ this.bottomLeft(),
+ this.corner,
+ this.topRight()];
+};
+
+Rectangle.prototype.extent = function () {
+ return this.corner.subtract(this.origin);
+};
+
+Rectangle.prototype.height = function () {
+ return this.corner.y - this.origin.y;
+};
+
+Rectangle.prototype.left = function () {
+ return this.origin.x;
+};
+
+Rectangle.prototype.leftCenter = function () {
+ return new Point(this.left(), this.center().y);
+};
+
+Rectangle.prototype.right = function () {
+ return this.corner.x;
+};
+
+Rectangle.prototype.rightCenter = function () {
+ return new Point(this.right(), this.center().y);
+};
+
+Rectangle.prototype.top = function () {
+ return this.origin.y;
+};
+
+Rectangle.prototype.topCenter = function () {
+ return new Point(this.center().x, this.top());
+};
+
+Rectangle.prototype.topLeft = function () {
+ return this.origin;
+};
+
+Rectangle.prototype.topRight = function () {
+ return new Point(this.corner.x, this.origin.y);
+};
+
+Rectangle.prototype.width = function () {
+ return this.corner.x - this.origin.x;
+};
+
+Rectangle.prototype.position = function () {
+ return this.origin;
+};
+
+// Rectangle comparison:
+
+Rectangle.prototype.eq = function (aRect) {
+ return this.origin.eq(aRect.origin) &&
+ this.corner.eq(aRect.corner);
+};
+
+Rectangle.prototype.abs = function () {
+ var newOrigin, newCorner;
+
+ newOrigin = this.origin.abs();
+ newCorner = this.corner.max(newOrigin);
+ return newOrigin.corner(newCorner);
+};
+
+// Rectangle functions:
+
+Rectangle.prototype.insetBy = function (delta) {
+ // delta can be either a Point or a Number
+ var result = new Rectangle();
+ result.origin = this.origin.add(delta);
+ result.corner = this.corner.subtract(delta);
+ return result;
+};
+
+Rectangle.prototype.expandBy = function (delta) {
+ // delta can be either a Point or a Number
+ var result = new Rectangle();
+ result.origin = this.origin.subtract(delta);
+ result.corner = this.corner.add(delta);
+ return result;
+};
+
+Rectangle.prototype.growBy = function (delta) {
+ // delta can be either a Point or a Number
+ var result = new Rectangle();
+ result.origin = this.origin.copy();
+ result.corner = this.corner.add(delta);
+ return result;
+};
+
+Rectangle.prototype.intersect = function (aRect) {
+ var result = new Rectangle();
+ result.origin = this.origin.max(aRect.origin);
+ result.corner = this.corner.min(aRect.corner);
+ return result;
+};
+
+Rectangle.prototype.merge = function (aRect) {
+ var result = new Rectangle();
+ result.origin = this.origin.min(aRect.origin);
+ result.corner = this.corner.max(aRect.corner);
+ return result;
+};
+
+Rectangle.prototype.round = function () {
+ return this.origin.round().corner(this.corner.round());
+};
+
+Rectangle.prototype.spread = function () {
+ // round me by applying floor() to my origin and ceil() to my corner
+ return this.origin.floor().corner(this.corner.ceil());
+};
+
+Rectangle.prototype.amountToTranslateWithin = function (aRect) {
+/*
+ Answer a Point, delta, such that self + delta is forced within
+ aRectangle. when all of me cannot be made to fit, prefer to keep
+ my topLeft inside. Taken from Squeak.
+*/
+ var dx, dy;
+
+ if (this.right() > aRect.right()) {
+ dx = aRect.right() - this.right();
+ }
+ if (this.bottom() > aRect.bottom()) {
+ dy = aRect.bottom() - this.bottom();
+ }
+ if ((this.left() + dx) < aRect.left()) {
+ dx = aRect.left() - this.right();
+ }
+ if ((this.top() + dy) < aRect.top()) {
+ dy = aRect.top() - this.top();
+ }
+ return new Point(dx, dy);
+};
+
+// Rectangle testing:
+
+Rectangle.prototype.containsPoint = function (aPoint) {
+ return this.origin.le(aPoint) && aPoint.lt(this.corner);
+};
+
+Rectangle.prototype.containsRectangle = function (aRect) {
+ return aRect.origin.gt(this.origin) &&
+ aRect.corner.lt(this.corner);
+};
+
+Rectangle.prototype.intersects = function (aRect) {
+ var ro = aRect.origin, rc = aRect.corner;
+ return (rc.x >= this.origin.x) &&
+ (rc.y >= this.origin.y) &&
+ (ro.x <= this.corner.x) &&
+ (ro.y <= this.corner.y);
+};
+
+// Rectangle transforming:
+
+Rectangle.prototype.scaleBy = function (scale) {
+ // scale can be either a Point or a scalar
+ var o = this.origin.multiplyBy(scale),
+ c = this.corner.multiplyBy(scale);
+ return new Rectangle(o.x, o.y, c.x, c.y);
+};
+
+Rectangle.prototype.translateBy = function (factor) {
+ // factor can be either a Point or a scalar
+ var o = this.origin.add(factor),
+ c = this.corner.add(factor);
+ return new Rectangle(o.x, o.y, c.x, c.y);
+};
+
+// Rectangle converting:
+
+Rectangle.prototype.asArray = function () {
+ return [this.left(), this.top(), this.right(), this.bottom()];
+};
+
+Rectangle.prototype.asArray_xywh = function () {
+ return [this.left(), this.top(), this.width(), this.height()];
+};
+
+// Nodes ///////////////////////////////////////////////////////////////
+
+// Node instance creation:
+
+function Node(parent, childrenArray) {
+ this.init(parent || null, childrenArray || []);
+}
+
+Node.prototype.init = function (parent, childrenArray) {
+ this.parent = parent || null;
+ this.children = childrenArray || [];
+};
+
+// Node string representation: e.g. 'a Node[3]'
+
+Node.prototype.toString = function () {
+ return 'a Node' + '[' + this.children.length.toString() + ']';
+};
+
+// Node accessing:
+
+Node.prototype.addChild = function (aNode) {
+ this.children.push(aNode);
+ aNode.parent = this;
+};
+
+Node.prototype.addChildFirst = function (aNode) {
+ this.children.splice(0, null, aNode);
+ aNode.parent = this;
+};
+
+Node.prototype.removeChild = function (aNode) {
+ var idx = this.children.indexOf(aNode);
+ if (idx !== -1) {
+ this.children.splice(idx, 1);
+ }
+};
+
+// Node functions:
+
+Node.prototype.root = function () {
+ if (this.parent === null) {
+ return this;
+ }
+ return this.parent.root();
+};
+
+Node.prototype.depth = function () {
+ if (this.parent === null) {
+ return 0;
+ }
+ return this.parent.depth() + 1;
+};
+
+Node.prototype.allChildren = function () {
+ // includes myself
+ var result = [this];
+ this.children.forEach(function (child) {
+ result = result.concat(child.allChildren());
+ });
+ return result;
+};
+
+Node.prototype.forAllChildren = function (aFunction) {
+ if (this.children.length > 0) {
+ this.children.forEach(function (child) {
+ child.forAllChildren(aFunction);
+ });
+ }
+ aFunction.call(null, this);
+};
+
+Node.prototype.allLeafs = function () {
+ var result = [];
+ this.allChildren().forEach(function (element) {
+ if (element.children.length === 0) {
+ result.push(element);
+ }
+ });
+ return result;
+};
+
+Node.prototype.allParents = function () {
+ // includes myself
+ var result = [this];
+ if (this.parent !== null) {
+ result = result.concat(this.parent.allParents());
+ }
+ return result;
+};
+
+Node.prototype.siblings = function () {
+ var myself = this;
+ if (this.parent === null) {
+ return [];
+ }
+ return this.parent.children.filter(function (child) {
+ return child !== myself;
+ });
+};
+
+Node.prototype.parentThatIsA = function (constructor) {
+ // including myself
+ if (this instanceof constructor) {
+ return this;
+ }
+ if (!this.parent) {
+ return null;
+ }
+ return this.parent.parentThatIsA(constructor);
+};
+
+Node.prototype.parentThatIsAnyOf = function (constructors) {
+ // including myself
+ var yup = false,
+ myself = this;
+ constructors.forEach(function (each) {
+ if (myself.constructor === each) {
+ yup = true;
+ return;
+ }
+ });
+ if (yup) {
+ return this;
+ }
+ if (!this.parent) {
+ return null;
+ }
+ return this.parent.parentThatIsAnyOf(constructors);
+};
+
+// Morphs //////////////////////////////////////////////////////////////
+
+// Morph: referenced constructors
+
+var Morph;
+var WorldMorph;
+var HandMorph;
+var ShadowMorph;
+var FrameMorph;
+var MenuMorph;
+var HandleMorph;
+var StringFieldMorph;
+var ColorPickerMorph;
+var SliderMorph;
+var ScrollFrameMorph;
+var InspectorMorph;
+var StringMorph;
+var TextMorph;
+
+// Morph inherits from Node:
+
+Morph.prototype = new Node();
+Morph.prototype.constructor = Morph;
+Morph.uber = Node.prototype;
+
+// Morph settings:
+
+/*
+ damage list housekeeping
+
+ the trackChanges property of the Morph prototype is a Boolean switch
+ that determines whether the World's damage list ('broken' rectangles)
+ tracks changes. By default the switch is always on. If set to false
+ changes are not stored. This can be very useful for housekeeping of
+ the damage list in situations where a large number of (sub-) morphs
+ are changed more or less at once. Instead of keeping track of every
+ single submorph's changes tremendous performance improvements can be
+ achieved by setting the trackChanges flag to false before propagating
+ the layout changes, setting it to true again and then storing the full
+ bounds of the surrounding morph. An an example refer to the
+
+ fixLayout()
+
+ method of InspectorMorph, or the
+
+ startLayout()
+ endLayout()
+
+ methods of SyntaxElementMorph in the Snap application.
+*/
+
+Morph.prototype.trackChanges = true;
+Morph.prototype.shadowBlur = 4;
+
+// Morph instance creation:
+
+function Morph() {
+ this.init();
+}
+
+// Morph initialization:
+
+Morph.prototype.init = function () {
+ Morph.uber.init.call(this);
+ this.isMorph = true;
+ this.bounds = new Rectangle(0, 0, 50, 40);
+ this.color = new Color(80, 80, 80);
+ this.texture = null; // optional url of a fill-image
+ this.cachedTexture = null; // internal cache of actual bg image
+ this.alpha = 1;
+ this.isVisible = true;
+ this.isDraggable = false;
+ this.isTemplate = false;
+ this.acceptsDrops = false;
+ this.noticesTransparentClick = false;
+ this.drawNew();
+ this.fps = 0;
+ this.customContextMenu = null;
+ this.lastTime = Date.now();
+ this.onNextStep = null; // optional function to be run once
+};
+
+// Morph string representation: e.g. 'a Morph 2 [20@45 | 130@250]'
+
+Morph.prototype.toString = function () {
+ return 'a ' +
+ (this.constructor.name ||
+ this.constructor.toString().split(' ')[1].split('(')[0]) +
+ ' ' +
+ this.children.length.toString() + ' ' +
+ this.bounds;
+};
+
+// Morph deleting:
+
+Morph.prototype.destroy = function () {
+ if (this.parent !== null) {
+ this.fullChanged();
+ this.parent.removeChild(this);
+ }
+};
+
+// Morph stepping:
+
+Morph.prototype.stepFrame = function () {
+ if (!this.step) {
+ return null;
+ }
+ var current, elapsed, leftover, nxt;
+ current = Date.now();
+ elapsed = current - this.lastTime;
+ if (this.fps > 0) {
+ leftover = (1000 / this.fps) - elapsed;
+ } else {
+ leftover = 0;
+ }
+ if (leftover < 1) {
+ this.lastTime = current;
+ if (this.onNextStep) {
+ nxt = this.onNextStep;
+ this.onNextStep = null;
+ nxt.call(this);
+ }
+ this.step();
+ this.children.forEach(function (child) {
+ child.stepFrame();
+ });
+ }
+};
+
+Morph.prototype.nextSteps = function (arrayOfFunctions) {
+ var lst = arrayOfFunctions || [],
+ nxt = lst.shift(),
+ myself = this;
+ if (nxt) {
+ this.onNextStep = function () {
+ nxt.call(myself);
+ myself.nextSteps(lst);
+ };
+ }
+};
+
+Morph.prototype.step = function () {
+ nop();
+};
+
+// Morph accessing - geometry getting:
+
+Morph.prototype.left = function () {
+ return this.bounds.left();
+};
+
+Morph.prototype.right = function () {
+ return this.bounds.right();
+};
+
+Morph.prototype.top = function () {
+ return this.bounds.top();
+};
+
+Morph.prototype.bottom = function () {
+ return this.bounds.bottom();
+};
+
+Morph.prototype.center = function () {
+ return this.bounds.center();
+};
+
+Morph.prototype.bottomCenter = function () {
+ return this.bounds.bottomCenter();
+};
+
+Morph.prototype.bottomLeft = function () {
+ return this.bounds.bottomLeft();
+};
+
+Morph.prototype.bottomRight = function () {
+ return this.bounds.bottomRight();
+};
+
+Morph.prototype.boundingBox = function () {
+ return this.bounds;
+};
+
+Morph.prototype.corners = function () {
+ return this.bounds.corners();
+};
+
+Morph.prototype.leftCenter = function () {
+ return this.bounds.leftCenter();
+};
+
+Morph.prototype.rightCenter = function () {
+ return this.bounds.rightCenter();
+};
+
+Morph.prototype.topCenter = function () {
+ return this.bounds.topCenter();
+};
+
+Morph.prototype.topLeft = function () {
+ return this.bounds.topLeft();
+};
+
+Morph.prototype.topRight = function () {
+ return this.bounds.topRight();
+};
+Morph.prototype.position = function () {
+ return this.bounds.origin;
+};
+
+Morph.prototype.extent = function () {
+ return this.bounds.extent();
+};
+
+Morph.prototype.width = function () {
+ return this.bounds.width();
+};
+
+Morph.prototype.height = function () {
+ return this.bounds.height();
+};
+
+Morph.prototype.fullBounds = function () {
+ var result;
+ result = this.bounds;
+ this.children.forEach(function (child) {
+ if (child.isVisible) {
+ result = result.merge(child.fullBounds());
+ }
+ });
+ return result;
+};
+
+Morph.prototype.fullBoundsNoShadow = function () {
+ // answer my full bounds but ignore any shadow
+ var result;
+ result = this.bounds;
+ this.children.forEach(function (child) {
+ if (!(child instanceof ShadowMorph) && (child.isVisible)) {
+ result = result.merge(child.fullBounds());
+ }
+ });
+ return result;
+};
+
+Morph.prototype.visibleBounds = function () {
+ // answer which part of me is not clipped by a Frame
+ var visible = this.bounds,
+ frames = this.allParents().filter(function (p) {
+ return p instanceof FrameMorph;
+ });
+ frames.forEach(function (f) {
+ visible = visible.intersect(f.bounds);
+ });
+ return visible;
+};
+
+// Morph accessing - simple changes:
+
+Morph.prototype.moveBy = function (delta) {
+ this.changed();
+ this.bounds = this.bounds.translateBy(delta);
+ this.children.forEach(function (child) {
+ child.moveBy(delta);
+ });
+ this.changed();
+};
+
+Morph.prototype.silentMoveBy = function (delta) {
+ this.bounds = this.bounds.translateBy(delta);
+ this.children.forEach(function (child) {
+ child.silentMoveBy(delta);
+ });
+};
+
+Morph.prototype.setPosition = function (aPoint) {
+ var delta = aPoint.subtract(this.topLeft());
+ if ((delta.x !== 0) || (delta.y !== 0)) {
+ this.moveBy(delta);
+ }
+};
+
+Morph.prototype.silentSetPosition = function (aPoint) {
+ var delta = aPoint.subtract(this.topLeft());
+ if ((delta.x !== 0) || (delta.y !== 0)) {
+ this.silentMoveBy(delta);
+ }
+};
+
+Morph.prototype.setLeft = function (x) {
+ this.setPosition(
+ new Point(
+ x,
+ this.top()
+ )
+ );
+};
+
+Morph.prototype.setRight = function (x) {
+ this.setPosition(
+ new Point(
+ x - this.width(),
+ this.top()
+ )
+ );
+};
+
+Morph.prototype.setTop = function (y) {
+ this.setPosition(
+ new Point(
+ this.left(),
+ y
+ )
+ );
+};
+
+Morph.prototype.setBottom = function (y) {
+ this.setPosition(
+ new Point(
+ this.left(),
+ y - this.height()
+ )
+ );
+};
+
+Morph.prototype.setCenter = function (aPoint) {
+ this.setPosition(
+ aPoint.subtract(
+ this.extent().floorDivideBy(2)
+ )
+ );
+};
+
+Morph.prototype.setFullCenter = function (aPoint) {
+ this.setPosition(
+ aPoint.subtract(
+ this.fullBounds().extent().floorDivideBy(2)
+ )
+ );
+};
+
+Morph.prototype.keepWithin = function (aMorph) {
+ // make sure I am completely within another Morph's bounds
+ var leftOff, rightOff, topOff, bottomOff;
+ leftOff = this.fullBounds().left() - aMorph.left();
+ if (leftOff < 0) {
+ this.moveBy(new Point(-leftOff, 0));
+ }
+ rightOff = this.fullBounds().right() - aMorph.right();
+ if (rightOff > 0) {
+ this.moveBy(new Point(-rightOff, 0));
+ }
+ topOff = this.fullBounds().top() - aMorph.top();
+ if (topOff < 0) {
+ this.moveBy(new Point(0, -topOff));
+ }
+ bottomOff = this.fullBounds().bottom() - aMorph.bottom();
+ if (bottomOff > 0) {
+ this.moveBy(new Point(0, -bottomOff));
+ }
+};
+
+// Morph accessing - dimensional changes requiring a complete redraw
+
+Morph.prototype.setExtent = function (aPoint) {
+ if (!aPoint.eq(this.extent())) {
+ this.changed();
+ this.silentSetExtent(aPoint);
+ this.changed();
+ this.drawNew();
+ }
+};
+
+Morph.prototype.silentSetExtent = function (aPoint) {
+ var ext, newWidth, newHeight;
+ ext = aPoint.round();
+ newWidth = Math.max(ext.x, 0);
+ newHeight = Math.max(ext.y, 0);
+ this.bounds.corner = new Point(
+ this.bounds.origin.x + newWidth,
+ this.bounds.origin.y + newHeight
+ );
+};
+
+Morph.prototype.setWidth = function (width) {
+ this.setExtent(new Point(width || 0, this.height()));
+};
+
+Morph.prototype.silentSetWidth = function (width) {
+ // do not drawNew() just yet
+ var w = Math.max(Math.round(width || 0), 0);
+ this.bounds.corner = new Point(
+ this.bounds.origin.x + w,
+ this.bounds.corner.y
+ );
+};
+
+Morph.prototype.setHeight = function (height) {
+ this.setExtent(new Point(this.width(), height || 0));
+};
+
+Morph.prototype.silentSetHeight = function (height) {
+ // do not drawNew() just yet
+ var h = Math.max(Math.round(height || 0), 0);
+ this.bounds.corner = new Point(
+ this.bounds.corner.x,
+ this.bounds.origin.y + h
+ );
+};
+
+Morph.prototype.setColor = function (aColor) {
+ if (aColor) {
+ if (!this.color.eq(aColor)) {
+ this.color = aColor;
+ this.changed();
+ this.drawNew();
+ }
+ }
+};
+
+// Morph displaying:
+
+Morph.prototype.drawNew = function () {
+ // initialize my surface property
+ this.image = newCanvas(this.extent());
+ var context = this.image.getContext('2d');
+ context.fillStyle = this.color.toString();
+ context.fillRect(0, 0, this.width(), this.height());
+ if (this.cachedTexture) {
+ this.drawCachedTexture();
+ } else if (this.texture) {
+ this.drawTexture(this.texture);
+ }
+};
+
+Morph.prototype.drawTexture = function (url) {
+ var myself = this;
+ this.cachedTexture = new Image();
+ this.cachedTexture.onload = function () {
+ myself.drawCachedTexture();
+ };
+ this.cachedTexture.src = this.texture = url; // make absolute
+};
+
+Morph.prototype.drawCachedTexture = function () {
+ var bg = this.cachedTexture,
+ cols = Math.floor(this.image.width / bg.width),
+ lines = Math.floor(this.image.height / bg.height),
+ x,
+ y,
+ context = this.image.getContext('2d');
+
+ for (y = 0; y <= lines; y += 1) {
+ for (x = 0; x <= cols; x += 1) {
+ context.drawImage(bg, x * bg.width, y * bg.height);
+ }
+ }
+ this.changed();
+};
+
+/*
+Morph.prototype.drawCachedTexture = function () {
+ var context = this.image.getContext('2d'),
+ pattern = context.createPattern(this.cachedTexture, 'repeat');
+ context.fillStyle = pattern;
+ context.fillRect(0, 0, this.image.width, this.image.height);
+ this.changed();
+};
+*/
+
+Morph.prototype.drawOn = function (aCanvas, aRect) {
+ var rectangle, area, delta, src, context, w, h, sl, st;
+ if (!this.isVisible) {
+ return null;
+ }
+ rectangle = aRect || this.bounds();
+ area = rectangle.intersect(this.bounds).round();
+ if (area.extent().gt(new Point(0, 0))) {
+ delta = this.position().neg();
+ src = area.copy().translateBy(delta).round();
+ context = aCanvas.getContext('2d');
+ context.globalAlpha = this.alpha;
+
+ sl = src.left();
+ st = src.top();
+ w = Math.min(src.width(), this.image.width - sl);
+ h = Math.min(src.height(), this.image.height - st);
+
+ if (w < 1 || h < 1) {
+ return null;
+ }
+
+ context.drawImage(
+ this.image,
+ src.left(),
+ src.top(),
+ w,
+ h,
+ area.left(),
+ area.top(),
+ w,
+ h
+ );
+
+ /* "for debugging purposes:"
+
+ try {
+ context.drawImage(
+ this.image,
+ src.left(),
+ src.top(),
+ w,
+ h,
+ area.left(),
+ area.top(),
+ w,
+ h
+ );
+ } catch (err) {
+ alert('internal error\n\n' + err
+ + '\n ---'
+ + '\n canvas: ' + aCanvas
+ + '\n canvas.width: ' + aCanvas.width
+ + '\n canvas.height: ' + aCanvas.height
+ + '\n ---'
+ + '\n image: ' + this.image
+ + '\n image.width: ' + this.image.width
+ + '\n image.height: ' + this.image.height
+ + '\n ---'
+ + '\n w: ' + w
+ + '\n h: ' + h
+ + '\n sl: ' + sl
+ + '\n st: ' + st
+ + '\n area.left: ' + area.left()
+ + '\n area.top ' + area.top()
+ );
+ }
+ */
+
+ }
+};
+
+Morph.prototype.fullDrawOn = function (aCanvas, aRect) {
+ var rectangle;
+ if (!this.isVisible) {
+ return null;
+ }
+ rectangle = aRect || this.fullBounds();
+ this.drawOn(aCanvas, rectangle);
+ this.children.forEach(function (child) {
+ child.fullDrawOn(aCanvas, rectangle);
+ });
+};
+
+Morph.prototype.hide = function () {
+ this.isVisible = false;
+ this.changed();
+ this.children.forEach(function (child) {
+ child.hide();
+ });
+};
+
+Morph.prototype.show = function () {
+ this.isVisible = true;
+ this.changed();
+ this.children.forEach(function (child) {
+ child.show();
+ });
+};
+
+Morph.prototype.toggleVisibility = function () {
+ this.isVisible = (!this.isVisible);
+ this.changed();
+ this.children.forEach(function (child) {
+ child.toggleVisibility();
+ });
+};
+
+// Morph full image:
+
+Morph.prototype.fullImageClassic = function () {
+ // why doesn't this work for all Morphs?
+ var fb = this.fullBounds(),
+ img = newCanvas(fb.extent()),
+ ctx = img.getContext('2d');
+ ctx.translate(-this.bounds.origin.x, -this.bounds.origin.y);
+ this.fullDrawOn(img, fb);
+ img.globalAlpha = this.alpha;
+ return img;
+};
+
+Morph.prototype.fullImage = function () {
+ var img, ctx, fb;
+ img = newCanvas(this.fullBounds().extent());
+ ctx = img.getContext('2d');
+ fb = this.fullBounds();
+ this.allChildren().forEach(function (morph) {
+ if (morph.isVisible) {
+ ctx.globalAlpha = morph.alpha;
+ ctx.drawImage(
+ morph.image,
+ morph.bounds.origin.x - fb.origin.x,
+ morph.bounds.origin.y - fb.origin.y
+ );
+ }
+ });
+ return img;
+};
+
+// Morph shadow:
+
+Morph.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.fullBounds().extent();
+ img = this.fullImage();
+ 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;
+};
+
+Morph.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.fullBounds().extent().add(blur * 2);
+ img = this.fullImage();
+ 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;
+};
+
+Morph.prototype.shadow = function (off, a, color) {
+ var shadow = new ShadowMorph(),
+ offset = off || new Point(7, 7),
+ alpha = a || ((a === 0) ? 0 : 0.2),
+ fb = this.fullBounds();
+ shadow.setExtent(fb.extent().add(this.shadowBlur * 2));
+ if (useBlurredShadows) {
+ shadow.image = this.shadowImageBlurred(offset, color);
+ shadow.alpha = alpha;
+ shadow.setPosition(fb.origin.add(offset).subtract(this.shadowBlur));
+ } else {
+ shadow.image = this.shadowImage(offset, color);
+ shadow.alpha = alpha;
+ shadow.setPosition(fb.origin.add(offset));
+ }
+ return shadow;
+};
+
+Morph.prototype.addShadow = function (off, a, color) {
+ var shadow,
+ offset = off || new Point(7, 7),
+ alpha = a || ((a === 0) ? 0 : 0.2);
+ shadow = this.shadow(offset, alpha, color);
+ this.addBack(shadow);
+ this.fullChanged();
+ return shadow;
+};
+
+Morph.prototype.getShadow = function () {
+ var shadows;
+ shadows = this.children.slice(0).reverse().filter(
+ function (child) {
+ return child instanceof ShadowMorph;
+ }
+ );
+ if (shadows.length !== 0) {
+ return shadows[0];
+ }
+ return null;
+};
+
+Morph.prototype.removeShadow = function () {
+ var shadow = this.getShadow();
+ if (shadow !== null) {
+ this.fullChanged();
+ this.removeChild(shadow);
+ }
+};
+
+// Morph pen trails:
+
+Morph.prototype.penTrails = function () {
+ // answer my pen trails canvas. default is to answer my image
+ return this.image;
+};
+
+// Morph updating:
+
+Morph.prototype.changed = function () {
+ if (this.trackChanges) {
+ var w = this.root();
+ if (w instanceof WorldMorph) {
+ w.broken.push(this.visibleBounds().spread());
+ }
+ }
+ if (this.parent) {
+ this.parent.childChanged(this);
+ }
+};
+
+Morph.prototype.fullChanged = function () {
+ if (this.trackChanges) {
+ var w = this.root();
+ if (w instanceof WorldMorph) {
+ w.broken.push(this.fullBounds().spread());
+ }
+ }
+};
+
+Morph.prototype.childChanged = function () {
+ // react to a change in one of my children,
+ // default is to just pass this message on upwards
+ // override this method for Morphs that need to adjust accordingly
+ if (this.parent) {
+ this.parent.childChanged(this);
+ }
+};
+
+// Morph accessing - structure:
+
+Morph.prototype.world = function () {
+ var root = this.root();
+ if (root instanceof WorldMorph) {
+ return root;
+ }
+ if (root instanceof HandMorph) {
+ return root.world;
+ }
+ return null;
+};
+
+Morph.prototype.add = function (aMorph) {
+ var owner = aMorph.parent;
+ if (owner !== null) {
+ owner.removeChild(aMorph);
+ }
+ this.addChild(aMorph);
+};
+
+Morph.prototype.addBack = function (aMorph) {
+ var owner = aMorph.parent;
+ if (owner !== null) {
+ owner.removeChild(aMorph);
+ }
+ this.addChildFirst(aMorph);
+};
+
+Morph.prototype.topMorphSuchThat = function (predicate) {
+ var next;
+ if (predicate.call(null, this)) {
+ next = detect(
+ this.children.slice(0).reverse(),
+ predicate
+ );
+ if (next) {
+ return next.topMorphSuchThat(predicate);
+ }
+ return this;
+ }
+ return null;
+};
+
+Morph.prototype.morphAt = function (aPoint) {
+ var morphs = this.allChildren().slice(0).reverse(),
+ result = null;
+ morphs.forEach(function (m) {
+ if (m.fullBounds().containsPoint(aPoint) &&
+ (result === null)) {
+ result = m;
+ }
+ });
+ return result;
+};
+
+/*
+ alternative - more elegant and possibly more
+ performant - solution for morphAt.
+ Has some issues, commented out for now
+
+Morph.prototype.morphAt = function (aPoint) {
+ return this.topMorphSuchThat(function (m) {
+ return m.fullBounds().containsPoint(aPoint);
+ });
+};
+*/
+
+Morph.prototype.overlappedMorphs = function () {
+ //exclude the World
+ var world = this.world(),
+ fb = this.fullBounds(),
+ myself = this,
+ allParents = this.allParents(),
+ allChildren = this.allChildren(),
+ morphs;
+
+ morphs = world.allChildren();
+ return morphs.filter(function (m) {
+ return m.isVisible &&
+ m !== myself &&
+ m !== world &&
+ !contains(allParents, m) &&
+ !contains(allChildren, m) &&
+ m.fullBounds().intersects(fb);
+ });
+};
+
+// Morph pixel access:
+
+Morph.prototype.getPixelColor = function (aPoint) {
+ var point, context, data;
+ point = aPoint.subtract(this.bounds.origin);
+ context = this.image.getContext('2d');
+ data = context.getImageData(point.x, point.y, 1, 1);
+ return new Color(
+ data.data[0],
+ data.data[1],
+ data.data[2],
+ data.data[3]
+ );
+};
+
+Morph.prototype.isTransparentAt = function (aPoint) {
+ var point, context, data;
+ if (this.bounds.containsPoint(aPoint)) {
+ if (this.texture) {
+ return false;
+ }
+ point = aPoint.subtract(this.bounds.origin);
+ context = this.image.getContext('2d');
+ data = context.getImageData(
+ Math.floor(point.x),
+ Math.floor(point.y),
+ 1,
+ 1
+ );
+ return data.data[3] === 0;
+ }
+ return false;
+};
+
+// Morph duplicating:
+
+Morph.prototype.copy = function () {
+ var c = copy(this);
+ c.parent = null;
+ c.children = [];
+ c.bounds = this.bounds.copy();
+ return c;
+};
+
+Morph.prototype.fullCopy = function () {
+ /*
+ Produce a copy of me with my entire tree of submorphs. Morphs
+ mentioned more than once are all directed to a single new copy.
+ Other properties are also *shallow* copied, so you must override
+ to deep copy Arrays and (complex) Objects
+ */
+ var dict = {}, c;
+ c = this.copyRecordingReferences(dict);
+ c.forAllChildren(function (m) {
+ m.updateReferences(dict);
+ });
+ return c;
+};
+
+Morph.prototype.copyRecordingReferences = function (dict) {
+ /*
+ Recursively copy this entire composite morph, recording the
+ correspondence between old and new morphs in the given dictionary.
+ This dictionary will be used to update intra-composite references
+ in the copy. See updateReferences().
+ Note: This default implementation copies ONLY morphs in the
+ submorph hierarchy. If a morph stores morphs in other properties
+ that it wants to copy, then it should override this method to do so.
+ The same goes for morphs that contain other complex data that
+ should be copied when the morph is duplicated.
+ */
+ var c = this.copy();
+ dict[this] = c;
+ this.children.forEach(function (m) {
+ c.add(m.copyRecordingReferences(dict));
+ });
+ return c;
+};
+
+Morph.prototype.updateReferences = function (dict) {
+ /*
+ Update intra-morph references within a composite morph that has
+ been copied. For example, if a button refers to morph X in the
+ orginal composite then the copy of that button in the new composite
+ should refer to the copy of X in new composite, not the original X.
+ */
+ var property;
+ for (property in this) {
+ if (property.isMorph && dict[property]) {
+ this[property] = dict[property];
+ }
+ }
+};
+
+// Morph dragging and dropping:
+
+Morph.prototype.rootForGrab = function () {
+ if (this instanceof ShadowMorph) {
+ return this.parent.rootForGrab();
+ }
+ if (this.parent instanceof ScrollFrameMorph) {
+ return this.parent;
+ }
+ if (this.parent === null ||
+ this.parent instanceof WorldMorph ||
+ this.parent instanceof FrameMorph ||
+ this.isDraggable === true) {
+ return this;
+ }
+ return this.parent.rootForGrab();
+};
+
+Morph.prototype.wantsDropOf = function (aMorph) {
+ // default is to answer the general flag - change for my heirs
+ if ((aMorph instanceof HandleMorph) ||
+ (aMorph instanceof MenuMorph) ||
+ (aMorph instanceof InspectorMorph)) {
+ return false;
+ }
+ return this.acceptsDrops;
+};
+
+Morph.prototype.pickUp = function (wrrld) {
+ var world = wrrld || this.world();
+ this.setPosition(
+ world.hand.position().subtract(
+ this.extent().floorDivideBy(2)
+ )
+ );
+ world.hand.grab(this);
+};
+
+Morph.prototype.isPickedUp = function () {
+ return this.parentThatIsA(HandMorph) !== null;
+};
+
+Morph.prototype.situation = function () {
+ // answer a dictionary specifying where I am right now, so
+ // I can slide back to it if I'm dropped somewhere else
+ if (this.parent) {
+ return {
+ origin: this.parent,
+ position: this.position().subtract(this.parent.position())
+ };
+ }
+ return null;
+};
+
+Morph.prototype.slideBackTo = function (situation, inSteps) {
+ var steps = inSteps || 5,
+ pos = situation.origin.position().add(situation.position),
+ xStep = -(this.left() - pos.x) / steps,
+ yStep = -(this.top() - pos.y) / steps,
+ stepCount = 0,
+ oldStep = this.step,
+ oldFps = this.fps,
+ myself = this;
+
+ this.fps = 0;
+ this.step = function () {
+ myself.fullChanged();
+ myself.silentMoveBy(new Point(xStep, yStep));
+ myself.fullChanged();
+ stepCount += 1;
+ if (stepCount === steps) {
+ situation.origin.add(myself);
+ if (situation.origin.reactToDropOf) {
+ situation.origin.reactToDropOf(myself);
+ }
+ myself.step = oldStep;
+ myself.fps = oldFps;
+ }
+ };
+};
+
+// Morph utilities:
+
+Morph.prototype.nop = function () {
+ nop();
+};
+
+Morph.prototype.resize = function () {
+ this.world().activeHandle = new HandleMorph(this);
+};
+
+Morph.prototype.move = function () {
+ this.world().activeHandle = new HandleMorph(
+ this,
+ null,
+ null,
+ null,
+ null,
+ 'move'
+ );
+};
+
+Morph.prototype.hint = function (msg) {
+ var m, text;
+ text = msg;
+ if (msg) {
+ if (msg.toString) {
+ text = msg.toString();
+ }
+ } else {
+ text = 'NULL';
+ }
+ m = new MenuMorph(this, text);
+ m.isDraggable = true;
+ m.popUpCenteredAtHand(this.world());
+};
+
+Morph.prototype.inform = function (msg) {
+ var m, text;
+ text = msg;
+ if (msg) {
+ if (msg.toString) {
+ text = msg.toString();
+ }
+ } else {
+ text = 'NULL';
+ }
+ m = new MenuMorph(this, text);
+ m.addItem("Ok");
+ m.isDraggable = true;
+ m.popUpCenteredAtHand(this.world());
+};
+
+Morph.prototype.prompt = function (
+ msg,
+ callback,
+ environment,
+ defaultContents,
+ width,
+ floorNum,
+ ceilingNum,
+ isRounded
+) {
+ var menu, entryField, slider, isNumeric;
+ if (ceilingNum) {
+ isNumeric = true;
+ }
+ menu = new MenuMorph(
+ callback || null,
+ msg || '',
+ environment || null
+ );
+ entryField = new StringFieldMorph(
+ defaultContents || '',
+ width || 100,
+ MorphicPreferences.prompterFontSize,
+ MorphicPreferences.prompterFontName,
+ false,
+ false,
+ isNumeric
+ );
+ menu.items.push(entryField);
+ if (ceilingNum || MorphicPreferences.useSliderForInput) {
+ slider = new SliderMorph(
+ floorNum || 0,
+ ceilingNum,
+ parseFloat(defaultContents),
+ Math.floor((ceilingNum - floorNum) / 4),
+ 'horizontal'
+ );
+ slider.alpha = 1;
+ slider.color = new Color(225, 225, 225);
+ slider.button.color = menu.borderColor;
+ slider.button.highlightColor = slider.button.color.copy();
+ slider.button.highlightColor.b += 100;
+ slider.button.pressColor = slider.button.color.copy();
+ slider.button.pressColor.b += 150;
+ slider.setHeight(MorphicPreferences.prompterSliderSize);
+ if (isRounded) {
+ slider.action = function (num) {
+ entryField.changed();
+ entryField.text.text = Math.round(num).toString();
+ entryField.text.drawNew();
+ entryField.text.changed();
+ entryField.text.edit();
+ };
+ } else {
+ slider.action = function (num) {
+ entryField.changed();
+ entryField.text.text = num.toString();
+ entryField.text.drawNew();
+ entryField.text.changed();
+ };
+ }
+ menu.items.push(slider);
+ }
+
+ menu.addLine(2);
+ menu.addItem('Ok', function () {
+ return entryField.string();
+ });
+ menu.addItem('Cancel', function () {
+ return null;
+ });
+ menu.isDraggable = true;
+ menu.popUpAtHand(this.world());
+ entryField.text.edit();
+};
+
+Morph.prototype.pickColor = function (
+ msg,
+ callback,
+ environment,
+ defaultContents
+) {
+ var menu, colorPicker;
+ menu = new MenuMorph(
+ callback || null,
+ msg || '',
+ environment || null
+ );
+ colorPicker = new ColorPickerMorph(defaultContents);
+ menu.items.push(colorPicker);
+ menu.addLine(2);
+ menu.addItem('Ok', function () {
+ return colorPicker.getChoice();
+ });
+ menu.addItem('Cancel', function () {
+ return null;
+ });
+ menu.isDraggable = true;
+ menu.popUpAtHand(this.world());
+};
+
+Morph.prototype.inspect = function (anotherObject) {
+ var world = this.world instanceof Function ?
+ this.world() : this.root() || this.world,
+ inspector,
+ inspectee = this;
+
+ if (anotherObject) {
+ inspectee = anotherObject;
+ }
+ inspector = new InspectorMorph(inspectee);
+ inspector.setPosition(world.hand.position());
+ inspector.keepWithin(world);
+ world.add(inspector);
+ inspector.changed();
+};
+
+// Morph menus:
+
+Morph.prototype.contextMenu = function () {
+ var world;
+
+ if (this.customContextMenu) {
+ return this.customContextMenu;
+ }
+ world = this.world instanceof Function ? this.world() : this.world;
+ if (world && world.isDevMode) {
+ if (this.parent === world) {
+ return this.developersMenu();
+ }
+ return this.hierarchyMenu();
+ }
+ return this.userMenu() ||
+ (this.parent && this.parent.userMenu());
+};
+
+Morph.prototype.hierarchyMenu = function () {
+ var parents = this.allParents(),
+ world = this.world instanceof Function ? this.world() : this.world,
+ menu = new MenuMorph(this, null);
+
+ parents.forEach(function (each) {
+ if (each.developersMenu && (each !== world)) {
+ menu.addItem(each.toString().slice(0, 50), function () {
+ each.developersMenu().popUpAtHand(world);
+ });
+ }
+ });
+ return menu;
+};
+
+Morph.prototype.developersMenu = function () {
+ // 'name' is not an official property of a function, hence:
+ var world = this.world instanceof Function ? this.world() : this.world,
+ userMenu = this.userMenu() ||
+ (this.parent && this.parent.userMenu()),
+ menu = new MenuMorph(this, this.constructor.name ||
+ this.constructor.toString().split(' ')[1].split('(')[0]);
+ if (userMenu) {
+ menu.addItem(
+ 'user features...',
+ function () {
+ userMenu.popUpAtHand(world);
+ }
+ );
+ menu.addLine();
+ }
+ menu.addItem(
+ "color...",
+ function () {
+ this.pickColor(
+ menu.title + '\ncolor:',
+ this.setColor,
+ this,
+ this.color
+ );
+ },
+ 'choose another color \nfor this morph'
+ );
+ menu.addItem(
+ "transparency...",
+ function () {
+ this.prompt(
+ menu.title + '\nalpha\nvalue:',
+ this.setAlphaScaled,
+ this,
+ (this.alpha * 100).toString(),
+ null,
+ 1,
+ 100,
+ true
+ );
+ },
+ 'set this morph\'s\nalpha value'
+ );
+ menu.addItem(
+ "resize...",
+ 'resize',
+ 'show a handle\nwhich can be dragged\nto change this morph\'s' +
+ ' extent'
+ );
+ menu.addLine();
+ menu.addItem(
+ "duplicate",
+ function () {
+ this.fullCopy().pickUp(this.world());
+ },
+ 'make a copy\nand pick it up'
+ );
+ menu.addItem(
+ "pick up",
+ 'pickUp',
+ 'disattach and put \ninto the hand'
+ );
+ menu.addItem(
+ "attach...",
+ 'attach',
+ 'stick this morph\nto another one'
+ );
+ menu.addItem(
+ "move...",
+ 'move',
+ 'show a handle\nwhich can be dragged\nto move this morph'
+ );
+ menu.addItem(
+ "inspect...",
+ 'inspect',
+ 'open a window\non all properties'
+ );
+ menu.addItem(
+ "pic...",
+ function () {
+ window.open(this.fullImageClassic().toDataURL());
+ },
+ 'open a new window\nwith a picture of this morph'
+ );
+ menu.addLine();
+ if (this.isDraggable) {
+ menu.addItem(
+ "lock",
+ 'toggleIsDraggable',
+ 'make this morph\nunmovable'
+ );
+ } else {
+ menu.addItem(
+ "unlock",
+ 'toggleIsDraggable',
+ 'make this morph\nmovable'
+ );
+ }
+ menu.addItem("hide", 'hide');
+ menu.addItem("delete", 'destroy');
+ if (!(this instanceof WorldMorph)) {
+ menu.addLine();
+ menu.addItem(
+ "World...",
+ function () {
+ world.contextMenu().popUpAtHand(world);
+ },
+ 'show the\nWorld\'s menu'
+ );
+ }
+ return menu;
+};
+
+Morph.prototype.userMenu = function () {
+ return null;
+};
+
+// Morph menu actions
+
+Morph.prototype.setAlphaScaled = function (alpha) {
+ // for context menu demo purposes
+ var newAlpha, unscaled;
+ if (typeof alpha === 'number') {
+ unscaled = alpha / 100;
+ this.alpha = Math.min(Math.max(unscaled, 0.1), 1);
+ } else {
+ newAlpha = parseFloat(alpha);
+ if (!isNaN(newAlpha)) {
+ unscaled = newAlpha / 100;
+ this.alpha = Math.min(Math.max(unscaled, 0.1), 1);
+ }
+ }
+ this.changed();
+};
+
+Morph.prototype.attach = function () {
+ var choices = this.overlappedMorphs(),
+ menu = new MenuMorph(this, 'choose new parent:'),
+ myself = this;
+
+ choices.forEach(function (each) {
+ menu.addItem(each.toString().slice(0, 50), function () {
+ each.add(myself);
+ myself.isDraggable = false;
+ });
+ });
+ if (choices.length > 0) {
+ menu.popUpAtHand(this.world());
+ }
+};
+
+Morph.prototype.toggleIsDraggable = function () {
+ // for context menu demo purposes
+ this.isDraggable = !this.isDraggable;
+};
+
+Morph.prototype.colorSetters = function () {
+ // for context menu demo purposes
+ return ['color'];
+};
+
+Morph.prototype.numericalSetters = function () {
+ // for context menu demo purposes
+ return [
+ 'setLeft',
+ 'setTop',
+ 'setWidth',
+ 'setHeight',
+ 'setAlphaScaled'
+ ];
+};
+
+// Morph entry field tabbing:
+
+Morph.prototype.allEntryFields = function () {
+ return this.allChildren().filter(function (each) {
+ return each.isEditable &&
+ (each instanceof StringMorph ||
+ each instanceof TextMorph);
+ });
+};
+
+Morph.prototype.nextEntryField = function (current) {
+ var fields = this.allEntryFields(),
+ idx = fields.indexOf(current);
+ if (idx !== -1) {
+ if (fields.length > idx + 1) {
+ return fields[idx + 1];
+ }
+ }
+ return fields[0];
+};
+
+Morph.prototype.previousEntryField = function (current) {
+ var fields = this.allEntryFields(),
+ idx = fields.indexOf(current);
+ if (idx !== -1) {
+ if (idx > 0) {
+ return fields[idx - 1];
+ }
+ return fields[fields.length - 1];
+ }
+ return fields[0];
+};
+
+Morph.prototype.tab = function (editField) {
+/*
+ the <tab> key was pressed in one of my edit fields.
+ invoke my "nextTab()" function if it exists, else
+ propagate it up my owner chain.
+*/
+ if (this.nextTab) {
+ this.nextTab(editField);
+ } else if (this.parent) {
+ this.parent.tab(editField);
+ }
+};
+
+Morph.prototype.backTab = function (editField) {
+/*
+ the <back tab> key was pressed in one of my edit fields.
+ invoke my "previousTab()" function if it exists, else
+ propagate it up my owner chain.
+*/
+ if (this.previousTab) {
+ this.previousTab(editField);
+ } else if (this.parent) {
+ this.parent.backTab(editField);
+ }
+};
+
+/*
+ the following are examples of what the navigation methods should
+ look like. Insert these at the World level for fallback, and at lower
+ levels in the Morphic tree (e.g. dialog boxes) for a more fine-grained
+ control over the tabbing cycle.
+
+Morph.prototype.nextTab = function (editField) {
+ var next = this.nextEntryField(editField);
+ editField.clearSelection();
+ next.selectAll();
+ next.edit();
+};
+
+Morph.prototype.previousTab = function (editField) {
+ var prev = this.previousEntryField(editField);
+ editField.clearSelection();
+ prev.selectAll();
+ prev.edit();
+};
+
+*/
+
+// Morph events:
+
+Morph.prototype.escalateEvent = function (functionName, arg) {
+ var handler = this.parent;
+ while (!handler[functionName] && handler.parent !== null) {
+ handler = handler.parent;
+ }
+ if (handler[functionName]) {
+ handler[functionName](arg);
+ }
+};
+
+// Morph eval:
+
+Morph.prototype.evaluateString = function (code) {
+ var result;
+
+ try {
+ result = eval(code);
+ this.drawNew();
+ this.changed();
+ } catch (err) {
+ this.inform(err);
+ }
+ return result;
+};
+
+// Morph collision detection:
+
+Morph.prototype.isTouching = function (otherMorph) {
+ var oImg = this.overlappingImage(otherMorph),
+ data = oImg.getContext('2d')
+ .getImageData(1, 1, oImg.width, oImg.height)
+ .data;
+ return detect(
+ data,
+ function (each) {
+ return each !== 0;
+ }
+ ) !== null;
+};
+
+Morph.prototype.overlappingImage = function (otherMorph) {
+ var fb = this.fullBounds(),
+ otherFb = otherMorph.fullBounds(),
+ oRect = fb.intersect(otherFb),
+ oImg = newCanvas(oRect.extent()),
+ ctx = oImg.getContext('2d');
+ if (oRect.width() < 1 || oRect.height() < 1) {
+ return newCanvas(new Point(1, 1));
+ }
+ ctx.drawImage(
+ this.fullImage(),
+ oRect.origin.x - fb.origin.x,
+ oRect.origin.y - fb.origin.y
+ );
+ ctx.globalCompositeOperation = 'source-in';
+ ctx.drawImage(
+ otherMorph.fullImage(),
+ otherFb.origin.x - oRect.origin.x,
+ otherFb.origin.y - oRect.origin.y
+ );
+ return oImg;
+};
+
+// ShadowMorph /////////////////////////////////////////////////////////
+
+// ShadowMorph inherits from Morph:
+
+ShadowMorph.prototype = new Morph();
+ShadowMorph.prototype.constructor = ShadowMorph;
+ShadowMorph.uber = Morph.prototype;
+
+// ShadowMorph instance creation:
+
+function ShadowMorph() {
+ this.init();
+}
+
+// HandleMorph ////////////////////////////////////////////////////////
+
+// I am a resize / move handle that can be attached to any Morph
+
+// HandleMorph inherits from Morph:
+
+HandleMorph.prototype = new Morph();
+HandleMorph.prototype.constructor = HandleMorph;
+HandleMorph.uber = Morph.prototype;
+
+// HandleMorph instance creation:
+
+function HandleMorph(target, minX, minY, insetX, insetY, type) {
+ // if insetY is missing, it will be the same as insetX
+ this.init(target, minX, minY, insetX, insetY, type);
+}
+
+HandleMorph.prototype.init = function (
+ target,
+ minX,
+ minY,
+ insetX,
+ insetY,
+ type
+) {
+ var size = MorphicPreferences.handleSize;
+ this.target = target || null;
+ this.minExtent = new Point(minX || 0, minY || 0);
+ this.inset = new Point(insetX || 0, insetY || insetX || 0);
+ this.type = type || 'resize'; // can also be 'move'
+ HandleMorph.uber.init.call(this);
+ this.color = new Color(255, 255, 255);
+ this.isDraggable = false;
+ this.noticesTransparentClick = true;
+ this.setExtent(new Point(size, size));
+};
+
+// HandleMorph drawing:
+
+HandleMorph.prototype.drawNew = function () {
+ this.normalImage = newCanvas(this.extent());
+ this.highlightImage = newCanvas(this.extent());
+ this.drawOnCanvas(
+ this.normalImage,
+ this.color,
+ new Color(100, 100, 100)
+ );
+ this.drawOnCanvas(
+ this.highlightImage,
+ new Color(100, 100, 255),
+ new Color(255, 255, 255)
+ );
+ this.image = this.normalImage;
+ if (this.target) {
+ this.setPosition(
+ this.target.bottomRight().subtract(
+ this.extent().add(this.inset)
+ )
+ );
+ this.target.add(this);
+ this.target.changed();
+ }
+};
+
+HandleMorph.prototype.drawOnCanvas = function (
+ aCanvas,
+ color,
+ shadowColor
+) {
+ var context = aCanvas.getContext('2d'),
+ p1,
+ p11,
+ p2,
+ p22,
+ i;
+
+ context.lineWidth = 1;
+ context.lineCap = 'round';
+
+ context.strokeStyle = color.toString();
+
+ if (this.type === 'move') {
+
+ p1 = this.bottomLeft().subtract(this.position());
+ p11 = p1.copy();
+ p2 = this.topRight().subtract(this.position());
+ p22 = p2.copy();
+
+ for (i = 0; i <= this.height(); i = i + 6) {
+ p11.y = p1.y - i;
+ p22.y = p2.y - i;
+
+ context.beginPath();
+ context.moveTo(p11.x, p11.y);
+ context.lineTo(p22.x, p22.y);
+ context.closePath();
+ context.stroke();
+ }
+ }
+
+ p1 = this.bottomLeft().subtract(this.position());
+ p11 = p1.copy();
+ p2 = this.topRight().subtract(this.position());
+ p22 = p2.copy();
+
+ for (i = 0; i <= this.width(); i = i + 6) {
+ p11.x = p1.x + i;
+ p22.x = p2.x + i;
+
+ context.beginPath();
+ context.moveTo(p11.x, p11.y);
+ context.lineTo(p22.x, p22.y);
+ context.closePath();
+ context.stroke();
+ }
+
+ context.strokeStyle = shadowColor.toString();
+
+ if (this.type === 'move') {
+
+ p1 = this.bottomLeft().subtract(this.position());
+ p11 = p1.copy();
+ p2 = this.topRight().subtract(this.position());
+ p22 = p2.copy();
+
+ for (i = -2; i <= this.height(); i = i + 6) {
+ p11.y = p1.y - i;
+ p22.y = p2.y - i;
+
+ context.beginPath();
+ context.moveTo(p11.x, p11.y);
+ context.lineTo(p22.x, p22.y);
+ context.closePath();
+ context.stroke();
+ }
+ }
+
+ p1 = this.bottomLeft().subtract(this.position());
+ p11 = p1.copy();
+ p2 = this.topRight().subtract(this.position());
+ p22 = p2.copy();
+
+ for (i = 2; i <= this.width(); i = i + 6) {
+ p11.x = p1.x + i;
+ p22.x = p2.x + i;
+
+ context.beginPath();
+ context.moveTo(p11.x, p11.y);
+ context.lineTo(p22.x, p22.y);
+ context.closePath();
+ context.stroke();
+ }
+};
+
+// HandleMorph stepping:
+
+HandleMorph.prototype.step = null;
+
+HandleMorph.prototype.mouseDownLeft = function (pos) {
+ var world = this.root(),
+ offset = pos.subtract(this.bounds.origin),
+ myself = this;
+
+ if (!this.target) {
+ return null;
+ }
+ this.step = function () {
+ var newPos, newExt;
+ if (world.hand.mouseButton) {
+ newPos = world.hand.bounds.origin.copy().subtract(offset);
+ if (this.type === 'resize') {
+ newExt = newPos.add(
+ myself.extent().add(myself.inset)
+ ).subtract(myself.target.bounds.origin);
+ newExt = newExt.max(myself.minExtent);
+ myself.target.setExtent(newExt);
+
+ myself.setPosition(
+ myself.target.bottomRight().subtract(
+ myself.extent().add(myself.inset)
+ )
+ );
+ } else { // type === 'move'
+ myself.target.setPosition(
+ newPos.subtract(this.target.extent())
+ .add(this.extent())
+ );
+ }
+ } else {
+ this.step = null;
+ }
+ };
+ if (!this.target.step) {
+ this.target.step = function () {
+ nop();
+ };
+ }
+};
+
+// HandleMorph dragging and dropping:
+
+HandleMorph.prototype.rootForGrab = function () {
+ return this;
+};
+
+// HandleMorph events:
+
+HandleMorph.prototype.mouseEnter = function () {
+ this.image = this.highlightImage;
+ this.changed();
+};
+
+HandleMorph.prototype.mouseLeave = function () {
+ this.image = this.normalImage;
+ this.changed();
+};
+
+// HandleMorph duplicating:
+
+HandleMorph.prototype.copyRecordingReferences = function (dict) {
+ // inherited, see comment in Morph
+ var c = HandleMorph.uber.copyRecordingReferences.call(
+ this,
+ dict
+ );
+ if (c.target && dict[this.target]) {
+ c.target = (dict[this.target]);
+ }
+ return c;
+};
+
+// HandleMorph menu:
+
+HandleMorph.prototype.attach = function () {
+ var choices = this.overlappedMorphs(),
+ menu = new MenuMorph(this, 'choose target:'),
+ myself = this;
+
+ choices.forEach(function (each) {
+ menu.addItem(each.toString().slice(0, 50), function () {
+ myself.isDraggable = false;
+ myself.target = each;
+ myself.drawNew();
+ myself.noticesTransparentClick = true;
+ });
+ });
+ if (choices.length > 0) {
+ menu.popUpAtHand(this.world());
+ }
+};
+
+// PenMorph ////////////////////////////////////////////////////////////
+
+// I am a simple LOGO-wise turtle.
+
+// PenMorph: referenced constructors
+
+var PenMorph;
+
+// PenMorph inherits from Morph:
+
+PenMorph.prototype = new Morph();
+PenMorph.prototype.constructor = PenMorph;
+PenMorph.uber = Morph.prototype;
+
+// PenMorph instance creation:
+
+function PenMorph() {
+ this.init();
+}
+
+PenMorph.prototype.init = function () {
+ var size = MorphicPreferences.handleSize * 4;
+
+ // additional properties:
+ this.isWarped = false; // internal optimization
+ this.heading = 0;
+ this.isDown = true;
+ this.size = 1;
+ this.wantsRedraw = false;
+ this.penPoint = 'tip'; // or 'center"
+
+ HandleMorph.uber.init.call(this);
+ this.setExtent(new Point(size, size));
+};
+
+// PenMorph updating - optimized for warping, i.e atomic recursion
+
+PenMorph.prototype.changed = function () {
+ if (this.isWarped === false) {
+ var w = this.root();
+ if (w instanceof WorldMorph) {
+ w.broken.push(this.visibleBounds().spread());
+ }
+ if (this.parent) {
+ this.parent.childChanged(this);
+ }
+ }
+};
+
+// PenMorph display:
+
+PenMorph.prototype.drawNew = function (facing) {
+/*
+ my orientation can be overridden with the "facing" parameter to
+ implement Scratch-style rotation styles
+
+*/
+ var context, start, dest, left, right, len,
+ direction = facing || this.heading;
+
+ if (this.isWarped) {
+ this.wantsRedraw = true;
+ return;
+ }
+ this.image = newCanvas(this.extent());
+ context = this.image.getContext('2d');
+ len = this.width() / 2;
+ start = this.center().subtract(this.bounds.origin);
+
+ if (this.penPoint === 'tip') {
+ dest = start.distanceAngle(len * 0.75, direction - 180);
+ left = start.distanceAngle(len, direction + 195);
+ right = start.distanceAngle(len, direction - 195);
+ } else { // 'middle'
+ dest = start.distanceAngle(len * 0.75, direction);
+ left = start.distanceAngle(len * 0.33, direction + 230);
+ right = start.distanceAngle(len * 0.33, direction - 230);
+ }
+
+ context.fillStyle = this.color.toString();
+ context.beginPath();
+
+ context.moveTo(start.x, start.y);
+ context.lineTo(left.x, left.y);
+ context.lineTo(dest.x, dest.y);
+ context.lineTo(right.x, right.y);
+
+ context.closePath();
+ context.strokeStyle = 'white';
+ context.lineWidth = 3;
+ context.stroke();
+ context.strokeStyle = 'black';
+ context.lineWidth = 1;
+ context.stroke();
+ context.fill();
+
+};
+
+// PenMorph access:
+
+PenMorph.prototype.setHeading = function (degrees) {
+ this.heading = parseFloat(degrees) % 360;
+ this.drawNew();
+ this.changed();
+};
+
+// PenMorph drawing:
+
+PenMorph.prototype.drawLine = function (start, dest) {
+ var context = this.parent.penTrails().getContext('2d'),
+ from = start.subtract(this.parent.bounds.origin),
+ to = dest.subtract(this.parent.bounds.origin);
+ if (this.isDown) {
+ context.lineWidth = this.size;
+ context.strokeStyle = this.color.toString();
+ context.lineCap = 'round';
+ context.lineJoin = 'round';
+ context.beginPath();
+ context.moveTo(from.x, from.y);
+ context.lineTo(to.x, to.y);
+ context.stroke();
+ if (this.isWarped === false) {
+ this.world().broken.push(
+ start.rectangle(dest).expandBy(
+ Math.max(this.size / 2, 1)
+ ).intersect(this.parent.visibleBounds()).spread()
+ );
+ }
+ }
+};
+
+// PenMorph turtle ops:
+
+PenMorph.prototype.turn = function (degrees) {
+ this.setHeading(this.heading + parseFloat(degrees));
+};
+
+PenMorph.prototype.forward = function (steps) {
+ var start = this.center(),
+ dest,
+ dist = parseFloat(steps);
+ if (dist >= 0) {
+ dest = this.position().distanceAngle(dist, this.heading);
+ } else {
+ dest = this.position().distanceAngle(
+ Math.abs(dist),
+ (this.heading - 180)
+ );
+ }
+ this.setPosition(dest);
+ this.drawLine(start, this.center());
+};
+
+PenMorph.prototype.down = function () {
+ this.isDown = true;
+};
+
+PenMorph.prototype.up = function () {
+ this.isDown = false;
+};
+
+PenMorph.prototype.clear = function () {
+ this.parent.drawNew();
+ this.parent.changed();
+};
+
+// PenMorph optimization for atomic recursion:
+
+PenMorph.prototype.startWarp = function () {
+ this.wantsRedraw = false;
+ this.isWarped = true;
+};
+
+PenMorph.prototype.endWarp = function () {
+ this.isWarped = false;
+ if (this.wantsRedraw) {
+ this.drawNew();
+ this.wantsRedraw = false;
+ }
+ this.parent.changed();
+};
+
+PenMorph.prototype.warp = function (fun) {
+ this.startWarp();
+ fun.call(this);
+ this.endWarp();
+};
+
+PenMorph.prototype.warpOp = function (selector, argsArray) {
+ this.startWarp();
+ this[selector].apply(this, argsArray);
+ this.endWarp();
+};
+
+// PenMorph demo ops:
+// try these with WARP eg.: this.warp(function () {tree(12, 120, 20)})
+
+PenMorph.prototype.warpSierpinski = function (length, min) {
+ this.warpOp('sierpinski', [length, min]);
+};
+
+PenMorph.prototype.sierpinski = function (length, min) {
+ var i;
+ if (length > min) {
+ for (i = 0; i < 3; i += 1) {
+ this.sierpinski(length * 0.5, min);
+ this.turn(120);
+ this.forward(length);
+ }
+ }
+};
+
+PenMorph.prototype.warpTree = function (level, length, angle) {
+ this.warpOp('tree', [level, length, angle]);
+};
+
+PenMorph.prototype.tree = function (level, length, angle) {
+ if (level > 0) {
+ this.size = level;
+ this.forward(length);
+ this.turn(angle);
+ this.tree(level - 1, length * 0.75, angle);
+ this.turn(angle * -2);
+ this.tree(level - 1, length * 0.75, angle);
+ this.turn(angle);
+ this.forward(-length);
+ }
+};
+
+// ColorPaletteMorph ///////////////////////////////////////////////////
+
+var ColorPaletteMorph;
+
+// ColorPaletteMorph inherits from Morph:
+
+ColorPaletteMorph.prototype = new Morph();
+ColorPaletteMorph.prototype.constructor = ColorPaletteMorph;
+ColorPaletteMorph.uber = Morph.prototype;
+
+// ColorPaletteMorph instance creation:
+
+function ColorPaletteMorph(target, sizePoint) {
+ this.init(
+ target || null,
+ sizePoint || new Point(80, 50)
+ );
+}
+
+ColorPaletteMorph.prototype.init = function (target, size) {
+ ColorPaletteMorph.uber.init.call(this);
+ this.target = target;
+ this.targetSetter = 'color';
+ this.silentSetExtent(size);
+ this.choice = null;
+ this.drawNew();
+};
+
+ColorPaletteMorph.prototype.drawNew = function () {
+ var context, ext, x, y, h, l;
+
+ ext = this.extent();
+ this.image = newCanvas(this.extent());
+ context = this.image.getContext('2d');
+ this.choice = new Color();
+ for (x = 0; x <= ext.x; x += 1) {
+ h = 360 * x / ext.x;
+ for (y = 0; y <= ext.y; y += 1) {
+ l = 100 - (y / ext.y * 100);
+ context.fillStyle = 'hsl(' + h + ',100%,' + l + '%)';
+ context.fillRect(x, y, 1, 1);
+ }
+ }
+};
+
+ColorPaletteMorph.prototype.mouseMove = function (pos) {
+ this.choice = this.getPixelColor(pos);
+ this.updateTarget();
+};
+
+ColorPaletteMorph.prototype.mouseDownLeft = function (pos) {
+ this.choice = this.getPixelColor(pos);
+ this.updateTarget();
+};
+
+ColorPaletteMorph.prototype.updateTarget = function () {
+ if (this.target instanceof Morph && this.choice !== null) {
+ if (this.target[this.targetSetter] instanceof Function) {
+ this.target[this.targetSetter](this.choice);
+ } else {
+ this.target[this.targetSetter] = this.choice;
+ this.target.drawNew();
+ this.target.changed();
+ }
+ }
+};
+
+// ColorPaletteMorph duplicating:
+
+ColorPaletteMorph.prototype.copyRecordingReferences = function (dict) {
+ // inherited, see comment in Morph
+ var c = ColorPaletteMorph.uber.copyRecordingReferences.call(
+ this,
+ dict
+ );
+ if (c.target && dict[this.target]) {
+ c.target = (dict[this.target]);
+ }
+ return c;
+};
+
+// ColorPaletteMorph menu:
+
+ColorPaletteMorph.prototype.developersMenu = function () {
+ var menu = ColorPaletteMorph.uber.developersMenu.call(this);
+ menu.addLine();
+ menu.addItem(
+ 'set target',
+ "setTarget",
+ 'choose another morph\nwhose color property\n will be' +
+ ' controlled by this one'
+ );
+ return menu;
+};
+
+ColorPaletteMorph.prototype.setTarget = function () {
+ var choices = this.overlappedMorphs(),
+ menu = new MenuMorph(this, 'choose target:'),
+ myself = this;
+
+ choices.push(this.world());
+ choices.forEach(function (each) {
+ menu.addItem(each.toString().slice(0, 50), function () {
+ myself.target = each;
+ myself.setTargetSetter();
+ });
+ });
+ if (choices.length === 1) {
+ this.target = choices[0];
+ this.setTargetSetter();
+ } else if (choices.length > 0) {
+ menu.popUpAtHand(this.world());
+ }
+};
+
+ColorPaletteMorph.prototype.setTargetSetter = function () {
+ var choices = this.target.colorSetters(),
+ menu = new MenuMorph(this, 'choose target property:'),
+ myself = this;
+
+ choices.forEach(function (each) {
+ menu.addItem(each, function () {
+ myself.targetSetter = each;
+ });
+ });
+ if (choices.length === 1) {
+ this.targetSetter = choices[0];
+ } else if (choices.length > 0) {
+ menu.popUpAtHand(this.world());
+ }
+};
+
+// GrayPaletteMorph ///////////////////////////////////////////////////
+
+var GrayPaletteMorph;
+
+// GrayPaletteMorph inherits from ColorPaletteMorph:
+
+GrayPaletteMorph.prototype = new ColorPaletteMorph();
+GrayPaletteMorph.prototype.constructor = GrayPaletteMorph;
+GrayPaletteMorph.uber = ColorPaletteMorph.prototype;
+
+// GrayPaletteMorph instance creation:
+
+function GrayPaletteMorph(target, sizePoint) {
+ this.init(
+ target || null,
+ sizePoint || new Point(80, 10)
+ );
+}
+
+GrayPaletteMorph.prototype.drawNew = function () {
+ var context, ext, gradient;
+
+ ext = this.extent();
+ this.image = newCanvas(this.extent());
+ context = this.image.getContext('2d');
+ this.choice = new Color();
+ gradient = context.createLinearGradient(0, 0, ext.x, ext.y);
+ gradient.addColorStop(0, 'black');
+ gradient.addColorStop(1, 'white');
+ context.fillStyle = gradient;
+ context.fillRect(0, 0, ext.x, ext.y);
+};
+
+// ColorPickerMorph ///////////////////////////////////////////////////
+
+// ColorPickerMorph inherits from Morph:
+
+ColorPickerMorph.prototype = new Morph();
+ColorPickerMorph.prototype.constructor = ColorPickerMorph;
+ColorPickerMorph.uber = Morph.prototype;
+
+// ColorPickerMorph instance creation:
+
+function ColorPickerMorph(defaultColor) {
+ this.init(defaultColor || new Color(255, 255, 255));
+}
+
+ColorPickerMorph.prototype.init = function (defaultColor) {
+ this.choice = defaultColor;
+ ColorPickerMorph.uber.init.call(this);
+ this.color = new Color(255, 255, 255);
+ this.silentSetExtent(new Point(80, 80));
+ this.drawNew();
+};
+
+ColorPickerMorph.prototype.drawNew = function () {
+ ColorPickerMorph.uber.drawNew.call(this);
+ this.buildSubmorphs();
+};
+
+ColorPickerMorph.prototype.buildSubmorphs = function () {
+ var cpal, gpal, x, y;
+
+ this.children.forEach(function (child) {
+ child.destroy();
+ });
+ this.children = [];
+ this.feedback = new Morph();
+ this.feedback.color = this.choice;
+ this.feedback.setExtent(new Point(20, 20));
+ cpal = new ColorPaletteMorph(
+ this.feedback,
+ new Point(this.width(), 50)
+ );
+ gpal = new GrayPaletteMorph(
+ this.feedback,
+ new Point(this.width(), 5)
+ );
+ cpal.setPosition(this.bounds.origin);
+ this.add(cpal);
+ gpal.setPosition(cpal.bottomLeft());
+ this.add(gpal);
+ x = (gpal.left() +
+ Math.floor((gpal.width() - this.feedback.width()) / 2));
+ y = gpal.bottom() + Math.floor((this.bottom() -
+ gpal.bottom() - this.feedback.height()) / 2);
+ this.feedback.setPosition(new Point(x, y));
+ this.add(this.feedback);
+};
+
+ColorPickerMorph.prototype.getChoice = function () {
+ return this.feedback.color;
+};
+
+ColorPickerMorph.prototype.rootForGrab = function () {
+ return this;
+};
+
+// BlinkerMorph ////////////////////////////////////////////////////////
+
+// can be used for text cursors
+
+var BlinkerMorph;
+
+// BlinkerMorph inherits from Morph:
+
+BlinkerMorph.prototype = new Morph();
+BlinkerMorph.prototype.constructor = BlinkerMorph;
+BlinkerMorph.uber = Morph.prototype;
+
+// BlinkerMorph instance creation:
+
+function BlinkerMorph(rate) {
+ this.init(rate);
+}
+
+BlinkerMorph.prototype.init = function (rate) {
+ BlinkerMorph.uber.init.call(this);
+ this.color = new Color(0, 0, 0);
+ this.fps = rate || 2;
+ this.drawNew();
+};
+
+// BlinkerMorph stepping:
+
+BlinkerMorph.prototype.step = function () {
+ this.toggleVisibility();
+};
+
+// CursorMorph /////////////////////////////////////////////////////////
+
+// I am a String/Text editing widget
+
+// CursorMorph: referenced constructors
+
+var CursorMorph;
+
+// CursorMorph inherits from BlinkerMorph:
+
+CursorMorph.prototype = new BlinkerMorph();
+CursorMorph.prototype.constructor = CursorMorph;
+CursorMorph.uber = BlinkerMorph.prototype;
+
+// CursorMorph preferences settings:
+
+CursorMorph.prototype.viewPadding = 1;
+
+// CursorMorph instance creation:
+
+function CursorMorph(aStringOrTextMorph) {
+ this.init(aStringOrTextMorph);
+}
+
+CursorMorph.prototype.init = function (aStringOrTextMorph) {
+ var ls;
+
+ // additional properties:
+ this.keyDownEventUsed = false;
+ this.target = aStringOrTextMorph;
+ this.originalContents = this.target.text;
+ this.originalAlignment = this.target.alignment;
+ this.slot = this.target.text.length;
+ CursorMorph.uber.init.call(this);
+ ls = fontHeight(this.target.fontSize);
+ this.setExtent(new Point(Math.max(Math.floor(ls / 20), 1), ls));
+ this.drawNew();
+ this.image.getContext('2d').font = this.target.font();
+ if (this.target instanceof TextMorph &&
+ (this.target.alignment !== 'left')) {
+ this.target.setAlignmentToLeft();
+ }
+ this.gotoSlot(this.slot);
+};
+
+// CursorMorph event processing:
+
+CursorMorph.prototype.processKeyPress = function (event) {
+ // this.inspectKeyEvent(event);
+ if (this.keyDownEventUsed) {
+ this.keyDownEventUsed = false;
+ return null;
+ }
+ if ((event.keyCode === 40) || event.charCode === 40) {
+ this.insert('(');
+ return null;
+ }
+ if ((event.keyCode === 37) || event.charCode === 37) {
+ this.insert('%');
+ return null;
+ }
+ if (event.keyCode) { // Opera doesn't support charCode
+ if (event.ctrlKey) {
+ this.ctrl(event.keyCode);
+ } else if (event.metaKey) {
+ this.cmd(event.keyCode);
+ } else {
+ this.insert(
+ String.fromCharCode(event.keyCode),
+ event.shiftKey
+ );
+ }
+ } else if (event.charCode) { // all other browsers
+ if (event.ctrlKey) {
+ this.ctrl(event.charCode);
+ } else if (event.metaKey) {
+ this.cmd(event.keyCode);
+ } else {
+ this.insert(
+ String.fromCharCode(event.charCode),
+ event.shiftKey
+ );
+ }
+ }
+ // notify target's parent of key event
+ this.target.escalateEvent('reactToKeystroke', event);
+};
+
+CursorMorph.prototype.processKeyDown = function (event) {
+ // this.inspectKeyEvent(event);
+ var shift = event.shiftKey;
+ this.keyDownEventUsed = false;
+ if (event.ctrlKey) {
+ this.ctrl(event.keyCode);
+ // notify target's parent of key event
+ this.target.escalateEvent('reactToKeystroke', event);
+ return;
+ }
+ if (event.metaKey) {
+ this.cmd(event.keyCode);
+ // notify target's parent of key event
+ this.target.escalateEvent('reactToKeystroke', event);
+ return;
+ }
+
+ switch (event.keyCode) {
+ case 37:
+ this.goLeft(shift);
+ this.keyDownEventUsed = true;
+ break;
+ case 39:
+ this.goRight(shift);
+ this.keyDownEventUsed = true;
+ break;
+ case 38:
+ this.goUp(shift);
+ this.keyDownEventUsed = true;
+ break;
+ case 40:
+ this.goDown(shift);
+ this.keyDownEventUsed = true;
+ break;
+ case 36:
+ this.goHome(shift);
+ this.keyDownEventUsed = true;
+ break;
+ case 35:
+ this.goEnd(shift);
+ this.keyDownEventUsed = true;
+ break;
+ case 46:
+ this.deleteRight();
+ this.keyDownEventUsed = true;
+ break;
+ case 8:
+ this.deleteLeft();
+ this.keyDownEventUsed = true;
+ break;
+ case 13:
+ if (this.target instanceof StringMorph) {
+ this.accept();
+ } else {
+ this.insert('\n');
+ }
+ this.keyDownEventUsed = true;
+ break;
+ case 27:
+ this.cancel();
+ this.keyDownEventUsed = true;
+ break;
+ default:
+ // this.inspectKeyEvent(event);
+ }
+ // notify target's parent of key event
+ this.target.escalateEvent('reactToKeystroke', event);
+};
+
+// CursorMorph navigation:
+
+/*
+// original non-scrolling code, commented out in case we need to fall back:
+
+CursorMorph.prototype.gotoSlot = function (newSlot) {
+ this.setPosition(this.target.slotPosition(newSlot));
+ this.slot = Math.max(newSlot, 0);
+};
+*/
+
+CursorMorph.prototype.gotoSlot = function (slot) {
+ var length = this.target.text.length,
+ pos = this.target.slotPosition(slot),
+ right,
+ left;
+ this.slot = slot < 0 ? 0 : slot > length ? length : slot;
+ if (this.parent && this.target.isScrollable) {
+ right = this.parent.right() - this.viewPadding;
+ left = this.parent.left() + this.viewPadding;
+ if (pos.x > right) {
+ this.target.setLeft(this.target.left() + right - pos.x);
+ pos.x = right;
+ }
+ if (pos.x < left) {
+ left = Math.min(this.parent.left(), left);
+ this.target.setLeft(this.target.left() + left - pos.x);
+ pos.x = left;
+ }
+ if (this.target.right() < right &&
+ right - this.target.width() < left) {
+ pos.x += right - this.target.right();
+ this.target.setRight(right);
+ }
+ }
+ this.show();
+ this.setPosition(pos);
+ if (this.parent
+ && this.parent.parent instanceof ScrollFrameMorph
+ && this.target.isScrollable) {
+ this.parent.parent.scrollCursorIntoView(this);
+ }
+};
+
+CursorMorph.prototype.goLeft = function (shift) {
+ this.updateSelection(shift);
+ this.gotoSlot(this.slot - 1);
+ this.updateSelection(shift);
+};
+
+CursorMorph.prototype.goRight = function (shift, howMany) {
+ this.updateSelection(shift);
+ this.gotoSlot(this.slot + (howMany || 1));
+ this.updateSelection(shift);
+};
+
+CursorMorph.prototype.goUp = function (shift) {
+ this.updateSelection(shift);
+ this.gotoSlot(this.target.upFrom(this.slot));
+ this.updateSelection(shift);
+};
+
+CursorMorph.prototype.goDown = function (shift) {
+ this.updateSelection(shift);
+ this.gotoSlot(this.target.downFrom(this.slot));
+ this.updateSelection(shift);
+};
+
+CursorMorph.prototype.goHome = function (shift) {
+ this.updateSelection(shift);
+ this.gotoSlot(this.target.startOfLine(this.slot));
+ this.updateSelection(shift);
+};
+
+CursorMorph.prototype.goEnd = function (shift) {
+ this.updateSelection(shift);
+ this.gotoSlot(this.target.endOfLine(this.slot));
+ this.updateSelection(shift);
+};
+
+CursorMorph.prototype.gotoPos = function (aPoint) {
+ this.gotoSlot(this.target.slotAt(aPoint));
+ this.show();
+};
+
+// CursorMorph selecting:
+
+CursorMorph.prototype.updateSelection = function (shift) {
+ if (shift) {
+ if (!this.target.endMark && !this.target.startMark) {
+ this.target.startMark = this.slot;
+ this.target.endMark = this.slot;
+ } else if (this.target.endMark !== this.slot) {
+ this.target.endMark = this.slot;
+ this.target.drawNew();
+ this.target.changed();
+ }
+ } else {
+ this.target.clearSelection();
+ }
+};
+
+// CursorMorph editing:
+
+CursorMorph.prototype.accept = function () {
+ var world = this.root();
+ if (world) {
+ world.stopEditing();
+ }
+ this.escalateEvent('accept', null);
+};
+
+CursorMorph.prototype.cancel = function () {
+ var world = this.root();
+ this.undo();
+ if (world) {
+ world.stopEditing();
+ }
+ this.escalateEvent('cancel', null);
+};
+
+CursorMorph.prototype.undo = function () {
+ this.target.text = this.originalContents;
+ this.target.changed();
+ this.target.drawNew();
+ this.target.changed();
+ this.gotoSlot(0);
+};
+
+CursorMorph.prototype.insert = function (aChar, shiftKey) {
+ var text;
+ if (aChar === '\u0009') {
+ this.target.escalateEvent('reactToEdit', this.target);
+ if (shiftKey) {
+ return this.target.backTab(this.target);
+ }
+ return this.target.tab(this.target);
+ }
+ if (!this.target.isNumeric ||
+ !isNaN(parseFloat(aChar)) ||
+ contains(['-', '.'], aChar)) {
+ if (this.target.selection() !== '') {
+ this.gotoSlot(this.target.selectionStartSlot());
+ this.target.deleteSelection();
+ }
+ text = this.target.text;
+ text = text.slice(0, this.slot) +
+ aChar +
+ text.slice(this.slot);
+ this.target.text = text;
+ this.target.drawNew();
+ this.target.changed();
+ this.goRight(false, aChar.length);
+ }
+};
+
+CursorMorph.prototype.ctrl = function (aChar) {
+ if ((aChar === 97) || (aChar === 65)) {
+ this.target.selectAll();
+ } else if (aChar === 90) {
+ this.undo();
+ } else if (aChar === 123) {
+ this.insert('{');
+ } else if (aChar === 125) {
+ this.insert('}');
+ } else if (aChar === 91) {
+ this.insert('[');
+ } else if (aChar === 93) {
+ this.insert(']');
+ } else if (aChar === 64) {
+ this.insert('@');
+ }
+
+};
+
+CursorMorph.prototype.cmd = function (aChar) {
+ if (aChar === 65) {
+ this.target.selectAll();
+ } else if (aChar === 90) {
+ this.undo();
+ }
+};
+
+CursorMorph.prototype.deleteRight = function () {
+ var text;
+ if (this.target.selection() !== '') {
+ this.gotoSlot(this.target.selectionStartSlot());
+ this.target.deleteSelection();
+ } else {
+ text = this.target.text;
+ this.target.changed();
+ text = text.slice(0, this.slot) + text.slice(this.slot + 1);
+ this.target.text = text;
+ this.target.drawNew();
+ }
+};
+
+CursorMorph.prototype.deleteLeft = function () {
+ var text;
+ if (this.target.selection()) {
+ this.gotoSlot(this.target.selectionStartSlot());
+ return this.target.deleteSelection();
+ }
+ text = this.target.text;
+ this.target.changed();
+ this.target.text = text.substring(0, this.slot - 1) +
+ text.substr(this.slot);
+ this.target.drawNew();
+ this.goLeft();
+};
+
+// CursorMorph destroying:
+
+CursorMorph.prototype.destroy = function () {
+ if (this.target.alignment !== this.originalAlignment) {
+ this.target.alignment = this.originalAlignment;
+ this.target.drawNew();
+ this.target.changed();
+ }
+ CursorMorph.uber.destroy.call(this);
+};
+
+// CursorMorph utilities:
+
+CursorMorph.prototype.inspectKeyEvent = function (event) {
+ // private
+ this.inform(
+ 'Key pressed: ' +
+ String.fromCharCode(event.charCode) +
+ '\n------------------------' +
+ '\ncharCode: ' +
+ event.charCode.toString() +
+ '\nkeyCode: ' +
+ event.keyCode.toString() +
+ '\nshiftKey: ' +
+ event.shiftKey.toString() +
+ '\naltKey: ' +
+ event.altKey.toString() +
+ '\nctrlKey: ' +
+ event.ctrlKey.toString() +
+ '\ncmdKey: ' +
+ event.metaKey.toString()
+ );
+};
+
+// BoxMorph ////////////////////////////////////////////////////////////
+
+// I can have an optionally rounded border
+
+var BoxMorph;
+
+// BoxMorph inherits from Morph:
+
+BoxMorph.prototype = new Morph();
+BoxMorph.prototype.constructor = BoxMorph;
+BoxMorph.uber = Morph.prototype;
+
+// BoxMorph instance creation:
+
+function BoxMorph(edge, border, borderColor) {
+ this.init(edge, border, borderColor);
+}
+
+BoxMorph.prototype.init = function (edge, border, borderColor) {
+ this.edge = edge || 4;
+ this.border = border || ((border === 0) ? 0 : 2);
+ this.borderColor = borderColor || new Color();
+ BoxMorph.uber.init.call(this);
+};
+
+// BoxMorph drawing:
+
+BoxMorph.prototype.drawNew = function () {
+ var context;
+
+ this.image = newCanvas(this.extent());
+ context = this.image.getContext('2d');
+ if ((this.edge === 0) && (this.border === 0)) {
+ BoxMorph.uber.drawNew.call(this);
+ return null;
+ }
+ context.fillStyle = this.color.toString();
+ context.beginPath();
+ this.outlinePath(
+ context,
+ Math.max(this.edge - this.border, 0),
+ this.border
+ );
+ context.closePath();
+ context.fill();
+ if (this.border > 0) {
+ context.lineWidth = this.border;
+ context.strokeStyle = this.borderColor.toString();
+ context.beginPath();
+ this.outlinePath(context, this.edge, this.border / 2);
+ context.closePath();
+ context.stroke();
+ }
+};
+
+BoxMorph.prototype.outlinePath = function (context, radius, inset) {
+ var offset = radius + inset,
+ w = this.width(),
+ h = this.height();
+
+ // top left:
+ context.arc(
+ offset,
+ offset,
+ radius,
+ radians(-180),
+ radians(-90),
+ false
+ );
+ // top right:
+ context.arc(
+ w - offset,
+ offset,
+ radius,
+ radians(-90),
+ radians(-0),
+ false
+ );
+ // bottom right:
+ context.arc(
+ w - offset,
+ h - offset,
+ radius,
+ radians(0),
+ radians(90),
+ false
+ );
+ // bottom left:
+ context.arc(
+ offset,
+ h - offset,
+ radius,
+ radians(90),
+ radians(180),
+ false
+ );
+};
+
+
+// BoxMorph menus:
+
+BoxMorph.prototype.developersMenu = function () {
+ var menu = BoxMorph.uber.developersMenu.call(this);
+ menu.addLine();
+ menu.addItem(
+ "border width...",
+ function () {
+ this.prompt(
+ menu.title + '\nborder\nwidth:',
+ this.setBorderWidth,
+ this,
+ this.border.toString(),
+ null,
+ 0,
+ 100,
+ true
+ );
+ },
+ 'set the border\'s\nline size'
+ );
+ menu.addItem(
+ "border color...",
+ function () {
+ this.pickColor(
+ menu.title + '\nborder color:',
+ this.setBorderColor,
+ this,
+ this.borderColor
+ );
+ },
+ 'set the border\'s\nline color'
+ );
+ menu.addItem(
+ "corner size...",
+ function () {
+ this.prompt(
+ menu.title + '\ncorner\nsize:',
+ this.setCornerSize,
+ this,
+ this.edge.toString(),
+ null,
+ 0,
+ 100,
+ true
+ );
+ },
+ 'set the corner\'s\nradius'
+ );
+ return menu;
+};
+
+BoxMorph.prototype.setBorderWidth = function (size) {
+ // for context menu demo purposes
+ var newSize;
+ if (typeof size === 'number') {
+ this.border = Math.max(size, 0);
+ } else {
+ newSize = parseFloat(size);
+ if (!isNaN(newSize)) {
+ this.border = Math.max(newSize, 0);
+ }
+ }
+ this.drawNew();
+ this.changed();
+};
+
+BoxMorph.prototype.setBorderColor = function (color) {
+ // for context menu demo purposes
+ if (color) {
+ this.borderColor = color;
+ this.drawNew();
+ this.changed();
+ }
+};
+
+BoxMorph.prototype.setCornerSize = function (size) {
+ // for context menu demo purposes
+ var newSize;
+ if (typeof size === 'number') {
+ this.edge = Math.max(size, 0);
+ } else {
+ newSize = parseFloat(size);
+ if (!isNaN(newSize)) {
+ this.edge = Math.max(newSize, 0);
+ }
+ }
+ this.drawNew();
+ this.changed();
+};
+
+BoxMorph.prototype.colorSetters = function () {
+ // for context menu demo purposes
+ return ['color', 'borderColor'];
+};
+
+BoxMorph.prototype.numericalSetters = function () {
+ // for context menu demo purposes
+ var list = BoxMorph.uber.numericalSetters.call(this);
+ list.push('setBorderWidth', 'setCornerSize');
+ return list;
+};
+
+// SpeechBubbleMorph ///////////////////////////////////////////////////
+
+/*
+ I am a comic-style speech bubble that can display either a string,
+ a Morph, a Canvas or a toString() representation of anything else.
+ If I am invoked using popUp() I behave like a tool tip.
+*/
+
+// SpeechBubbleMorph: referenced constructors
+
+var SpeechBubbleMorph;
+
+// SpeechBubbleMorph inherits from BoxMorph:
+
+SpeechBubbleMorph.prototype = new BoxMorph();
+SpeechBubbleMorph.prototype.constructor = SpeechBubbleMorph;
+SpeechBubbleMorph.uber = BoxMorph.prototype;
+
+// SpeechBubbleMorph instance creation:
+
+function SpeechBubbleMorph(
+ contents,
+ color,
+ edge,
+ border,
+ borderColor,
+ padding,
+ isThought
+) {
+ this.init(contents, color, edge, border, borderColor, padding, isThought);
+}
+
+SpeechBubbleMorph.prototype.init = function (
+ contents,
+ color,
+ edge,
+ border,
+ borderColor,
+ padding,
+ isThought
+) {
+ this.isPointingRight = true; // orientation of text
+ this.contents = contents || '';
+ this.padding = padding || 0; // additional vertical pixels
+ this.isThought = isThought || false; // draw "think" bubble
+ this.isClickable = false;
+ SpeechBubbleMorph.uber.init.call(
+ this,
+ edge || 6,
+ border || ((border === 0) ? 0 : 1),
+ borderColor || new Color(140, 140, 140)
+ );
+ this.color = color || new Color(230, 230, 230);
+ this.drawNew();
+};
+
+// SpeechBubbleMorph invoking:
+
+SpeechBubbleMorph.prototype.popUp = function (world, pos, isClickable) {
+ this.drawNew();
+ this.setPosition(pos.subtract(new Point(0, this.height())));
+ this.addShadow(new Point(2, 2), 80);
+ this.keepWithin(world);
+ world.add(this);
+ this.changed();
+ world.hand.destroyTemporaries();
+ world.hand.temporaries.push(this);
+
+ if (!isClickable) {
+ this.mouseEnter = function () {
+ this.destroy();
+ };
+ } else {
+ this.isClickable = true;
+ }
+};
+
+// SpeechBubbleMorph drawing:
+
+SpeechBubbleMorph.prototype.drawNew = function () {
+ // re-build my contents
+ if (this.contentsMorph) {
+ this.contentsMorph.destroy();
+ }
+ if (this.contents instanceof Morph) {
+ this.contentsMorph = this.contents;
+ } else if (isString(this.contents)) {
+ this.contentsMorph = new TextMorph(
+ this.contents,
+ MorphicPreferences.bubbleHelpFontSize,
+ null,
+ false,
+ true,
+ 'center'
+ );
+ } else if (this.contents instanceof HTMLCanvasElement) {
+ this.contentsMorph = new Morph();
+ this.contentsMorph.silentSetWidth(this.contents.width);
+ this.contentsMorph.silentSetHeight(this.contents.height);
+ this.contentsMorph.image = this.contents;
+ } else {
+ this.contentsMorph = new TextMorph(
+ this.contents.toString(),
+ MorphicPreferences.bubbleHelpFontSize,
+ null,
+ false,
+ true,
+ 'center'
+ );
+ }
+ this.add(this.contentsMorph);
+
+ // adjust my layout
+ this.silentSetWidth(this.contentsMorph.width() +
+ (this.padding ? this.padding * 2 : this.edge * 2));
+ this.silentSetHeight(this.contentsMorph.height() +
+ this.edge +
+ this.border * 2 +
+ this.padding * 2 +
+ 2);
+
+ // draw my outline
+ SpeechBubbleMorph.uber.drawNew.call(this);
+
+ // position my contents
+ this.contentsMorph.setPosition(this.position().add(
+ new Point(
+ this.padding || this.edge,
+ this.border + this.padding + 1
+ )
+ ));
+};
+
+SpeechBubbleMorph.prototype.outlinePath = function (
+ context,
+ radius,
+ inset
+) {
+ var offset = radius + inset,
+ w = this.width(),
+ h = this.height(),
+ rad;
+
+ function circle(x, y, r) {
+ context.moveTo(x + r, y);
+ context.arc(x, y, r, radians(0), radians(360));
+ }
+
+ // top left:
+ context.arc(
+ offset,
+ offset,
+ radius,
+ radians(-180),
+ radians(-90),
+ false
+ );
+ // top right:
+ context.arc(
+ w - offset,
+ offset,
+ radius,
+ radians(-90),
+ radians(-0),
+ false
+ );
+ // bottom right:
+ context.arc(
+ w - offset,
+ h - offset - radius,
+ radius,
+ radians(0),
+ radians(90),
+ false
+ );
+ if (!this.isThought) { // draw speech bubble hook
+ if (this.isPointingRight) {
+ context.lineTo(
+ offset + radius,
+ h - offset
+ );
+ context.lineTo(
+ radius / 2 + inset,
+ h - inset
+ );
+ } else { // pointing left
+ context.lineTo(
+ w - (radius / 2 + inset),
+ h - inset
+ );
+ context.lineTo(
+ w - (offset + radius),
+ h - offset
+ );
+ }
+ }
+ // bottom left:
+ context.arc(
+ offset,
+ h - offset - radius,
+ radius,
+ radians(90),
+ radians(180),
+ false
+ );
+ if (this.isThought) {
+ // close large bubble:
+ context.lineTo(
+ inset,
+ offset
+ );
+ // draw thought bubbles:
+ if (this.isPointingRight) {
+ // tip bubble:
+ rad = radius / 4;
+ circle(rad + inset, h - rad - inset, rad);
+ // middle bubble:
+ rad = radius / 3.2;
+ circle(rad * 2 + inset, h - rad - inset * 2, rad);
+ // top bubble:
+ rad = radius / 2.8;
+ circle(rad * 3 + inset * 2, h - rad - inset * 4, rad);
+ } else { // pointing left
+ // tip bubble:
+ rad = radius / 4;
+ circle(w - (rad + inset), h - rad - inset, rad);
+ // middle bubble:
+ rad = radius / 3.2;
+ circle(w - (rad * 2 + inset), h - rad - inset * 2, rad);
+ // top bubble:
+ rad = radius / 2.8;
+ circle(w - (rad * 3 + inset * 2), h - rad - inset * 4, rad);
+ }
+ }
+};
+
+// SpeechBubbleMorph shadow
+
+/*
+ only take the 'plain' image, so the box rounding and the
+ shadow doesn't become conflicted by embedded scrolling panes
+*/
+
+SpeechBubbleMorph.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;
+};
+
+SpeechBubbleMorph.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;
+};
+
+// SpeechBubbleMorph resizing
+
+SpeechBubbleMorph.prototype.fixLayout = function () {
+ this.removeShadow();
+ this.drawNew();
+ this.addShadow(new Point(2, 2), 80);
+};
+
+// CircleBoxMorph //////////////////////////////////////////////////////
+
+// I can be used for sliders
+
+var CircleBoxMorph;
+
+// CircleBoxMorph inherits from Morph:
+
+CircleBoxMorph.prototype = new Morph();
+CircleBoxMorph.prototype.constructor = CircleBoxMorph;
+CircleBoxMorph.uber = Morph.prototype;
+
+function CircleBoxMorph(orientation) {
+ this.init(orientation || 'vertical');
+}
+
+CircleBoxMorph.prototype.init = function (orientation) {
+ CircleBoxMorph.uber.init.call(this);
+ this.orientation = orientation;
+ this.autoOrient = true;
+ this.setExtent(new Point(20, 100));
+};
+
+CircleBoxMorph.prototype.autoOrientation = function () {
+ if (this.height() > this.width()) {
+ this.orientation = 'vertical';
+ } else {
+ this.orientation = 'horizontal';
+ }
+};
+
+CircleBoxMorph.prototype.drawNew = function () {
+ var radius, center1, center2, rect, points, x, y,
+ context, ext,
+ myself = this;
+
+ if (this.autoOrient) {
+ this.autoOrientation();
+ }
+ this.image = newCanvas(this.extent());
+ context = this.image.getContext('2d');
+
+ if (this.orientation === 'vertical') {
+ radius = this.width() / 2;
+ x = this.center().x;
+ center1 = new Point(x, this.top() + radius);
+ center2 = new Point(x, this.bottom() - radius);
+ rect = this.bounds.origin.add(new Point(0, radius)).corner(
+ this.bounds.corner.subtract(new Point(0, radius))
+ );
+ } else {
+ radius = this.height() / 2;
+ y = this.center().y;
+ center1 = new Point(this.left() + radius, y);
+ center2 = new Point(this.right() - radius, y);
+ rect = this.bounds.origin.add(new Point(radius, 0)).corner(
+ this.bounds.corner.subtract(new Point(radius, 0))
+ );
+ }
+ points = [ center1.subtract(this.bounds.origin),
+ center2.subtract(this.bounds.origin)];
+ points.forEach(function (center) {
+ context.fillStyle = myself.color.toString();
+ context.beginPath();
+ context.arc(
+ center.x,
+ center.y,
+ radius,
+ 0,
+ 2 * Math.PI,
+ false
+ );
+ context.closePath();
+ context.fill();
+ });
+ rect = rect.translateBy(this.bounds.origin.neg());
+ ext = rect.extent();
+ if (ext.x > 0 && ext.y > 0) {
+ context.fillRect(
+ rect.origin.x,
+ rect.origin.y,
+ rect.width(),
+ rect.height()
+ );
+ }
+};
+
+// CircleBoxMorph menu:
+
+CircleBoxMorph.prototype.developersMenu = function () {
+ var menu = CircleBoxMorph.uber.developersMenu.call(this);
+ menu.addLine();
+ if (this.orientation === 'vertical') {
+ menu.addItem(
+ "horizontal...",
+ 'toggleOrientation',
+ 'toggle the\norientation'
+ );
+ } else {
+ menu.addItem(
+ "vertical...",
+ 'toggleOrientation',
+ 'toggle the\norientation'
+ );
+ }
+ return menu;
+};
+
+CircleBoxMorph.prototype.toggleOrientation = function () {
+ var center = this.center();
+ this.changed();
+ if (this.orientation === 'vertical') {
+ this.orientation = 'horizontal';
+ } else {
+ this.orientation = 'vertical';
+ }
+ this.silentSetExtent(new Point(this.height(), this.width()));
+ this.setCenter(center);
+ this.drawNew();
+ this.changed();
+};
+
+// SliderButtonMorph ///////////////////////////////////////////////////
+
+var SliderButtonMorph;
+
+// SliderButtonMorph inherits from CircleBoxMorph:
+
+SliderButtonMorph.prototype = new CircleBoxMorph();
+SliderButtonMorph.prototype.constructor = SliderButtonMorph;
+SliderButtonMorph.uber = CircleBoxMorph.prototype;
+
+function SliderButtonMorph(orientation) {
+ this.init(orientation);
+}
+
+SliderButtonMorph.prototype.init = function (orientation) {
+ this.color = new Color(80, 80, 80);
+ this.highlightColor = new Color(90, 90, 140);
+ this.pressColor = new Color(80, 80, 160);
+ this.is3D = true;
+ this.hasMiddleDip = true;
+ SliderButtonMorph.uber.init.call(this, orientation);
+};
+
+SliderButtonMorph.prototype.autoOrientation = function () {
+ nop();
+};
+
+SliderButtonMorph.prototype.drawNew = function () {
+ var colorBak = this.color.copy();
+
+ SliderButtonMorph.uber.drawNew.call(this);
+ if (this.is3D) {
+ this.drawEdges();
+ }
+ this.normalImage = this.image;
+
+ this.color = this.highlightColor.copy();
+ SliderButtonMorph.uber.drawNew.call(this);
+ if (this.is3D) {
+ this.drawEdges();
+ }
+ this.highlightImage = this.image;
+
+ this.color = this.pressColor.copy();
+ SliderButtonMorph.uber.drawNew.call(this);
+ if (this.is3D) {
+ this.drawEdges();
+ }
+ this.pressImage = this.image;
+
+ this.color = colorBak;
+ this.image = this.normalImage;
+
+};
+
+SliderButtonMorph.prototype.drawEdges = function () {
+ var context = this.image.getContext('2d'),
+ gradient,
+ radius,
+ w = this.width(),
+ h = this.height();
+
+ context.lineJoin = 'round';
+ context.lineCap = 'round';
+
+ if (this.orientation === 'vertical') {
+ context.lineWidth = w / 3;
+ gradient = context.createLinearGradient(
+ 0,
+ 0,
+ context.lineWidth,
+ 0
+ );
+ gradient.addColorStop(0, 'white');
+ gradient.addColorStop(1, this.color.toString());
+
+ context.strokeStyle = gradient;
+ context.beginPath();
+ context.moveTo(context.lineWidth * 0.5, w / 2);
+ context.lineTo(context.lineWidth * 0.5, h - w / 2);
+ context.stroke();
+
+ gradient = context.createLinearGradient(
+ w - context.lineWidth,
+ 0,
+ w,
+ 0
+ );
+ gradient.addColorStop(0, this.color.toString());
+ gradient.addColorStop(1, 'black');
+
+ context.strokeStyle = gradient;
+ context.beginPath();
+ context.moveTo(w - context.lineWidth * 0.5, w / 2);
+ context.lineTo(w - context.lineWidth * 0.5, h - w / 2);
+ context.stroke();
+
+ if (this.hasMiddleDip) {
+ gradient = context.createLinearGradient(
+ context.lineWidth,
+ 0,
+ w - context.lineWidth,
+ 0
+ );
+
+ radius = w / 4;
+ gradient.addColorStop(0, 'black');
+ gradient.addColorStop(0.35, this.color.toString());
+ gradient.addColorStop(0.65, this.color.toString());
+ gradient.addColorStop(1, 'white');
+
+ context.fillStyle = gradient;
+ context.beginPath();
+ context.arc(
+ w / 2,
+ h / 2,
+ radius,
+ radians(0),
+ radians(360),
+ false
+ );
+ context.closePath();
+ context.fill();
+ }
+ } else if (this.orientation === 'horizontal') {
+ context.lineWidth = h / 3;
+ gradient = context.createLinearGradient(
+ 0,
+ 0,
+ 0,
+ context.lineWidth
+ );
+ gradient.addColorStop(0, 'white');
+ gradient.addColorStop(1, this.color.toString());
+
+ context.strokeStyle = gradient;
+ context.beginPath();
+ context.moveTo(h / 2, context.lineWidth * 0.5);
+ context.lineTo(w - h / 2, context.lineWidth * 0.5);
+ context.stroke();
+
+ gradient = context.createLinearGradient(
+ 0,
+ h - context.lineWidth,
+ 0,
+ h
+ );
+ gradient.addColorStop(0, this.color.toString());
+ gradient.addColorStop(1, 'black');
+
+ context.strokeStyle = gradient;
+ context.beginPath();
+ context.moveTo(h / 2, h - context.lineWidth * 0.5);
+ context.lineTo(w - h / 2, h - context.lineWidth * 0.5);
+ context.stroke();
+
+ if (this.hasMiddleDip) {
+ gradient = context.createLinearGradient(
+ 0,
+ context.lineWidth,
+ 0,
+ h - context.lineWidth
+ );
+
+ radius = h / 4;
+ gradient.addColorStop(0, 'black');
+ gradient.addColorStop(0.35, this.color.toString());
+ gradient.addColorStop(0.65, this.color.toString());
+ gradient.addColorStop(1, 'white');
+
+ context.fillStyle = gradient;
+ context.beginPath();
+ context.arc(
+ this.width() / 2,
+ this.height() / 2,
+ radius,
+ radians(0),
+ radians(360),
+ false
+ );
+ context.closePath();
+ context.fill();
+ }
+ }
+};
+
+//SliderButtonMorph events:
+
+SliderButtonMorph.prototype.mouseEnter = function () {
+ this.image = this.highlightImage;
+ this.changed();
+};
+
+SliderButtonMorph.prototype.mouseLeave = function () {
+ this.image = this.normalImage;
+ this.changed();
+};
+
+SliderButtonMorph.prototype.mouseDownLeft = function (pos) {
+ this.image = this.pressImage;
+ this.changed();
+ this.escalateEvent('mouseDownLeft', pos);
+};
+
+SliderButtonMorph.prototype.mouseClickLeft = function () {
+ this.image = this.highlightImage;
+ this.changed();
+};
+
+SliderButtonMorph.prototype.mouseMove = function () {
+ // prevent my parent from getting picked up
+ nop();
+};
+
+// SliderMorph ///////////////////////////////////////////////////
+
+// SliderMorph inherits from CircleBoxMorph:
+
+SliderMorph.prototype = new CircleBoxMorph();
+SliderMorph.prototype.constructor = SliderMorph;
+SliderMorph.uber = CircleBoxMorph.prototype;
+
+function SliderMorph(start, stop, value, size, orientation, color) {
+ this.init(
+ start || 1,
+ stop || 100,
+ value || 50,
+ size || 10,
+ orientation || 'vertical',
+ color
+ );
+}
+
+SliderMorph.prototype.init = function (
+ start,
+ stop,
+ value,
+ size,
+ orientation,
+ color
+) {
+ this.target = null;
+ this.action = null;
+ this.start = start;
+ this.stop = stop;
+ this.value = value;
+ this.size = size;
+ this.offset = null;
+ this.button = new SliderButtonMorph();
+ this.button.isDraggable = false;
+ this.button.color = new Color(200, 200, 200);
+ this.button.highlightColor = new Color(210, 210, 255);
+ this.button.pressColor = new Color(180, 180, 255);
+ SliderMorph.uber.init.call(this, orientation);
+ this.add(this.button);
+ this.alpha = 0.3;
+ this.color = color || new Color(0, 0, 0);
+ this.setExtent(new Point(20, 100));
+ // this.drawNew();
+};
+
+SliderMorph.prototype.autoOrientation = function () {
+ nop();
+};
+
+SliderMorph.prototype.rangeSize = function () {
+ return this.stop - this.start;
+};
+
+SliderMorph.prototype.ratio = function () {
+ return this.size / this.rangeSize();
+};
+
+SliderMorph.prototype.unitSize = function () {
+ if (this.orientation === 'vertical') {
+ return (this.height() - this.button.height()) /
+ this.rangeSize();
+ }
+ return (this.width() - this.button.width()) /
+ this.rangeSize();
+};
+
+SliderMorph.prototype.drawNew = function () {
+ var bw, bh, posX, posY;
+
+ SliderMorph.uber.drawNew.call(this);
+ this.button.orientation = this.orientation;
+ if (this.orientation === 'vertical') {
+ bw = this.width() - 2;
+ bh = Math.max(bw, Math.round(this.height() * this.ratio()));
+ this.button.silentSetExtent(new Point(bw, bh));
+ posX = 1;
+ posY = Math.min(
+ Math.round((this.value - this.start) * this.unitSize()),
+ this.height() - this.button.height()
+ );
+ } else {
+ bh = this.height() - 2;
+ bw = Math.max(bh, Math.round(this.width() * this.ratio()));
+ this.button.silentSetExtent(new Point(bw, bh));
+ posY = 1;
+ posX = Math.min(
+ Math.round((this.value - this.start) * this.unitSize()),
+ this.width() - this.button.width()
+ );
+ }
+ this.button.setPosition(
+ new Point(posX, posY).add(this.bounds.origin)
+ );
+ this.button.drawNew();
+ this.button.changed();
+};
+
+SliderMorph.prototype.updateValue = function () {
+ var relPos;
+ if (this.orientation === 'vertical') {
+ relPos = this.button.top() - this.top();
+ } else {
+ relPos = this.button.left() - this.left();
+ }
+ this.value = Math.round(relPos / this.unitSize() + this.start);
+ this.updateTarget();
+};
+
+SliderMorph.prototype.updateTarget = function () {
+ if (this.action) {
+ if (typeof this.action === 'function') {
+ this.action.call(this.target, this.value);
+ } else { // assume it's a String
+ this.target[this.action](this.value);
+ }
+ }
+};
+
+// SliderMorph duplicating:
+
+SliderMorph.prototype.copyRecordingReferences = function (dict) {
+ // inherited, see comment in Morph
+ var c = SliderMorph.uber.copyRecordingReferences.call(
+ this,
+ dict
+ );
+ if (c.target && dict[this.target]) {
+ c.target = (dict[this.target]);
+ }
+ if (c.button && dict[this.button]) {
+ c.button = (dict[this.button]);
+ }
+ return c;
+};
+
+// SliderMorph menu:
+
+SliderMorph.prototype.developersMenu = function () {
+ var menu = SliderMorph.uber.developersMenu.call(this);
+ menu.addItem(
+ "show value...",
+ 'showValue',
+ 'display a dialog box\nshowing the selected number'
+ );
+ menu.addItem(
+ "floor...",
+ function () {
+ this.prompt(
+ menu.title + '\nfloor:',
+ this.setStart,
+ this,
+ this.start.toString(),
+ null,
+ 0,
+ this.stop - this.size,
+ true
+ );
+ },
+ 'set the minimum value\nwhich can be selected'
+ );
+ menu.addItem(
+ "ceiling...",
+ function () {
+ this.prompt(
+ menu.title + '\nceiling:',
+ this.setStop,
+ this,
+ this.stop.toString(),
+ null,
+ this.start + this.size,
+ this.size * 100,
+ true
+ );
+ },
+ 'set the maximum value\nwhich can be selected'
+ );
+ menu.addItem(
+ "button size...",
+ function () {
+ this.prompt(
+ menu.title + '\nbutton size:',
+ this.setSize,
+ this,
+ this.size.toString(),
+ null,
+ 1,
+ this.stop - this.start,
+ true
+ );
+ },
+ 'set the range\ncovered by\nthe slider button'
+ );
+ menu.addLine();
+ menu.addItem(
+ 'set target',
+ "setTarget",
+ 'select another morph\nwhose numerical property\nwill be ' +
+ 'controlled by this one'
+ );
+ return menu;
+};
+
+SliderMorph.prototype.showValue = function () {
+ this.inform(this.value);
+};
+
+SliderMorph.prototype.userSetStart = function (num) {
+ // for context menu demo purposes
+ this.start = Math.max(num, this.stop);
+};
+
+SliderMorph.prototype.setStart = function (num) {
+ // for context menu demo purposes
+ var newStart;
+ if (typeof num === 'number') {
+ this.start = Math.min(
+ Math.max(num, 0),
+ this.stop - this.size
+ );
+ } else {
+ newStart = parseFloat(num);
+ if (!isNaN(newStart)) {
+ this.start = Math.min(
+ Math.max(newStart, 0),
+ this.stop - this.size
+ );
+ }
+ }
+ this.value = Math.max(this.value, this.start);
+ this.updateTarget();
+ this.drawNew();
+ this.changed();
+};
+
+SliderMorph.prototype.setStop = function (num) {
+ // for context menu demo purposes
+ var newStop;
+ if (typeof num === 'number') {
+ this.stop = Math.max(num, this.start + this.size);
+ } else {
+ newStop = parseFloat(num);
+ if (!isNaN(newStop)) {
+ this.stop = Math.max(newStop, this.start + this.size);
+ }
+ }
+ this.value = Math.min(this.value, this.stop);
+ this.updateTarget();
+ this.drawNew();
+ this.changed();
+};
+
+SliderMorph.prototype.setSize = function (num) {
+ // for context menu demo purposes
+ var newSize;
+ if (typeof num === 'number') {
+ this.size = Math.min(
+ Math.max(num, 1),
+ this.stop - this.start
+ );
+ } else {
+ newSize = parseFloat(num);
+ if (!isNaN(newSize)) {
+ this.size = Math.min(
+ Math.max(newSize, 1),
+ this.stop - this.start
+ );
+ }
+ }
+ this.value = Math.min(this.value, this.stop - this.size);
+ this.updateTarget();
+ this.drawNew();
+ this.changed();
+};
+
+SliderMorph.prototype.setTarget = function () {
+ var choices = this.overlappedMorphs(),
+ menu = new MenuMorph(this, 'choose target:'),
+ myself = this;
+
+ choices.push(this.world());
+ choices.forEach(function (each) {
+ menu.addItem(each.toString().slice(0, 50), function () {
+ myself.target = each;
+ myself.setTargetSetter();
+ });
+ });
+ if (choices.length === 1) {
+ this.target = choices[0];
+ this.setTargetSetter();
+ } else if (choices.length > 0) {
+ menu.popUpAtHand(this.world());
+ }
+};
+
+SliderMorph.prototype.setTargetSetter = function () {
+ var choices = this.target.numericalSetters(),
+ menu = new MenuMorph(this, 'choose target property:'),
+ myself = this;
+
+ choices.forEach(function (each) {
+ menu.addItem(each, function () {
+ myself.action = each;
+ });
+ });
+ if (choices.length === 1) {
+ this.action = choices[0];
+ } else if (choices.length > 0) {
+ menu.popUpAtHand(this.world());
+ }
+};
+
+SliderMorph.prototype.numericalSetters = function () {
+ // for context menu demo purposes
+ var list = SliderMorph.uber.numericalSetters.call(this);
+ list.push('setStart', 'setStop', 'setSize');
+ return list;
+};
+
+// SliderMorph stepping:
+
+SliderMorph.prototype.step = null;
+
+SliderMorph.prototype.mouseDownLeft = function (pos) {
+ var world, myself = this;
+
+ if (!this.button.bounds.containsPoint(pos)) {
+ this.offset = new Point(); // return null;
+ } else {
+ this.offset = pos.subtract(this.button.bounds.origin);
+ }
+ world = this.root();
+ this.step = function () {
+ var mousePos, newX, newY;
+ if (world.hand.mouseButton) {
+ mousePos = world.hand.bounds.origin;
+ if (myself.orientation === 'vertical') {
+ newX = myself.button.bounds.origin.x;
+ newY = Math.max(
+ Math.min(
+ mousePos.y - myself.offset.y,
+ myself.bottom() - myself.button.height()
+ ),
+ myself.top()
+ );
+ } else {
+ newY = myself.button.bounds.origin.y;
+ newX = Math.max(
+ Math.min(
+ mousePos.x - myself.offset.x,
+ myself.right() - myself.button.width()
+ ),
+ myself.left()
+ );
+ }
+ myself.button.setPosition(new Point(newX, newY));
+ myself.updateValue();
+ } else {
+ this.step = null;
+ }
+ };
+};
+
+// MouseSensorMorph ////////////////////////////////////////////////////
+
+// for demo and debuggin purposes only, to be removed later
+
+var MouseSensorMorph;
+
+// MouseSensorMorph inherits from BoxMorph:
+
+MouseSensorMorph.prototype = new BoxMorph();
+MouseSensorMorph.prototype.constructor = MouseSensorMorph;
+MouseSensorMorph.uber = BoxMorph.prototype;
+
+// MouseSensorMorph instance creation:
+
+function MouseSensorMorph(edge, border, borderColor) {
+ this.init(edge, border, borderColor);
+}
+
+MouseSensorMorph.prototype.init = function (edge, border, borderColor) {
+ MouseSensorMorph.uber.init.call(this);
+ this.edge = edge || 4;
+ this.border = border || 2;
+ this.color = new Color(255, 255, 255);
+ this.borderColor = borderColor || new Color();
+ this.isTouched = false;
+ this.upStep = 0.05;
+ this.downStep = 0.02;
+ this.noticesTransparentClick = false;
+ this.drawNew();
+};
+
+MouseSensorMorph.prototype.touch = function () {
+ var myself = this;
+ if (!this.isTouched) {
+ this.isTouched = true;
+ this.alpha = 0.6;
+
+ this.step = function () {
+ if (myself.isTouched) {
+ if (myself.alpha < 1) {
+ myself.alpha = myself.alpha + myself.upStep;
+ }
+ } else if (myself.alpha > (myself.downStep)) {
+ myself.alpha = myself.alpha - myself.downStep;
+ } else {
+ myself.alpha = 0;
+ myself.step = null;
+ }
+ myself.changed();
+ };
+ }
+};
+
+MouseSensorMorph.prototype.unTouch = function () {
+ this.isTouched = false;
+};
+
+MouseSensorMorph.prototype.mouseEnter = function () {
+ this.touch();
+};
+
+MouseSensorMorph.prototype.mouseLeave = function () {
+ this.unTouch();
+};
+
+MouseSensorMorph.prototype.mouseDownLeft = function () {
+ this.touch();
+};
+
+MouseSensorMorph.prototype.mouseClickLeft = function () {
+ this.unTouch();
+};
+
+// InspectorMorph //////////////////////////////////////////////////////
+
+// InspectorMorph: referenced constructors
+
+var ListMorph;
+var TriggerMorph;
+
+// InspectorMorph inherits from BoxMorph:
+
+InspectorMorph.prototype = new BoxMorph();
+InspectorMorph.prototype.constructor = InspectorMorph;
+InspectorMorph.uber = BoxMorph.prototype;
+
+// InspectorMorph instance creation:
+
+function InspectorMorph(target) {
+ this.init(target);
+}
+
+InspectorMorph.prototype.init = function (target) {
+ // additional properties:
+ this.target = target;
+ this.currentProperty = null;
+ this.showing = 'attributes';
+ this.markOwnProperties = false;
+
+ // initialize inherited properties:
+ InspectorMorph.uber.init.call(this);
+
+ // override inherited properties:
+ this.silentSetExtent(
+ new Point(
+ MorphicPreferences.handleSize * 20,
+ MorphicPreferences.handleSize * 20 * 2 / 3
+ )
+ );
+ this.isDraggable = true;
+ this.border = 1;
+ this.edge = 5;
+ this.color = new Color(60, 60, 60);
+ this.borderColor = new Color(95, 95, 95);
+ this.drawNew();
+
+ // panes:
+ this.label = null;
+ this.list = null;
+ this.detail = null;
+ this.work = null;
+ this.buttonInspect = null;
+ this.buttonClose = null;
+ this.buttonSubset = null;
+ this.buttonEdit = null;
+ this.resizer = null;
+
+ if (this.target) {
+ this.buildPanes();
+ }
+};
+
+InspectorMorph.prototype.setTarget = function (target) {
+ this.target = target;
+ this.currentProperty = null;
+ this.buildPanes();
+};
+
+InspectorMorph.prototype.buildPanes = function () {
+ var attribs = [], property, myself = this, ctrl, ev;
+
+ // remove existing panes
+ this.children.forEach(function (m) {
+ if (m !== this.work) { // keep work pane around
+ m.destroy();
+ }
+ });
+ this.children = [];
+
+ // label
+ this.label = new TextMorph(this.target.toString());
+ this.label.fontSize = MorphicPreferences.menuFontSize;
+ this.label.isBold = true;
+ this.label.color = new Color(255, 255, 255);
+ this.label.drawNew();
+ this.add(this.label);
+
+ // properties list
+ for (property in this.target) {
+ if (property) { // dummy condition, to be refined
+ attribs.push(property);
+ }
+ }
+ if (this.showing === 'attributes') {
+ attribs = attribs.filter(function (prop) {
+ return typeof myself.target[prop] !== 'function';
+ });
+ } else if (this.showing === 'methods') {
+ attribs = attribs.filter(function (prop) {
+ return typeof myself.target[prop] === 'function';
+ });
+ } // otherwise show all properties
+ this.list = new ListMorph(
+ this.target instanceof Array ? attribs : attribs.sort(),
+ null, // label getter
+ this.markOwnProperties ?
+ [ // format list
+ [ // format element: [color, predicate(element]
+ new Color(0, 0, 180),
+ function (element) {
+ return myself.target.hasOwnProperty(element);
+ }
+ ]
+ ]
+ : null
+ );
+ this.list.action = function (selected) {
+ var val, txt, cnts;
+ val = myself.target[selected];
+ myself.currentProperty = val;
+ if (val === null) {
+ txt = 'NULL';
+ } else if (isString(val)) {
+ txt = val;
+ } else {
+ txt = val.toString();
+ }
+ cnts = new TextMorph(txt);
+ cnts.isEditable = true;
+ cnts.enableSelecting();
+ cnts.setReceiver(myself.target);
+ myself.detail.setContents(cnts);
+ };
+ this.list.hBar.alpha = 0.6;
+ this.list.vBar.alpha = 0.6;
+ this.list.contents.step = null;
+ this.add(this.list);
+
+ // details pane
+ this.detail = new ScrollFrameMorph();
+ this.detail.acceptsDrops = false;
+ this.detail.contents.acceptsDrops = false;
+ this.detail.isTextLineWrapping = true;
+ this.detail.color = new Color(255, 255, 255);
+ this.detail.hBar.alpha = 0.6;
+ this.detail.vBar.alpha = 0.6;
+ ctrl = new TextMorph('');
+ ctrl.isEditable = true;
+ ctrl.enableSelecting();
+ ctrl.setReceiver(this.target);
+ this.detail.setContents(ctrl);
+ this.add(this.detail);
+
+ // work ('evaluation') pane
+ // don't refresh the work pane if it already exists
+ if (this.work === null) {
+ this.work = new ScrollFrameMorph();
+ this.work.acceptsDrops = false;
+ this.work.contents.acceptsDrops = false;
+ this.work.isTextLineWrapping = true;
+ this.work.color = new Color(255, 255, 255);
+ this.work.hBar.alpha = 0.6;
+ this.work.vBar.alpha = 0.6;
+ ev = new TextMorph('');
+ ev.isEditable = true;
+ ev.enableSelecting();
+ ev.setReceiver(this.target);
+ this.work.setContents(ev);
+ }
+ this.add(this.work);
+
+ // properties button
+ this.buttonSubset = new TriggerMorph();
+ this.buttonSubset.labelString = 'show...';
+ this.buttonSubset.action = function () {
+ var menu;
+ menu = new MenuMorph();
+ menu.addItem(
+ 'attributes',
+ function () {
+ myself.showing = 'attributes';
+ myself.buildPanes();
+ }
+ );
+ menu.addItem(
+ 'methods',
+ function () {
+ myself.showing = 'methods';
+ myself.buildPanes();
+ }
+ );
+ menu.addItem(
+ 'all',
+ function () {
+ myself.showing = 'all';
+ myself.buildPanes();
+ }
+ );
+ menu.addLine();
+ menu.addItem(
+ (myself.markOwnProperties ?
+ 'un-mark own' : 'mark own'),
+ function () {
+ myself.markOwnProperties = !myself.markOwnProperties;
+ myself.buildPanes();
+ },
+ 'highlight\n\'own\' properties'
+ );
+ menu.popUpAtHand(myself.world());
+ };
+ this.add(this.buttonSubset);
+
+ // inspect button
+ this.buttonInspect = new TriggerMorph();
+ this.buttonInspect.labelString = 'inspect...';
+ this.buttonInspect.action = function () {
+ var menu, world, inspector;
+ if (isObject(myself.currentProperty)) {
+ menu = new MenuMorph();
+ menu.addItem(
+ 'in new inspector...',
+ function () {
+ world = myself.world();
+ inspector = new InspectorMorph(
+ myself.currentProperty
+ );
+ inspector.setPosition(world.hand.position());
+ inspector.keepWithin(world);
+ world.add(inspector);
+ inspector.changed();
+ }
+ );
+ menu.addItem(
+ 'here...',
+ function () {
+ myself.setTarget(myself.currentProperty);
+ }
+ );
+ menu.popUpAtHand(myself.world());
+ } else {
+ myself.inform(
+ (myself.currentProperty === null ?
+ 'null' : typeof myself.currentProperty) +
+ '\nis not inspectable'
+ );
+ }
+ };
+ this.add(this.buttonInspect);
+
+ // edit button
+
+ this.buttonEdit = new TriggerMorph();
+ this.buttonEdit.labelString = 'edit...';
+ this.buttonEdit.action = function () {
+ var menu;
+ menu = new MenuMorph(myself);
+ menu.addItem("save", 'save', 'accept changes');
+ menu.addLine();
+ menu.addItem("add property...", 'addProperty');
+ menu.addItem("rename...", 'renameProperty');
+ menu.addItem("remove...", 'removeProperty');
+ menu.popUpAtHand(myself.world());
+ };
+ this.add(this.buttonEdit);
+
+ // close button
+ this.buttonClose = new TriggerMorph();
+ this.buttonClose.labelString = 'close';
+ this.buttonClose.action = function () {
+ myself.destroy();
+ };
+ this.add(this.buttonClose);
+
+ // resizer
+ this.resizer = new HandleMorph(
+ this,
+ 150,
+ 100,
+ this.edge,
+ this.edge
+ );
+
+ // update layout
+ this.fixLayout();
+};
+
+InspectorMorph.prototype.fixLayout = function () {
+ var x, y, r, b, w, h;
+
+ Morph.prototype.trackChanges = false;
+
+ // label
+ x = this.left() + this.edge;
+ y = this.top() + this.edge;
+ r = this.right() - this.edge;
+ w = r - x;
+ this.label.setPosition(new Point(x, y));
+ this.label.setWidth(w);
+ if (this.label.height() > (this.height() - 50)) {
+ this.silentSetHeight(this.label.height() + 50);
+ this.drawNew();
+ this.changed();
+ this.resizer.drawNew();
+ }
+
+ // list
+ y = this.label.bottom() + 2;
+ w = Math.min(
+ Math.floor(this.width() / 3),
+ this.list.listContents.width()
+ );
+
+ w -= this.edge;
+ b = this.bottom() - (2 * this.edge) -
+ MorphicPreferences.handleSize;
+ h = b - y;
+ this.list.setPosition(new Point(x, y));
+ this.list.setExtent(new Point(w, h));
+
+ // detail
+ x = this.list.right() + this.edge;
+ r = this.right() - this.edge;
+ w = r - x;
+ this.detail.setPosition(new Point(x, y));
+ this.detail.setExtent(new Point(w, (h * 2 / 3) - this.edge));
+
+ // work
+ y = this.detail.bottom() + this.edge;
+ this.work.setPosition(new Point(x, y));
+ this.work.setExtent(new Point(w, h / 3));
+
+ // properties button
+ x = this.list.left();
+ y = this.list.bottom() + this.edge;
+ w = this.list.width();
+ h = MorphicPreferences.handleSize;
+ this.buttonSubset.setPosition(new Point(x, y));
+ this.buttonSubset.setExtent(new Point(w, h));
+
+ // inspect button
+ x = this.detail.left();
+ w = this.detail.width() - this.edge -
+ MorphicPreferences.handleSize;
+ w = w / 3 - this.edge / 3;
+ this.buttonInspect.setPosition(new Point(x, y));
+ this.buttonInspect.setExtent(new Point(w, h));
+
+ // edit button
+ x = this.buttonInspect.right() + this.edge;
+ this.buttonEdit.setPosition(new Point(x, y));
+ this.buttonEdit.setExtent(new Point(w, h));
+
+ // close button
+ x = this.buttonEdit.right() + this.edge;
+ r = this.detail.right() - this.edge -
+ MorphicPreferences.handleSize;
+ w = r - x;
+ this.buttonClose.setPosition(new Point(x, y));
+ this.buttonClose.setExtent(new Point(w, h));
+
+ Morph.prototype.trackChanges = true;
+ this.changed();
+
+};
+
+InspectorMorph.prototype.setExtent = function (aPoint) {
+ InspectorMorph.uber.setExtent.call(this, aPoint);
+ this.fixLayout();
+};
+
+//InspectorMorph editing ops:
+
+InspectorMorph.prototype.save = function () {
+ var txt = this.detail.contents.children[0].text.toString(),
+ prop = this.list.selected;
+ try {
+ // this.target[prop] = evaluate(txt);
+ this.target.evaluateString('this.' + prop + ' = ' + txt);
+ if (this.target.drawNew) {
+ this.target.changed();
+ this.target.drawNew();
+ this.target.changed();
+ }
+ } catch (err) {
+ this.inform(err);
+ }
+};
+
+InspectorMorph.prototype.addProperty = function () {
+ var myself = this;
+ this.prompt(
+ 'new property name:',
+ function (prop) {
+ if (prop) {
+ myself.target[prop] = null;
+ myself.buildPanes();
+ if (myself.target.drawNew) {
+ myself.target.changed();
+ myself.target.drawNew();
+ myself.target.changed();
+ }
+ }
+ },
+ this,
+ 'property' // Chrome cannot handle empty strings (others do)
+ );
+};
+
+InspectorMorph.prototype.renameProperty = function () {
+ var myself = this,
+ propertyName = this.list.selected;
+ this.prompt(
+ 'property name:',
+ function (prop) {
+ try {
+ delete (myself.target[propertyName]);
+ myself.target[prop] = myself.currentProperty;
+ } catch (err) {
+ myself.inform(err);
+ }
+ myself.buildPanes();
+ if (myself.target.drawNew) {
+ myself.target.changed();
+ myself.target.drawNew();
+ myself.target.changed();
+ }
+ },
+ this,
+ propertyName
+ );
+};
+
+InspectorMorph.prototype.removeProperty = function () {
+ var prop = this.list.selected;
+ try {
+ delete (this.target[prop]);
+ this.currentProperty = null;
+ this.buildPanes();
+ if (this.target.drawNew) {
+ this.target.changed();
+ this.target.drawNew();
+ this.target.changed();
+ }
+ } catch (err) {
+ this.inform(err);
+ }
+};
+
+// MenuMorph ///////////////////////////////////////////////////////////
+
+// MenuMorph: referenced constructors
+
+var MenuItemMorph;
+
+// MenuMorph inherits from BoxMorph:
+
+MenuMorph.prototype = new BoxMorph();
+MenuMorph.prototype.constructor = MenuMorph;
+MenuMorph.uber = BoxMorph.prototype;
+
+// MenuMorph instance creation:
+
+function MenuMorph(target, title, environment, fontSize) {
+ this.init(target, title, environment, fontSize);
+
+ /*
+ if target is a function, use it as callback:
+ execute target as callback function with the action property
+ of the triggered MenuItem as argument.
+ Use the environment, if it is 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
+ */
+}
+
+MenuMorph.prototype.init = function (target, title, environment, fontSize) {
+ // additional properties:
+ this.target = target;
+ this.title = title || null;
+ this.environment = environment || null;
+ this.fontSize = fontSize || null;
+ this.items = [];
+ this.label = null;
+ this.world = null;
+ this.isListContents = false;
+
+ // initialize inherited properties:
+ MenuMorph.uber.init.call(this);
+
+ // override inherited properties:
+ this.isDraggable = false;
+
+ // immutable properties:
+ this.border = null;
+ this.edge = null;
+};
+
+MenuMorph.prototype.addItem = function (labelString, action, hint, color) {
+ /*
+ labelString is normally a single-line string. But it can also be one
+ of the following:
+
+ * a multi-line string (containing line breaks)
+ * an icon (either a Morph or a Canvas)
+ * a tuple of format: [icon, string]
+ */
+ this.items.push([
+ localize(labelString || 'close'),
+ action || nop,
+ hint,
+ color]);
+};
+
+MenuMorph.prototype.addLine = function (width) {
+ this.items.push([0, width || 1]);
+};
+
+MenuMorph.prototype.createLabel = function () {
+ var text;
+ if (this.label !== null) {
+ this.label.destroy();
+ }
+ text = new TextMorph(
+ localize(this.title),
+ this.fontSize || MorphicPreferences.menuFontSize,
+ MorphicPreferences.menuFontName,
+ true,
+ false,
+ 'center'
+ );
+ text.alignment = 'center';
+ text.color = new Color(255, 255, 255);
+ text.backgroundColor = this.borderColor;
+ text.drawNew();
+ this.label = new BoxMorph(3, 0);
+ this.label.color = this.borderColor;
+ this.label.borderColor = this.borderColor;
+ this.label.setExtent(text.extent().add(4));
+ this.label.drawNew();
+ this.label.add(text);
+ this.label.text = text;
+};
+
+MenuMorph.prototype.drawNew = function () {
+ var myself = this,
+ item,
+ fb,
+ x,
+ y,
+ isLine = false;
+
+ this.children.forEach(function (m) {
+ m.destroy();
+ });
+ this.children = [];
+ if (!this.isListContents) {
+ this.edge = 5;
+ this.border = 2;
+ }
+ this.color = new Color(255, 255, 255);
+ this.borderColor = new Color(60, 60, 60);
+ this.silentSetExtent(new Point(0, 0));
+
+ y = 2;
+ x = this.left() + 4;
+ if (!this.isListContents) {
+ if (this.title) {
+ this.createLabel();
+ this.label.setPosition(this.bounds.origin.add(4));
+ this.add(this.label);
+ y = this.label.bottom();
+ } else {
+ y = this.top() + 4;
+ }
+ }
+ y += 1;
+ this.items.forEach(function (tuple) {
+ isLine = false;
+ if (tuple instanceof StringFieldMorph ||
+ tuple instanceof ColorPickerMorph ||
+ tuple instanceof SliderMorph) {
+ item = tuple;
+ } else if (tuple[0] === 0) {
+ isLine = true;
+ item = new Morph();
+ item.color = myself.borderColor;
+ item.setHeight(tuple[1]);
+ } else {
+ item = new MenuItemMorph(
+ myself.target,
+ tuple[1],
+ tuple[0],
+ myself.fontSize || MorphicPreferences.menuFontSize,
+ MorphicPreferences.menuFontName,
+ myself.environment,
+ tuple[2], // bubble help hint
+ tuple[3] // color
+ );
+ }
+ if (isLine) {
+ y += 1;
+ }
+ item.setPosition(new Point(x, y));
+ myself.add(item);
+ y = y + item.height();
+ if (isLine) {
+ y += 1;
+ }
+ });
+
+ fb = this.fullBounds();
+ this.silentSetExtent(fb.extent().add(4));
+ this.adjustWidths();
+ MenuMorph.uber.drawNew.call(this);
+};
+
+MenuMorph.prototype.maxWidth = function () {
+ var w = 0;
+
+ if (this.parent instanceof FrameMorph) {
+ if (this.parent.scrollFrame instanceof ScrollFrameMorph) {
+ w = this.parent.scrollFrame.width();
+ }
+ }
+ this.children.forEach(function (item) {
+
+ if (item instanceof MenuItemMorph) {
+ w = Math.max(w, item.children[0].width() + 8);
+ } else if ((item instanceof StringFieldMorph) ||
+ (item instanceof ColorPickerMorph) ||
+ (item instanceof SliderMorph)) {
+ w = Math.max(w, item.width());
+ }
+ });
+ if (this.label) {
+ w = Math.max(w, this.label.width());
+ }
+ return w;
+};
+
+MenuMorph.prototype.adjustWidths = function () {
+ var w = this.maxWidth(),
+ isSelected,
+ myself = this;
+ this.children.forEach(function (item) {
+ item.silentSetWidth(w);
+ if (item instanceof MenuItemMorph) {
+ isSelected = (item.image === item.pressImage);
+ item.createBackgrounds();
+ if (isSelected) {
+ item.image = item.pressImage;
+ }
+ } else {
+ item.drawNew();
+ if (item === myself.label) {
+ item.text.setPosition(
+ item.center().subtract(
+ item.text.extent().floorDivideBy(2)
+ )
+ );
+ }
+ }
+ });
+};
+
+MenuMorph.prototype.unselectAllItems = function () {
+ this.children.forEach(function (item) {
+ if (item instanceof MenuItemMorph) {
+ item.image = item.normalImage;
+ }
+ });
+ this.changed();
+};
+
+MenuMorph.prototype.popup = function (world, pos) {
+ this.drawNew();
+ this.setPosition(pos);
+ this.addShadow(new Point(2, 2), 80);
+ this.keepWithin(world);
+ if (world.activeMenu) {
+ world.activeMenu.destroy();
+ }
+ world.add(this);
+ world.activeMenu = this;
+ this.fullChanged();
+};
+
+MenuMorph.prototype.popUpAtHand = function (world) {
+ var wrrld = world || this.world;
+ this.popup(wrrld, wrrld.hand.position());
+};
+
+MenuMorph.prototype.popUpCenteredAtHand = function (world) {
+ var wrrld = world || this.world;
+ this.drawNew();
+ this.popup(
+ wrrld,
+ wrrld.hand.position().subtract(
+ this.extent().floorDivideBy(2)
+ )
+ );
+};
+
+MenuMorph.prototype.popUpCenteredInWorld = function (world) {
+ var wrrld = world || this.world;
+ this.drawNew();
+ this.popup(
+ wrrld,
+ wrrld.center().subtract(
+ this.extent().floorDivideBy(2)
+ )
+ );
+};
+
+// StringMorph /////////////////////////////////////////////////////////
+
+// I am a single line of text
+
+// StringMorph inherits from Morph:
+
+StringMorph.prototype = new Morph();
+StringMorph.prototype.constructor = StringMorph;
+StringMorph.uber = Morph.prototype;
+
+// StringMorph instance creation:
+
+function StringMorph(
+ text,
+ fontSize,
+ fontStyle,
+ bold,
+ italic,
+ isNumeric,
+ shadowOffset,
+ shadowColor,
+ color,
+ fontName
+) {
+ this.init(
+ text,
+ fontSize,
+ fontStyle,
+ bold,
+ italic,
+ isNumeric,
+ shadowOffset,
+ shadowColor,
+ color,
+ fontName
+ );
+}
+
+StringMorph.prototype.init = function (
+ text,
+ fontSize,
+ fontStyle,
+ bold,
+ italic,
+ isNumeric,
+ shadowOffset,
+ shadowColor,
+ color,
+ fontName
+) {
+ // additional properties:
+ this.text = text || ((text === '') ? '' : 'StringMorph');
+ this.fontSize = fontSize || 12;
+ this.fontName = fontName || MorphicPreferences.globalFontFamily;
+ this.fontStyle = fontStyle || 'sans-serif';
+ this.isBold = bold || false;
+ this.isItalic = italic || false;
+ this.isEditable = false;
+ this.isNumeric = isNumeric || false;
+ this.isPassword = false;
+ this.shadowOffset = shadowOffset || new Point(0, 0);
+ this.shadowColor = shadowColor || null;
+ this.isShowingBlanks = false;
+ this.blanksColor = new Color(180, 140, 140);
+
+ // additional properties for text-editing:
+ this.isScrollable = true; // scrolls into view when edited
+ this.currentlySelecting = false;
+ this.startMark = 0;
+ this.endMark = 0;
+ this.markedTextColor = new Color(255, 255, 255);
+ this.markedBackgoundColor = new Color(60, 60, 120);
+
+ // initialize inherited properties:
+ StringMorph.uber.init.call(this);
+
+ // override inherited properites:
+ this.color = color || new Color(0, 0, 0);
+ this.noticesTransparentClick = true;
+ this.drawNew();
+};
+
+StringMorph.prototype.toString = function () {
+ // e.g. 'a StringMorph("Hello World")'
+ return 'a ' +
+ (this.constructor.name ||
+ this.constructor.toString().split(' ')[1].split('(')[0]) +
+ '("' + this.text.slice(0, 30) + '...")';
+};
+
+StringMorph.prototype.password = function (letter, length) {
+ var ans = '',
+ i;
+ for (i = 0; i < length; i += 1) {
+ ans += letter;
+ }
+ return ans;
+};
+
+StringMorph.prototype.font = function () {
+ // answer a font string, e.g. 'bold italic 12px sans-serif'
+ var font = '';
+ if (this.isBold) {
+ font = font + 'bold ';
+ }
+ if (this.isItalic) {
+ font = font + 'italic ';
+ }
+ return font +
+ this.fontSize + 'px ' +
+ (this.fontName ? this.fontName + ', ' : '') +
+ this.fontStyle;
+};
+
+StringMorph.prototype.drawNew = function () {
+ var context, width, start, stop, i, p, c, x, y,
+ txt = this.isPassword ?
+ this.password('*', this.text.length) : this.text;
+
+ // initialize my surface property
+ this.image = newCanvas();
+ context = this.image.getContext('2d');
+ context.font = this.font();
+
+ // set my extent
+ width = Math.max(
+ context.measureText(txt).width +
+ Math.abs(this.shadowOffset.x),
+ 1
+ );
+ this.bounds.corner = this.bounds.origin.add(
+ new Point(
+ width,
+ fontHeight(this.fontSize) + Math.abs(this.shadowOffset.y)
+ )
+ );
+ this.image.width = width;
+ this.image.height = this.height();
+
+ // prepare context for drawing text
+ context.font = this.font();
+ context.textAlign = 'left';
+ context.textBaseline = 'bottom';
+
+ // first draw the shadow, if any
+ if (this.shadowColor) {
+ x = Math.max(this.shadowOffset.x, 0);
+ y = Math.max(this.shadowOffset.y, 0);
+ context.fillStyle = this.shadowColor.toString();
+ context.fillText(txt, x, fontHeight(this.fontSize) + y);
+ }
+
+ // now draw the actual text
+ x = Math.abs(Math.min(this.shadowOffset.x, 0));
+ y = Math.abs(Math.min(this.shadowOffset.y, 0));
+ context.fillStyle = this.color.toString();
+
+ if (this.isShowingBlanks) {
+ this.renderWithBlanks(context, x, fontHeight(this.fontSize) + y);
+ } else {
+ context.fillText(txt, x, fontHeight(this.fontSize) + y);
+ }
+
+ // draw the selection
+ start = Math.min(this.startMark, this.endMark);
+ stop = Math.max(this.startMark, this.endMark);
+ for (i = start; i < stop; i += 1) {
+ p = this.slotPosition(i).subtract(this.position());
+ c = txt.charAt(i);
+ context.fillStyle = this.markedBackgoundColor.toString();
+ context.fillRect(p.x, p.y, context.measureText(c).width + 1 + x,
+ fontHeight(this.fontSize) + y);
+ context.fillStyle = this.markedTextColor.toString();
+ context.fillText(c, p.x + x, fontHeight(this.fontSize) + y);
+ }
+
+ // notify my parent of layout change
+ if (this.parent) {
+ if (this.parent.fixLayout) {
+ this.parent.fixLayout();
+ }
+ }
+};
+
+StringMorph.prototype.renderWithBlanks = function (context, startX, y) {
+ var space = context.measureText(' ').width,
+ blank = newCanvas(new Point(space, this.height())),
+ ctx = blank.getContext('2d'),
+ words = this.text.split(' '),
+ x = startX || 0,
+ isFirst = true;
+
+ // create the blank form
+ ctx.fillStyle = this.blanksColor.toString();
+ ctx.arc(
+ space / 2,
+ blank.height / 2,
+ space / 2,
+ radians(0),
+ radians(360)
+ );
+ ctx.fill();
+
+ function drawBlank() {
+ context.drawImage(blank, x, 0);
+ x += space;
+ }
+
+ // render my text inserting blanks
+ words.forEach(function (word) {
+ if (!isFirst) {
+ drawBlank();
+ }
+ isFirst = false;
+ if (word !== '') {
+ context.fillText(word, x, y);
+ x += context.measureText(word).width;
+ }
+ });
+};
+
+// StringMorph mesuring:
+
+StringMorph.prototype.slotPosition = function (slot) {
+ // answer the position point of the given index ("slot")
+ // where the cursor should be placed
+ var txt = this.isPassword ?
+ this.password('*', this.text.length) : this.text,
+ dest = Math.min(Math.max(slot, 0), txt.length),
+ context = this.image.getContext('2d'),
+ xOffset,
+ x,
+ y,
+ idx;
+
+ xOffset = 0;
+ for (idx = 0; idx < dest; idx += 1) {
+ xOffset += context.measureText(txt[idx]).width;
+ }
+ this.pos = dest;
+ x = this.left() + xOffset;
+ y = this.top();
+ return new Point(x, y);
+};
+
+StringMorph.prototype.slotAt = function (aPoint) {
+ // answer the slot (index) closest to the given point
+ // so the cursor can be moved accordingly
+ var txt = this.isPassword ?
+ this.password('*', this.text.length) : this.text,
+ idx = 0,
+ charX = 0,
+ context = this.image.getContext('2d');
+
+ while (aPoint.x - this.left() > charX) {
+ charX += context.measureText(txt[idx]).width;
+ idx += 1;
+ if (idx === txt.length) {
+ if ((context.measureText(txt).width -
+ (context.measureText(txt[idx - 1]).width / 2)) <
+ (aPoint.x - this.left())) {
+ return idx;
+ }
+ }
+ }
+ return idx - 1;
+};
+
+StringMorph.prototype.upFrom = function (slot) {
+ // answer the slot above the given one
+ return slot;
+};
+
+StringMorph.prototype.downFrom = function (slot) {
+ // answer the slot below the given one
+ return slot;
+};
+
+StringMorph.prototype.startOfLine = function () {
+ // answer the first slot (index) of the line for the given slot
+ return 0;
+};
+
+StringMorph.prototype.endOfLine = function () {
+ // answer the slot (index) indicating the EOL for the given slot
+ return this.text.length;
+};
+
+StringMorph.prototype.rawHeight = function () {
+ // answer my corrected fontSize
+ return this.height() / 1.2;
+};
+
+// StringMorph menus:
+
+StringMorph.prototype.developersMenu = function () {
+ var menu = StringMorph.uber.developersMenu.call(this);
+
+ menu.addLine();
+ menu.addItem("edit", 'edit');
+ menu.addItem(
+ "font size...",
+ function () {
+ this.prompt(
+ menu.title + '\nfont\nsize:',
+ this.setFontSize,
+ this,
+ this.fontSize.toString(),
+ null,
+ 6,
+ 500,
+ true
+ );
+ },
+ 'set this String\'s\nfont point size'
+ );
+ if (this.fontStyle !== 'serif') {
+ menu.addItem("serif", 'setSerif');
+ }
+ if (this.fontStyle !== 'sans-serif') {
+ menu.addItem("sans-serif", 'setSansSerif');
+ }
+ if (this.isBold) {
+ menu.addItem("normal weight", 'toggleWeight');
+ } else {
+ menu.addItem("bold", 'toggleWeight');
+ }
+ if (this.isItalic) {
+ menu.addItem("normal style", 'toggleItalic');
+ } else {
+ menu.addItem("italic", 'toggleItalic');
+ }
+ if (this.isShowingBlanks) {
+ menu.addItem("hide blanks", 'toggleShowBlanks');
+ } else {
+ menu.addItem("show blanks", 'toggleShowBlanks');
+ }
+ if (this.isPassword) {
+ menu.addItem("show characters", 'toggleIsPassword');
+ } else {
+ menu.addItem("hide characters", 'toggleIsPassword');
+ }
+ return menu;
+};
+
+StringMorph.prototype.toggleIsDraggable = function () {
+ // for context menu demo purposes
+ this.isDraggable = !this.isDraggable;
+ if (this.isDraggable) {
+ this.disableSelecting();
+ } else {
+ this.enableSelecting();
+ }
+};
+
+StringMorph.prototype.toggleShowBlanks = function () {
+ this.isShowingBlanks = !this.isShowingBlanks;
+ this.changed();
+ this.drawNew();
+ this.changed();
+};
+
+StringMorph.prototype.toggleWeight = function () {
+ this.isBold = !this.isBold;
+ this.changed();
+ this.drawNew();
+ this.changed();
+};
+
+StringMorph.prototype.toggleItalic = function () {
+ this.isItalic = !this.isItalic;
+ this.changed();
+ this.drawNew();
+ this.changed();
+};
+
+StringMorph.prototype.toggleIsPassword = function () {
+ this.isPassword = !this.isPassword;
+ this.changed();
+ this.drawNew();
+ this.changed();
+};
+
+StringMorph.prototype.setSerif = function () {
+ this.fontStyle = 'serif';
+ this.changed();
+ this.drawNew();
+ this.changed();
+};
+
+StringMorph.prototype.setSansSerif = function () {
+ this.fontStyle = 'sans-serif';
+ this.changed();
+ this.drawNew();
+ this.changed();
+};
+
+StringMorph.prototype.setFontSize = function (size) {
+ // for context menu demo purposes
+ var newSize;
+ if (typeof size === 'number') {
+ this.fontSize = Math.round(Math.min(Math.max(size, 4), 500));
+ } else {
+ newSize = parseFloat(size);
+ if (!isNaN(newSize)) {
+ this.fontSize = Math.round(
+ Math.min(Math.max(newSize, 4), 500)
+ );
+ }
+ }
+ this.changed();
+ this.drawNew();
+ this.changed();
+};
+
+StringMorph.prototype.setText = function (size) {
+ // for context menu demo purposes
+ this.text = Math.round(size).toString();
+ this.changed();
+ this.drawNew();
+ this.changed();
+};
+
+StringMorph.prototype.numericalSetters = function () {
+ // for context menu demo purposes
+ return [
+ 'setLeft',
+ 'setTop',
+ 'setAlphaScaled',
+ 'setFontSize',
+ 'setText'
+ ];
+};
+
+// StringMorph editing:
+
+StringMorph.prototype.edit = function () {
+ this.root().edit(this);
+};
+
+StringMorph.prototype.selection = function () {
+ var start, stop;
+ start = Math.min(this.startMark, this.endMark);
+ stop = Math.max(this.startMark, this.endMark);
+ return this.text.slice(start, stop);
+};
+
+StringMorph.prototype.selectionStartSlot = function () {
+ return Math.min(this.startMark, this.endMark);
+};
+
+StringMorph.prototype.clearSelection = function () {
+ this.currentlySelecting = false;
+ this.startMark = 0;
+ this.endMark = 0;
+ this.drawNew();
+ this.changed();
+};
+
+StringMorph.prototype.deleteSelection = function () {
+ var start, stop, text;
+ text = this.text;
+ start = Math.min(this.startMark, this.endMark);
+ stop = Math.max(this.startMark, this.endMark);
+ this.text = text.slice(0, start) + text.slice(stop);
+ this.changed();
+ this.clearSelection();
+};
+
+StringMorph.prototype.selectAll = function () {
+ if (this.isEditable) {
+ this.startMark = 0;
+ this.endMark = this.text.length;
+ this.drawNew();
+ this.changed();
+ }
+};
+
+StringMorph.prototype.mouseDownLeft = function (pos) {
+ if (this.isEditable) {
+ this.clearSelection();
+ } else {
+ this.escalateEvent('mouseDownLeft', pos);
+ }
+};
+
+StringMorph.prototype.mouseClickLeft = function (pos) {
+ var cursor;
+ if (this.isEditable) {
+ if (!this.currentlySelecting) {
+ this.edit(); // creates a new cursor
+ }
+ cursor = this.root().cursor;
+ if (cursor) {
+ cursor.gotoPos(pos);
+ }
+ this.currentlySelecting = true;
+ } else {
+ this.escalateEvent('mouseClickLeft', pos);
+ }
+};
+
+StringMorph.prototype.enableSelecting = function () {
+ this.mouseDownLeft = function (pos) {
+ this.clearSelection();
+ if (this.isEditable && (!this.isDraggable)) {
+ this.edit();
+ this.root().cursor.gotoPos(pos);
+ this.startMark = this.slotAt(pos);
+ this.endMark = this.startMark;
+ this.currentlySelecting = true;
+ }
+ };
+ this.mouseMove = function (pos) {
+ if (this.isEditable &&
+ this.currentlySelecting &&
+ (!this.isDraggable)) {
+ var newMark = this.slotAt(pos);
+ if (newMark !== this.endMark) {
+ this.endMark = newMark;
+ this.drawNew();
+ this.changed();
+ }
+ }
+ };
+};
+
+StringMorph.prototype.disableSelecting = function () {
+ this.mouseDownLeft = StringMorph.prototype.mouseDownLeft;
+ delete this.mouseMove;
+};
+
+// TextMorph ////////////////////////////////////////////////////////////////
+
+// I am a multi-line, word-wrapping String, quasi-inheriting from StringMorph
+
+// TextMorph inherits from Morph:
+
+TextMorph.prototype = new Morph();
+TextMorph.prototype.constructor = TextMorph;
+TextMorph.uber = Morph.prototype;
+
+// TextMorph instance creation:
+
+function TextMorph(
+ text,
+ fontSize,
+ fontStyle,
+ bold,
+ italic,
+ alignment,
+ width,
+ fontName,
+ shadowOffset,
+ shadowColor
+) {
+ this.init(text,
+ fontSize,
+ fontStyle,
+ bold,
+ italic,
+ alignment,
+ width,
+ fontName,
+ shadowOffset,
+ shadowColor);
+}
+
+TextMorph.prototype.init = function (
+ text,
+ fontSize,
+ fontStyle,
+ bold,
+ italic,
+ alignment,
+ width,
+ fontName,
+ shadowOffset,
+ shadowColor
+) {
+ // additional properties:
+ this.text = text || (text === '' ? text : 'TextMorph');
+ this.words = [];
+ this.lines = [];
+ this.lineSlots = [];
+ this.fontSize = fontSize || 12;
+ this.fontName = fontName || MorphicPreferences.globalFontFamily;
+ this.fontStyle = fontStyle || 'sans-serif';
+ this.isBold = bold || false;
+ this.isItalic = italic || false;
+ this.alignment = alignment || 'left';
+ this.shadowOffset = shadowOffset || new Point(0, 0);
+ this.shadowColor = shadowColor || null;
+ this.maxWidth = width || 0;
+ this.maxLineWidth = 0;
+ this.backgroundColor = null;
+ this.isEditable = false;
+
+ //additional properties for ad-hoc evaluation:
+ this.receiver = null;
+
+ // additional properties for text-editing:
+ this.isScrollable = true; // scrolls into view when edited
+ this.currentlySelecting = false;
+ this.startMark = 0;
+ this.endMark = 0;
+ this.markedTextColor = new Color(255, 255, 255);
+ this.markedBackgoundColor = new Color(60, 60, 120);
+
+ // initialize inherited properties:
+ TextMorph.uber.init.call(this);
+
+ // override inherited properites:
+ this.color = new Color(0, 0, 0);
+ this.noticesTransparentClick = true;
+ this.drawNew();
+};
+
+TextMorph.prototype.toString = function () {
+ // e.g. 'a TextMorph("Hello World")'
+ return 'a TextMorph' + '("' + this.text.slice(0, 30) + '...")';
+};
+
+TextMorph.prototype.font = StringMorph.prototype.font;
+
+TextMorph.prototype.parse = function () {
+ var myself = this,
+ paragraphs = this.text.split('\n'),
+ canvas = newCanvas(),
+ context = canvas.getContext('2d'),
+ oldline = '',
+ newline,
+ w,
+ slot = 0;
+
+ context.font = this.font();
+ this.maxLineWidth = 0;
+ this.lines = [];
+ this.lineSlots = [0];
+ this.words = [];
+
+ paragraphs.forEach(function (p) {
+ myself.words = myself.words.concat(p.split(' '));
+ myself.words.push('\n');
+ });
+
+ this.words.forEach(function (word) {
+ if (word === '\n') {
+ myself.lines.push(oldline);
+ myself.lineSlots.push(slot);
+ myself.maxLineWidth = Math.max(
+ myself.maxLineWidth,
+ context.measureText(oldline).width
+ );
+ oldline = '';
+ } else {
+ if (myself.maxWidth > 0) {
+ newline = oldline + word + ' ';
+ w = context.measureText(newline).width;
+ if (w > myself.maxWidth) {
+ myself.lines.push(oldline);
+ myself.lineSlots.push(slot);
+ myself.maxLineWidth = Math.max(
+ myself.maxLineWidth,
+ context.measureText(oldline).width
+ );
+ oldline = word + ' ';
+ } else {
+ oldline = newline;
+ }
+ } else {
+ oldline = oldline + word + ' ';
+ }
+ slot += word.length + 1;
+ }
+ });
+};
+
+TextMorph.prototype.drawNew = function () {
+ var context, height, i, line, width, shadowHeight, shadowWidth,
+ offx, offy, x, y, start, stop, p, c;
+
+ this.image = newCanvas();
+ context = this.image.getContext('2d');
+ context.font = this.font();
+ this.parse();
+
+ // set my extent
+ shadowWidth = Math.abs(this.shadowOffset.x);
+ shadowHeight = Math.abs(this.shadowOffset.y);
+ height = this.lines.length * (fontHeight(this.fontSize) + shadowHeight);
+ if (this.maxWidth === 0) {
+ this.bounds = this.bounds.origin.extent(
+ new Point(this.maxLineWidth + shadowWidth, height)
+ );
+ } else {
+ this.bounds = this.bounds.origin.extent(
+ new Point(this.maxWidth + shadowWidth, height)
+ );
+ }
+ this.image.width = this.width();
+ this.image.height = this.height();
+
+ // prepare context for drawing text
+ context = this.image.getContext('2d');
+ context.font = this.font();
+ context.textAlign = 'left';
+ context.textBaseline = 'bottom';
+
+ // fill the background, if desired
+ if (this.backgroundColor) {
+ context.fillStyle = this.backgroundColor.toString();
+ context.fillRect(0, 0, this.width(), this.height());
+ }
+
+ // draw the shadow, if any
+ if (this.shadowColor) {
+ offx = Math.max(this.shadowOffset.x, 0);
+ offy = Math.max(this.shadowOffset.y, 0);
+ context.fillStyle = this.shadowColor.toString();
+
+ for (i = 0; i < this.lines.length; i = i + 1) {
+ line = this.lines[i];
+ width = context.measureText(line).width + shadowWidth;
+ if (this.alignment === 'right') {
+ x = this.width() - width;
+ } else if (this.alignment === 'center') {
+ x = (this.width() - width) / 2;
+ } else { // 'left'
+ x = 0;
+ }
+ y = (i + 1) * (fontHeight(this.fontSize) + shadowHeight)
+ - shadowHeight;
+ context.fillText(line, x + offx, y + offy);
+ }
+ }
+
+ // now draw the actual text
+ offx = Math.abs(Math.min(this.shadowOffset.x, 0));
+ offy = Math.abs(Math.min(this.shadowOffset.y, 0));
+ context.fillStyle = this.color.toString();
+
+ for (i = 0; i < this.lines.length; i = i + 1) {
+ line = this.lines[i];
+ width = context.measureText(line).width + shadowWidth;
+ if (this.alignment === 'right') {
+ x = this.width() - width;
+ } else if (this.alignment === 'center') {
+ x = (this.width() - width) / 2;
+ } else { // 'left'
+ x = 0;
+ }
+ y = (i + 1) * (fontHeight(this.fontSize) + shadowHeight)
+ - shadowHeight;
+ context.fillText(line, x + offx, y + offy);
+ }
+
+ // draw the selection
+ start = Math.min(this.startMark, this.endMark);
+ stop = Math.max(this.startMark, this.endMark);
+ for (i = start; i < stop; i += 1) {
+ p = this.slotPosition(i).subtract(this.position());
+ c = this.text.charAt(i);
+ context.fillStyle = this.markedBackgoundColor.toString();
+ context.fillRect(p.x, p.y, context.measureText(c).width + 1,
+ fontHeight(this.fontSize));
+ context.fillStyle = this.markedTextColor.toString();
+ context.fillText(c, p.x, p.y + fontHeight(this.fontSize));
+ }
+
+ // notify my parent of layout change
+ if (this.parent) {
+ if (this.parent.layoutChanged) {
+ this.parent.layoutChanged();
+ }
+ }
+};
+
+TextMorph.prototype.setExtent = function (aPoint) {
+ this.maxWidth = Math.max(aPoint.x, 0);
+ this.changed();
+ this.drawNew();
+};
+
+// TextMorph mesuring:
+
+TextMorph.prototype.columnRow = function (slot) {
+ // answer the logical position point of the given index ("slot")
+ var row,
+ col,
+ idx = 0;
+
+ for (row = 0; row < this.lines.length; row += 1) {
+ idx = this.lineSlots[row];
+ for (col = 0; col < this.lines[row].length; col += 1) {
+ if (idx === slot) {
+ return new Point(col, row);
+ }
+ idx += 1;
+ }
+ }
+ // return new Point(0, 0);
+ return new Point(
+ this.lines[this.lines.length - 1].length - 1,
+ this.lines.length - 1
+ );
+};
+
+TextMorph.prototype.slotPosition = function (slot) {
+ // answer the physical position point of the given index ("slot")
+ // where the cursor should be placed
+ var colRow = this.columnRow(slot),
+ context = this.image.getContext('2d'),
+ shadowHeight = Math.abs(this.shadowOffset.y),
+ xOffset = 0,
+ yOffset,
+ x,
+ y,
+ idx;
+
+ yOffset = colRow.y * (fontHeight(this.fontSize) + shadowHeight);
+ for (idx = 0; idx < colRow.x; idx += 1) {
+ xOffset += context.measureText(this.lines[colRow.y][idx]).width;
+ }
+ x = this.left() + xOffset;
+ y = this.top() + yOffset;
+ return new Point(x, y);
+};
+
+TextMorph.prototype.slotAt = function (aPoint) {
+ // answer the slot (index) closest to the given point
+ // so the cursor can be moved accordingly
+ var charX = 0,
+ row = 0,
+ col = 0,
+ shadowHeight = Math.abs(this.shadowOffset.y),
+ context = this.image.getContext('2d');
+
+ while (aPoint.y - this.top() >
+ ((fontHeight(this.fontSize) + shadowHeight) * row)) {
+ row += 1;
+ }
+ row = Math.max(row, 1);
+ while (aPoint.x - this.left() > charX) {
+ charX += context.measureText(this.lines[row - 1][col]).width;
+ col += 1;
+ }
+ return this.lineSlots[Math.max(row - 1, 0)] + col - 1;
+};
+
+TextMorph.prototype.upFrom = function (slot) {
+ // answer the slot above the given one
+ var above,
+ colRow = this.columnRow(slot);
+ if (colRow.y < 1) {
+ return slot;
+ }
+ above = this.lines[colRow.y - 1];
+ if (above.length < colRow.x - 1) {
+ return this.lineSlots[colRow.y - 1] + above.length;
+ }
+ return this.lineSlots[colRow.y - 1] + colRow.x;
+};
+
+TextMorph.prototype.downFrom = function (slot) {
+ // answer the slot below the given one
+ var below,
+ colRow = this.columnRow(slot);
+ if (colRow.y > this.lines.length - 2) {
+ return slot;
+ }
+ below = this.lines[colRow.y + 1];
+ if (below.length < colRow.x - 1) {
+ return this.lineSlots[colRow.y + 1] + below.length;
+ }
+ return this.lineSlots[colRow.y + 1] + colRow.x;
+};
+
+TextMorph.prototype.startOfLine = function (slot) {
+ // answer the first slot (index) of the line for the given slot
+ return this.lineSlots[this.columnRow(slot).y];
+};
+
+TextMorph.prototype.endOfLine = function (slot) {
+ // answer the slot (index) indicating the EOL for the given slot
+ return this.startOfLine(slot) +
+ this.lines[this.columnRow(slot).y].length - 1;
+};
+
+// TextMorph editing:
+
+TextMorph.prototype.edit = StringMorph.prototype.edit;
+
+TextMorph.prototype.selection = StringMorph.prototype.selection;
+
+TextMorph.prototype.selectionStartSlot
+ = StringMorph.prototype.selectionStartSlot;
+
+TextMorph.prototype.clearSelection = StringMorph.prototype.clearSelection;
+
+TextMorph.prototype.deleteSelection = StringMorph.prototype.deleteSelection;
+
+TextMorph.prototype.selectAll = StringMorph.prototype.selectAll;
+
+TextMorph.prototype.mouseDownLeft = StringMorph.prototype.mouseDownLeft;
+
+TextMorph.prototype.mouseClickLeft = StringMorph.prototype.mouseClickLeft;
+
+TextMorph.prototype.enableSelecting = StringMorph.prototype.enableSelecting;
+
+TextMorph.prototype.disableSelecting = StringMorph.prototype.disableSelecting;
+
+TextMorph.prototype.selectAllAndEdit = function () {
+ this.edit();
+ this.selectAll();
+};
+
+// TextMorph menus:
+
+TextMorph.prototype.developersMenu = function () {
+ var menu = TextMorph.uber.developersMenu.call(this);
+ menu.addLine();
+ menu.addItem("edit", 'edit');
+ menu.addItem(
+ "font size...",
+ function () {
+ this.prompt(
+ menu.title + '\nfont\nsize:',
+ this.setFontSize,
+ this,
+ this.fontSize.toString(),
+ null,
+ 6,
+ 100,
+ true
+ );
+ },
+ 'set this Text\'s\nfont point size'
+ );
+ if (this.alignment !== 'left') {
+ menu.addItem("align left", 'setAlignmentToLeft');
+ }
+ if (this.alignment !== 'right') {
+ menu.addItem("align right", 'setAlignmentToRight');
+ }
+ if (this.alignment !== 'center') {
+ menu.addItem("align center", 'setAlignmentToCenter');
+ }
+ menu.addLine();
+ if (this.fontStyle !== 'serif') {
+ menu.addItem("serif", 'setSerif');
+ }
+ if (this.fontStyle !== 'sans-serif') {
+ menu.addItem("sans-serif", 'setSansSerif');
+ }
+ if (this.isBold) {
+ menu.addItem("normal weight", 'toggleWeight');
+ } else {
+ menu.addItem("bold", 'toggleWeight');
+ }
+ if (this.isItalic) {
+ menu.addItem("normal style", 'toggleItalic');
+ } else {
+ menu.addItem("italic", 'toggleItalic');
+ }
+ return menu;
+};
+
+TextMorph.prototype.setAlignmentToLeft = function () {
+ this.alignment = 'left';
+ this.drawNew();
+ this.changed();
+};
+
+TextMorph.prototype.setAlignmentToRight = function () {
+ this.alignment = 'right';
+ this.drawNew();
+ this.changed();
+};
+
+TextMorph.prototype.setAlignmentToCenter = function () {
+ this.alignment = 'center';
+ this.drawNew();
+ this.changed();
+};
+
+TextMorph.prototype.toggleIsDraggable
+ = StringMorph.prototype.toggleIsDraggable;
+
+TextMorph.prototype.toggleWeight = StringMorph.prototype.toggleWeight;
+
+TextMorph.prototype.toggleItalic = StringMorph.prototype.toggleItalic;
+
+TextMorph.prototype.setSerif = StringMorph.prototype.setSerif;
+
+TextMorph.prototype.setSansSerif = StringMorph.prototype.setSansSerif;
+
+TextMorph.prototype.setText = StringMorph.prototype.setText;
+
+TextMorph.prototype.setFontSize = StringMorph.prototype.setFontSize;
+
+TextMorph.prototype.numericalSetters = StringMorph.prototype.numericalSetters;
+
+// TextMorph evaluation:
+
+TextMorph.prototype.evaluationMenu = function () {
+ var menu = new MenuMorph(this, null);
+ menu.addItem(
+ "do it",
+ 'doIt',
+ 'evaluate the\nselected expression'
+ );
+ menu.addItem(
+ "show it",
+ 'showIt',
+ 'evaluate the\nselected expression\nand show the result'
+ );
+ menu.addItem(
+ "inspect it",
+ 'inspectIt',
+ 'evaluate the\nselected expression\nand inspect the result'
+ );
+ menu.addLine();
+ menu.addItem("select all", 'selectAllAndEdit');
+ return menu;
+};
+
+TextMorph.prototype.setReceiver = function (obj) {
+ this.receiver = obj;
+ this.customContextMenu = this.evaluationMenu();
+};
+
+TextMorph.prototype.doIt = function () {
+ this.receiver.evaluateString(this.selection());
+ this.edit();
+};
+
+TextMorph.prototype.showIt = function () {
+ var result = this.receiver.evaluateString(this.selection());
+ if (result !== null) {
+ this.inform(result);
+ }
+};
+
+TextMorph.prototype.inspectIt = function () {
+ var result = this.receiver.evaluateString(this.selection()),
+ world = this.world(),
+ inspector;
+ if (result !== null) {
+ inspector = new InspectorMorph(result);
+ inspector.setPosition(world.hand.position());
+ inspector.keepWithin(world);
+ world.add(inspector);
+ inspector.changed();
+ }
+};
+
+// TriggerMorph ////////////////////////////////////////////////////////
+
+// I provide basic button functionality
+
+// TriggerMorph inherits from Morph:
+
+TriggerMorph.prototype = new Morph();
+TriggerMorph.prototype.constructor = TriggerMorph;
+TriggerMorph.uber = Morph.prototype;
+
+// TriggerMorph instance creation:
+
+function TriggerMorph(
+ target,
+ action,
+ labelString,
+ fontSize,
+ fontStyle,
+ environment,
+ hint,
+ labelColor
+) {
+ this.init(
+ target,
+ action,
+ labelString,
+ fontSize,
+ fontStyle,
+ environment,
+ hint,
+ labelColor
+ );
+}
+
+TriggerMorph.prototype.init = function (
+ target,
+ action,
+ labelString,
+ fontSize,
+ fontStyle,
+ environment,
+ hint,
+ labelColor
+) {
+ // additional properties:
+ this.target = target || null;
+ this.action = action || null;
+ this.environment = environment || null;
+ this.labelString = labelString || null;
+ this.label = null;
+ this.hint = hint || null;
+ this.fontSize = fontSize || MorphicPreferences.menuFontSize;
+ this.fontStyle = fontStyle || 'sans-serif';
+ this.highlightColor = new Color(192, 192, 192);
+ this.pressColor = new Color(128, 128, 128);
+ this.labelColor = labelColor || new Color(0, 0, 0);
+
+ // initialize inherited properties:
+ TriggerMorph.uber.init.call(this);
+
+ // override inherited properites:
+ this.color = new Color(255, 255, 255);
+ this.drawNew();
+};
+
+// TriggerMorph drawing:
+
+TriggerMorph.prototype.drawNew = function () {
+ this.createBackgrounds();
+ if (this.labelString !== null) {
+ this.createLabel();
+ }
+};
+
+TriggerMorph.prototype.createBackgrounds = function () {
+ var context,
+ ext = this.extent();
+
+ this.normalImage = newCanvas(ext);
+ context = this.normalImage.getContext('2d');
+ context.fillStyle = this.color.toString();
+ context.fillRect(0, 0, ext.x, ext.y);
+
+ this.highlightImage = newCanvas(ext);
+ context = this.highlightImage.getContext('2d');
+ context.fillStyle = this.highlightColor.toString();
+ context.fillRect(0, 0, ext.x, ext.y);
+
+ this.pressImage = newCanvas(ext);
+ context = this.pressImage.getContext('2d');
+ context.fillStyle = this.pressColor.toString();
+ context.fillRect(0, 0, ext.x, ext.y);
+
+ this.image = this.normalImage;
+};
+
+TriggerMorph.prototype.createLabel = function () {
+ if (this.label !== null) {
+ this.label.destroy();
+ }
+ this.label = new StringMorph(
+ this.labelString,
+ this.fontSize,
+ this.fontStyle,
+ false, // bold
+ false, // italic
+ false, // numeric
+ null, // shadow offset
+ null, // shadow color
+ this.labelColor
+ );
+ this.label.setPosition(
+ this.center().subtract(
+ this.label.extent().floorDivideBy(2)
+ )
+ );
+ this.add(this.label);
+};
+
+// TriggerMorph duplicating:
+
+TriggerMorph.prototype.copyRecordingReferences = function (dict) {
+ // inherited, see comment in Morph
+ var c = TriggerMorph.uber.copyRecordingReferences.call(
+ this,
+ dict
+ );
+ if (c.label && dict[this.label]) {
+ c.label = (dict[this.label]);
+ }
+ return c;
+};
+
+// TriggerMorph action:
+
+TriggerMorph.prototype.trigger = 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 (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);
+ } else { // assume it's a String
+ this.target[this.action]();
+ }
+ }
+};
+
+// TriggerMorph events:
+
+TriggerMorph.prototype.mouseEnter = function () {
+ this.image = this.highlightImage;
+ this.changed();
+ if (this.hint) {
+ this.bubbleHelp(this.hint);
+ }
+};
+
+TriggerMorph.prototype.mouseLeave = function () {
+ this.image = this.normalImage;
+ this.changed();
+ if (this.hint) {
+ this.world().hand.destroyTemporaries();
+ }
+};
+
+TriggerMorph.prototype.mouseDownLeft = function () {
+ this.image = this.pressImage;
+ this.changed();
+};
+
+TriggerMorph.prototype.mouseClickLeft = function () {
+ this.image = this.highlightImage;
+ this.changed();
+ this.trigger();
+};
+
+// TriggerMorph bubble help:
+
+TriggerMorph.prototype.bubbleHelp = function (contents) {
+ var myself = this;
+ this.fps = 2;
+ this.step = function () {
+ if (this.bounds.containsPoint(this.world().hand.position())) {
+ myself.popUpbubbleHelp(contents);
+ }
+ myself.fps = 0;
+ delete myself.step;
+ };
+};
+
+TriggerMorph.prototype.popUpbubbleHelp = function (contents) {
+ new SpeechBubbleMorph(
+ localize(contents),
+ null,
+ null,
+ 1
+ ).popUp(this.world(), this.rightCenter().add(new Point(-8, 0)));
+};
+
+// MenuItemMorph ///////////////////////////////////////////////////////
+
+// I automatically determine my bounds
+
+var MenuItemMorph;
+
+// MenuItemMorph inherits from TriggerMorph:
+
+MenuItemMorph.prototype = new TriggerMorph();
+MenuItemMorph.prototype.constructor = MenuItemMorph;
+MenuItemMorph.uber = TriggerMorph.prototype;
+
+// MenuItemMorph instance creation:
+
+function MenuItemMorph(
+ target,
+ action,
+ labelString, // can also be a Morph or a Canvas or a tuple: [icon, string]
+ fontSize,
+ fontStyle,
+ environment,
+ hint,
+ color
+) {
+ this.init(
+ target,
+ action,
+ labelString,
+ fontSize,
+ fontStyle,
+ environment,
+ hint,
+ color
+ );
+}
+
+MenuItemMorph.prototype.createLabel = function () {
+ var icon, lbl, np;
+ if (this.label !== null) {
+ this.label.destroy();
+ }
+ if (isString(this.labelString)) {
+ this.label = this.createLabelString(this.labelString);
+ } else if (this.labelString instanceof Array) {
+ // assume its pattern is: [icon, string]
+ this.label = new Morph();
+ this.label.alpha = 0; // transparent
+ this.label.add(icon = this.createIcon(this.labelString[0]));
+ this.label.add(lbl = this.createLabelString(this.labelString[1]));
+ lbl.setCenter(icon.center());
+ lbl.setLeft(icon.right() + 4);
+ this.label.bounds = (icon.bounds.merge(lbl.bounds));
+ this.label.drawNew();
+ } else { // assume it's either a Morph or a Canvas
+ this.label = this.createIcon(this.labelString);
+ }
+ this.silentSetExtent(this.label.extent().add(new Point(8, 0)));
+ np = this.position().add(new Point(4, 0));
+ this.label.bounds = np.extent(this.label.extent());
+ this.add(this.label);
+};
+
+MenuItemMorph.prototype.createIcon = function (source) {
+ // source can be either a Morph or an HTMLCanvasElement
+ var icon = new Morph(),
+ src;
+ icon.image = source instanceof Morph ? source.fullImage() : source;
+ // adjust shadow dimensions
+ if (source instanceof Morph && source.getShadow()) {
+ src = icon.image;
+ icon.image = newCanvas(
+ source.fullBounds().extent().subtract(
+ this.shadowBlur * (useBlurredShadows ? 1 : 2)
+ )
+ );
+ icon.image.getContext('2d').drawImage(src, 0, 0);
+ }
+ icon.silentSetWidth(icon.image.width);
+ icon.silentSetHeight(icon.image.height);
+ return icon;
+};
+
+MenuItemMorph.prototype.createLabelString = function (string) {
+ var lbl = new TextMorph(
+ string,
+ this.fontSize,
+ this.fontStyle
+ );
+ lbl.setColor(this.labelColor);
+ return lbl;
+};
+
+// MenuItemMorph events:
+
+MenuItemMorph.prototype.mouseEnter = function () {
+ if (!this.isListItem()) {
+ this.image = this.highlightImage;
+ this.changed();
+ }
+ if (this.hint) {
+ this.bubbleHelp(this.hint);
+ }
+};
+
+MenuItemMorph.prototype.mouseLeave = function () {
+ if (!this.isListItem()) {
+ this.image = this.normalImage;
+ this.changed();
+ }
+ if (this.hint) {
+ this.world().hand.destroyTemporaries();
+ }
+};
+
+MenuItemMorph.prototype.mouseDownLeft = function (pos) {
+ if (this.isListItem()) {
+ this.parent.unselectAllItems();
+ this.escalateEvent('mouseDownLeft', pos);
+ }
+ this.image = this.pressImage;
+ this.changed();
+};
+
+MenuItemMorph.prototype.mouseMove = function () {
+ if (this.isListItem()) {
+ this.escalateEvent('mouseMove');
+ }
+};
+
+MenuItemMorph.prototype.mouseClickLeft = function () {
+ if (!this.isListItem()) {
+ this.parent.destroy();
+ this.root().activeMenu = null;
+ }
+ this.trigger();
+};
+
+MenuItemMorph.prototype.isListItem = function () {
+ if (this.parent) {
+ return this.parent.isListContents;
+ }
+ return false;
+};
+
+MenuItemMorph.prototype.isSelectedListItem = function () {
+ if (this.isListItem()) {
+ return this.image === this.pressImage;
+ }
+ return false;
+};
+
+// FrameMorph //////////////////////////////////////////////////////////
+
+// I clip my submorphs at my bounds
+
+// Frames inherit from Morph:
+
+FrameMorph.prototype = new Morph();
+FrameMorph.prototype.constructor = FrameMorph;
+FrameMorph.uber = Morph.prototype;
+
+function FrameMorph(aScrollFrame) {
+ this.init(aScrollFrame);
+}
+
+FrameMorph.prototype.init = function (aScrollFrame) {
+ this.scrollFrame = aScrollFrame || null;
+
+ FrameMorph.uber.init.call(this);
+ this.color = new Color(255, 250, 245);
+ this.drawNew();
+ this.acceptsDrops = true;
+
+ if (this.scrollFrame) {
+ this.isDraggable = false;
+ this.noticesTransparentClick = false;
+ this.alpha = 0;
+ }
+};
+
+FrameMorph.prototype.fullBounds = function () {
+ var shadow = this.getShadow();
+ if (shadow !== null) {
+ return this.bounds.merge(shadow.bounds);
+ }
+ return this.bounds;
+};
+
+FrameMorph.prototype.fullImage = function () {
+ // use only for shadows
+ return this.image;
+};
+
+FrameMorph.prototype.fullDrawOn = function (aCanvas, aRect) {
+ var rectangle, dirty;
+ if (!this.isVisible) {
+ return null;
+ }
+ rectangle = aRect || this.fullBounds();
+ dirty = this.bounds.intersect(rectangle);
+ if (!dirty.extent().gt(new Point(0, 0))) {
+ return null;
+ }
+ this.drawOn(aCanvas, dirty);
+ this.children.forEach(function (child) {
+ if (child instanceof ShadowMorph) {
+ child.fullDrawOn(aCanvas, rectangle);
+ } else {
+ child.fullDrawOn(aCanvas, dirty);
+ }
+ });
+};
+
+// FrameMorph scrolling optimization:
+
+FrameMorph.prototype.moveBy = function (delta) {
+ this.changed();
+ this.bounds = this.bounds.translateBy(delta);
+ this.children.forEach(function (child) {
+ child.silentMoveBy(delta);
+ });
+ this.changed();
+};
+
+// FrameMorph scrolling support:
+
+FrameMorph.prototype.submorphBounds = function () {
+ var result = null;
+
+ if (this.children.length > 0) {
+ result = this.children[0].bounds;
+ this.children.forEach(function (child) {
+ result = result.merge(child.fullBounds());
+ });
+ }
+ return result;
+};
+
+FrameMorph.prototype.keepInScrollFrame = function () {
+ if (this.scrollFrame === null) {
+ return null;
+ }
+ if (this.left() > this.scrollFrame.left()) {
+ this.moveBy(
+ new Point(this.scrollFrame.left() - this.left(), 0)
+ );
+ }
+ if (this.right() < this.scrollFrame.right()) {
+ this.moveBy(
+ new Point(this.scrollFrame.right() - this.right(), 0)
+ );
+ }
+ if (this.top() > this.scrollFrame.top()) {
+ this.moveBy(
+ new Point(0, this.scrollFrame.top() - this.top())
+ );
+ }
+ if (this.bottom() < this.scrollFrame.bottom()) {
+ this.moveBy(
+ 0,
+ new Point(this.scrollFrame.bottom() - this.bottom(), 0)
+ );
+ }
+};
+
+FrameMorph.prototype.adjustBounds = function () {
+ var subBounds,
+ newBounds,
+ myself = this;
+
+ if (this.scrollFrame === null) {
+ return null;
+ }
+
+ subBounds = this.submorphBounds();
+ if (subBounds && (!this.scrollFrame.isTextLineWrapping)) {
+ newBounds = subBounds
+ .expandBy(this.scrollFrame.padding)
+ .growBy(this.scrollFrame.growth)
+ .merge(this.scrollFrame.bounds);
+ } else {
+ newBounds = this.scrollFrame.bounds.copy();
+ }
+ if (!this.bounds.eq(newBounds)) {
+ this.bounds = newBounds;
+ this.drawNew();
+ this.keepInScrollFrame();
+ }
+
+ if (this.scrollFrame.isTextLineWrapping) {
+ this.children.forEach(function (morph) {
+ if (morph instanceof TextMorph) {
+ morph.setWidth(myself.width());
+ myself.setHeight(
+ Math.max(morph.height(), myself.scrollFrame.height())
+ );
+ }
+ });
+ }
+
+ this.scrollFrame.adjustScrollBars();
+};
+
+// FrameMorph dragging & dropping of contents:
+
+FrameMorph.prototype.reactToDropOf = function () {
+ this.adjustBounds();
+};
+
+FrameMorph.prototype.reactToGrabOf = function () {
+ this.adjustBounds();
+};
+
+// FrameMorph duplicating:
+
+FrameMorph.prototype.copyRecordingReferences = function (dict) {
+ // inherited, see comment in Morph
+ var c = FrameMorph.uber.copyRecordingReferences.call(
+ this,
+ dict
+ );
+ if (c.frame && dict[this.scrollFrame]) {
+ c.frame = (dict[this.scrollFrame]);
+ }
+ return c;
+};
+
+// FrameMorph menus:
+
+FrameMorph.prototype.developersMenu = function () {
+ var menu = FrameMorph.uber.developersMenu.call(this);
+ if (this.children.length > 0) {
+ menu.addLine();
+ menu.addItem(
+ "move all inside...",
+ 'keepAllSubmorphsWithin',
+ 'keep all submorphs\nwithin and visible'
+ );
+ }
+ return menu;
+};
+
+FrameMorph.prototype.keepAllSubmorphsWithin = function () {
+ var myself = this;
+ this.children.forEach(function (m) {
+ m.keepWithin(myself);
+ });
+};
+
+// ScrollFrameMorph ////////////////////////////////////////////////////
+
+ScrollFrameMorph.prototype = new FrameMorph();
+ScrollFrameMorph.prototype.constructor = ScrollFrameMorph;
+ScrollFrameMorph.uber = FrameMorph.prototype;
+
+function ScrollFrameMorph(scroller, size, sliderColor) {
+ this.init(scroller, size, sliderColor);
+}
+
+ScrollFrameMorph.prototype.init = function (scroller, size, sliderColor) {
+ var myself = this;
+
+ ScrollFrameMorph.uber.init.call(this);
+ this.scrollBarSize = size || MorphicPreferences.scrollBarSize;
+ this.autoScrollTrigger = null;
+ this.isScrollingByDragging = true; // change if desired
+ this.hasVelocity = true; // dto.
+ this.padding = 0; // around the scrollable area
+ this.growth = 0; // pixels or Point to grow right/left when near edge
+ this.isTextLineWrapping = false;
+ this.contents = scroller || new FrameMorph(this);
+ this.add(this.contents);
+ this.hBar = new SliderMorph(
+ null, // start
+ null, // stop
+ null, // value
+ null, // size
+ 'horizontal',
+ sliderColor
+ );
+ this.hBar.setHeight(this.scrollBarSize);
+ this.hBar.action = function (num) {
+ myself.contents.setPosition(
+ new Point(
+ myself.left() - num,
+ myself.contents.position().y
+ )
+ );
+ };
+ this.hBar.isDraggable = false;
+ this.add(this.hBar);
+ this.vBar = new SliderMorph(
+ null, // start
+ null, // stop
+ null, // value
+ null, // size
+ 'vertical',
+ sliderColor
+ );
+ this.vBar.setWidth(this.scrollBarSize);
+ this.vBar.action = function (num) {
+ myself.contents.setPosition(
+ new Point(
+ myself.contents.position().x,
+ myself.top() - num
+ )
+ );
+ };
+ this.vBar.isDraggable = false;
+ this.add(this.vBar);
+};
+
+ScrollFrameMorph.prototype.adjustScrollBars = function () {
+ var hWidth = this.width() - this.scrollBarSize,
+ vHeight = this.height() - this.scrollBarSize;
+
+ this.changed();
+ if (this.contents.width() > this.width() +
+ MorphicPreferences.scrollBarSize) {
+ this.hBar.show();
+ if (this.hBar.width() !== hWidth) {
+ this.hBar.setWidth(hWidth);
+ }
+
+ this.hBar.setPosition(
+ new Point(
+ this.left(),
+ this.bottom() - this.hBar.height()
+ )
+ );
+ this.hBar.start = 0;
+ this.hBar.stop = this.contents.width() - this.width();
+ this.hBar.size =
+ this.width() / this.contents.width() * this.hBar.stop;
+ this.hBar.value = this.left() - this.contents.left();
+ this.hBar.drawNew();
+ } else {
+ this.hBar.hide();
+ }
+
+ if (this.contents.height() > this.height() +
+ this.scrollBarSize) {
+ this.vBar.show();
+ if (this.vBar.height() !== vHeight) {
+ this.vBar.setHeight(vHeight);
+ }
+
+ this.vBar.setPosition(
+ new Point(
+ this.right() - this.vBar.width(),
+ this.top()
+ )
+ );
+ this.vBar.start = 0;
+ this.vBar.stop = this.contents.height() - this.height();
+ this.vBar.size =
+ this.height() / this.contents.height() * this.vBar.stop;
+ this.vBar.value = this.top() - this.contents.top();
+ this.vBar.drawNew();
+ } else {
+ this.vBar.hide();
+ }
+};
+
+ScrollFrameMorph.prototype.addContents = function (aMorph) {
+ this.contents.add(aMorph);
+ this.contents.adjustBounds();
+};
+
+ScrollFrameMorph.prototype.setContents = function (aMorph) {
+ this.contents.children.forEach(function (m) {
+ m.destroy();
+ });
+ this.contents.children = [];
+ aMorph.setPosition(this.position().add(this.padding + 2));
+ this.addContents(aMorph);
+};
+
+ScrollFrameMorph.prototype.setExtent = function (aPoint) {
+ if (this.isTextLineWrapping) {
+ this.contents.setPosition(this.position().copy());
+ }
+ ScrollFrameMorph.uber.setExtent.call(this, aPoint);
+ this.contents.adjustBounds();
+};
+
+// ScrollFrameMorph scrolling by dragging:
+
+ScrollFrameMorph.prototype.scrollX = function (steps) {
+ var cl = this.contents.left(),
+ l = this.left(),
+ cw = this.contents.width(),
+ r = this.right(),
+ newX;
+
+ newX = cl + steps;
+ if (newX > l) {
+ newX = l;
+ }
+ if (newX + cw < r) {
+ newX = r - cw;
+ }
+ if (newX !== cl) {
+ this.contents.setLeft(newX);
+ }
+};
+
+ScrollFrameMorph.prototype.scrollY = function (steps) {
+ var ct = this.contents.top(),
+ t = this.top(),
+ ch = this.contents.height(),
+ b = this.bottom(),
+ newY;
+
+ newY = ct + steps;
+ if (newY > t) {
+ newY = t;
+ }
+ if (newY + ch < b) {
+ newY = b - ch;
+ }
+ if (newY !== ct) {
+ this.contents.setTop(newY);
+ }
+};
+
+ScrollFrameMorph.prototype.step = function () {
+ nop();
+};
+
+ScrollFrameMorph.prototype.mouseDownLeft = function (pos) {
+ if (!this.isScrollingByDragging) {
+ return null;
+ }
+ var world = this.root(),
+ oldPos = pos,
+ myself = this,
+ deltaX = 0,
+ deltaY = 0,
+ friction = 0.8;
+
+ this.step = function () {
+ var newPos;
+ if (world.hand.mouseButton &&
+ (world.hand.children.length === 0) &&
+ (myself.bounds.containsPoint(world.hand.position()))) {
+ newPos = world.hand.bounds.origin;
+ deltaX = newPos.x - oldPos.x;
+ if (deltaX !== 0) {
+ myself.scrollX(deltaX);
+ }
+ deltaY = newPos.y - oldPos.y;
+ if (deltaY !== 0) {
+ myself.scrollY(deltaY);
+ }
+ oldPos = newPos;
+ } else {
+ if (!myself.hasVelocity) {
+ myself.step = function () {
+ nop();
+ };
+ } else {
+ if ((Math.abs(deltaX) < 0.5) &&
+ (Math.abs(deltaY) < 0.5)) {
+ myself.step = function () {
+ nop();
+ };
+ } else {
+ deltaX = deltaX * friction;
+ myself.scrollX(Math.round(deltaX));
+ deltaY = deltaY * friction;
+ myself.scrollY(Math.round(deltaY));
+ }
+ }
+ }
+ this.adjustScrollBars();
+ };
+};
+
+ScrollFrameMorph.prototype.startAutoScrolling = function () {
+ var myself = this,
+ inset = MorphicPreferences.scrollBarSize * 3,
+ world = this.world(),
+ hand,
+ inner,
+ pos;
+
+ if (!world) {
+ return null;
+ }
+ hand = world.hand;
+ if (!this.autoScrollTrigger) {
+ this.autoScrollTrigger = Date.now();
+ }
+ this.step = function () {
+ pos = hand.bounds.origin;
+ inner = myself.bounds.insetBy(inset);
+ if ((myself.bounds.containsPoint(pos)) &&
+ (!(inner.containsPoint(pos))) &&
+ (hand.children.length > 0)) {
+ myself.autoScroll(pos);
+ } else {
+ myself.step = function () {
+ nop();
+ };
+ myself.autoScrollTrigger = null;
+ }
+ };
+};
+
+ScrollFrameMorph.prototype.autoScroll = function (pos) {
+ var inset, area;
+
+ if (Date.now() - this.autoScrollTrigger < 500) {
+ return null;
+ }
+
+ inset = MorphicPreferences.scrollBarSize * 3;
+ area = this.topLeft().extent(new Point(this.width(), inset));
+ if (area.containsPoint(pos)) {
+ this.scrollY(inset - (pos.y - this.top()));
+ }
+ area = this.topLeft().extent(new Point(inset, this.height()));
+ if (area.containsPoint(pos)) {
+ this.scrollX(inset - (pos.x - this.left()));
+ }
+ area = (new Point(this.right() - inset, this.top()))
+ .extent(new Point(inset, this.height()));
+ if (area.containsPoint(pos)) {
+ this.scrollX(-(inset - (this.right() - pos.x)));
+ }
+ area = (new Point(this.left(), this.bottom() - inset))
+ .extent(new Point(this.width(), inset));
+ if (area.containsPoint(pos)) {
+ this.scrollY(-(inset - (this.bottom() - pos.y)));
+ }
+ this.adjustScrollBars();
+};
+
+// ScrollFrameMorph scrolling by editing text:
+
+ScrollFrameMorph.prototype.scrollCursorIntoView = function (morph) {
+ var txt = morph.target,
+ offset = txt.position().subtract(this.contents.position()),
+ ft = this.top() + this.padding,
+ fb = this.bottom() - this.padding;
+ this.contents.setExtent(txt.extent().add(offset).add(this.padding));
+ if (morph.top() < ft) {
+ this.contents.setTop(this.contents.top() + ft - morph.top());
+ morph.setTop(ft);
+ } else if (morph.bottom() > fb) {
+ this.contents.setBottom(this.contents.bottom() + fb - morph.bottom());
+ morph.setBottom(fb);
+ }
+ this.adjustScrollBars();
+};
+
+// ScrollFrameMorph events:
+
+ScrollFrameMorph.prototype.mouseScroll = function (y, x) {
+ if (y) {
+ this.scrollY(y * MorphicPreferences.mouseScrollAmount);
+ }
+ if (x) {
+ this.scrollX(x * MorphicPreferences.mouseScrollAmount);
+ }
+ this.adjustScrollBars();
+};
+
+// ScrollFrameMorph duplicating:
+
+ScrollFrameMorph.prototype.copyRecordingReferences = function (dict) {
+ // inherited, see comment in Morph
+ var c = ScrollFrameMorph.uber.copyRecordingReferences.call(
+ this,
+ dict
+ );
+ if (c.contents && dict[this.contents]) {
+ c.contents = (dict[this.contents]);
+ }
+ if (c.hBar && dict[this.hBar]) {
+ c.hBar = (dict[this.hBar]);
+ c.hBar.action = function (num) {
+ c.contents.setPosition(
+ new Point(c.left() - num, c.contents.position().y)
+ );
+ };
+ }
+ if (c.vBar && dict[this.vBar]) {
+ c.vBar = (dict[this.vBar]);
+ c.vBar.action = function (num) {
+ c.contents.setPosition(
+ new Point(c.contents.position().x, c.top() - num)
+ );
+ };
+ }
+ return c;
+};
+
+// ScrollFrameMorph menu:
+
+ScrollFrameMorph.prototype.developersMenu = function () {
+ var menu = ScrollFrameMorph.uber.developersMenu.call(this);
+ if (this.isTextLineWrapping) {
+ menu.addItem(
+ "auto line wrap off...",
+ 'toggleTextLineWrapping',
+ 'turn automatic\nline wrapping\noff'
+ );
+ } else {
+ menu.addItem(
+ "auto line wrap on...",
+ 'toggleTextLineWrapping',
+ 'enable automatic\nline wrapping'
+ );
+ }
+ return menu;
+};
+
+
+ScrollFrameMorph.prototype.toggleTextLineWrapping = function () {
+ this.isTextLineWrapping = !this.isTextLineWrapping;
+};
+
+// ListMorph ///////////////////////////////////////////////////////////
+
+ListMorph.prototype = new ScrollFrameMorph();
+ListMorph.prototype.constructor = ListMorph;
+ListMorph.uber = ScrollFrameMorph.prototype;
+
+function ListMorph(elements, labelGetter, format) {
+/*
+ passing a format is optional. If the format parameter is specified
+ it has to be of the following pattern:
+
+ [
+ [<color>, <single-argument predicate>],
+ ...
+ ]
+
+ multiple color conditions can be passed in such a format list, the
+ last predicate to evaluate true when given the list element sets
+ the given color. If no condition is met, the default color (black)
+ will be assigned.
+
+ An example of how to use fomats can be found in the InspectorMorph's
+ "markOwnProperties" mechanism.
+*/
+ this.init(
+ elements || [],
+ labelGetter || function (element) {
+ if (isString(element)) {
+ return element;
+ }
+ if (element.toSource) {
+ return element.toSource();
+ }
+ return element.toString();
+ },
+ format || []
+ );
+}
+
+ListMorph.prototype.init = function (elements, labelGetter, format) {
+ ListMorph.uber.init.call(this);
+
+ this.contents.acceptsDrops = false;
+ this.color = new Color(255, 255, 255);
+ this.hBar.alpha = 0.6;
+ this.vBar.alpha = 0.6;
+ this.elements = elements || [];
+ this.labelGetter = labelGetter;
+ this.format = format;
+ this.listContents = null;
+ this.selected = null;
+ this.action = null;
+ this.acceptsDrops = false;
+ this.buildListContents();
+};
+
+ListMorph.prototype.buildListContents = function () {
+ var myself = this;
+ if (this.listContents) {
+ this.listContents.destroy();
+ }
+ this.listContents = new MenuMorph(
+ this.select,
+ null,
+ this
+ );
+ if (this.elements.length === 0) {
+ this.elements = ['(empty)'];
+ }
+ this.elements.forEach(function (element) {
+ var color = null;
+
+ myself.format.forEach(function (pair) {
+ if (pair[1].call(null, element)) {
+ color = pair[0];
+ }
+ });
+ myself.listContents.addItem(
+ myself.labelGetter(element), // label string
+ element, // action
+ null, // hint
+ color
+ );
+ });
+ this.listContents.setPosition(this.contents.position());
+ this.listContents.isListContents = true;
+ this.listContents.drawNew();
+ this.addContents(this.listContents);
+};
+
+ListMorph.prototype.select = function (item) {
+ this.selected = item;
+ if (this.action) {
+ this.action.call(null, item);
+ }
+};
+
+ListMorph.prototype.setExtent = function (aPoint) {
+ var lb = this.listContents.bounds,
+ nb = this.bounds.origin.copy().corner(
+ this.bounds.origin.add(aPoint)
+ );
+
+ if (nb.right() > lb.right() && nb.width() <= lb.width()) {
+ this.listContents.setRight(nb.right());
+ }
+ if (nb.bottom() > lb.bottom() && nb.height() <= lb.height()) {
+ this.listContents.setBottom(nb.bottom());
+ }
+ ListMorph.uber.setExtent.call(this, aPoint);
+};
+
+// StringFieldMorph ////////////////////////////////////////////////////
+
+// StringFieldMorph inherit from FrameMorph:
+
+StringFieldMorph.prototype = new FrameMorph();
+StringFieldMorph.prototype.constructor = StringFieldMorph;
+StringFieldMorph.uber = FrameMorph.prototype;
+
+function StringFieldMorph(
+ defaultContents,
+ minWidth,
+ fontSize,
+ fontStyle,
+ bold,
+ italic,
+ isNumeric
+) {
+ this.init(
+ defaultContents || '',
+ minWidth || 100,
+ fontSize || 12,
+ fontStyle || 'sans-serif',
+ bold || false,
+ italic || false,
+ isNumeric
+ );
+}
+
+StringFieldMorph.prototype.init = function (
+ defaultContents,
+ minWidth,
+ fontSize,
+ fontStyle,
+ bold,
+ italic,
+ isNumeric
+) {
+ this.defaultContents = defaultContents;
+ this.minWidth = minWidth;
+ this.fontSize = fontSize;
+ this.fontStyle = fontStyle;
+ this.isBold = bold;
+ this.isItalic = italic;
+ this.isNumeric = isNumeric || false;
+ this.text = null;
+ StringFieldMorph.uber.init.call(this);
+ this.color = new Color(255, 255, 255);
+ this.isEditable = true;
+ this.acceptsDrops = false;
+ this.drawNew();
+};
+
+StringFieldMorph.prototype.drawNew = function () {
+ var txt;
+ txt = this.text ? this.string() : this.defaultContents;
+ this.text = null;
+ this.children.forEach(function (child) {
+ child.destroy();
+ });
+ this.children = [];
+ this.text = new StringMorph(
+ txt,
+ this.fontSize,
+ this.fontStyle,
+ this.isBold,
+ this.isItalic,
+ this.isNumeric
+ );
+
+ this.text.isNumeric = this.isNumeric; // for whichever reason...
+ this.text.setPosition(this.bounds.origin.copy());
+ this.text.isEditable = this.isEditable;
+ this.text.isDraggable = false;
+ this.text.enableSelecting();
+ this.silentSetExtent(
+ new Point(
+ Math.max(this.width(), this.minWidth),
+ this.text.height()
+ )
+ );
+ StringFieldMorph.uber.drawNew.call(this);
+ this.add(this.text);
+};
+
+StringFieldMorph.prototype.string = function () {
+ return this.text.text;
+};
+
+StringFieldMorph.prototype.mouseClickLeft = function (pos) {
+ if (this.isEditable) {
+ this.text.edit();
+ } else {
+ this.escalateEvent('mouseClickLeft', pos);
+ }
+};
+
+// StringFieldMorph duplicating:
+
+StringFieldMorph.prototype.copyRecordingReferences = function (dict) {
+ // inherited, see comment in Morph
+ var c = StringFieldMorph.uber.copyRecordingReferences.call(
+ this,
+ dict
+ );
+ if (c.text && dict[this.text]) {
+ c.text = (dict[this.text]);
+ }
+ return c;
+};
+
+// BouncerMorph ////////////////////////////////////////////////////////
+
+// I am a Demo of a stepping custom Morph
+
+var BouncerMorph;
+
+// Bouncers inherit from Morph:
+
+BouncerMorph.prototype = new Morph();
+BouncerMorph.prototype.constructor = BouncerMorph;
+BouncerMorph.uber = Morph.prototype;
+
+// BouncerMorph instance creation:
+
+function BouncerMorph() {
+ this.init();
+}
+
+// BouncerMorph initialization:
+
+BouncerMorph.prototype.init = function (type, speed) {
+ BouncerMorph.uber.init.call(this);
+ this.fps = 50;
+
+ // additional properties:
+ this.isStopped = false;
+ this.type = type || 'vertical';
+ if (this.type === 'vertical') {
+ this.direction = 'down';
+ } else {
+ this.direction = 'right';
+ }
+ this.speed = speed || 1;
+};
+
+// BouncerMorph moving:
+
+BouncerMorph.prototype.moveUp = function () {
+ this.moveBy(new Point(0, -this.speed));
+};
+
+BouncerMorph.prototype.moveDown = function () {
+ this.moveBy(new Point(0, this.speed));
+};
+
+BouncerMorph.prototype.moveRight = function () {
+ this.moveBy(new Point(this.speed, 0));
+};
+
+BouncerMorph.prototype.moveLeft = function () {
+ this.moveBy(new Point(-this.speed, 0));
+};
+
+// BouncerMorph stepping:
+
+BouncerMorph.prototype.step = function () {
+ if (!this.isStopped) {
+ if (this.type === 'vertical') {
+ if (this.direction === 'down') {
+ this.moveDown();
+ } else {
+ this.moveUp();
+ }
+ if (this.fullBounds().top() < this.parent.top() &&
+ this.direction === 'up') {
+ this.direction = 'down';
+ }
+ if (this.fullBounds().bottom() > this.parent.bottom() &&
+ this.direction === 'down') {
+ this.direction = 'up';
+ }
+ } else if (this.type === 'horizontal') {
+ if (this.direction === 'right') {
+ this.moveRight();
+ } else {
+ this.moveLeft();
+ }
+ if (this.fullBounds().left() < this.parent.left() &&
+ this.direction === 'left') {
+ this.direction = 'right';
+ }
+ if (this.fullBounds().right() > this.parent.right() &&
+ this.direction === 'right') {
+ this.direction = 'left';
+ }
+ }
+ }
+};
+
+// HandMorph ///////////////////////////////////////////////////////////
+
+// I represent the Mouse cursor
+
+// HandMorph inherits from Morph:
+
+HandMorph.prototype = new Morph();
+HandMorph.prototype.constructor = HandMorph;
+HandMorph.uber = Morph.prototype;
+
+// HandMorph instance creation:
+
+function HandMorph(aWorld) {
+ this.init(aWorld);
+}
+
+// HandMorph initialization:
+
+HandMorph.prototype.init = function (aWorld) {
+ HandMorph.uber.init.call(this);
+ this.bounds = new Rectangle();
+
+ // additional properties:
+ this.world = aWorld;
+ this.mouseButton = null;
+ this.mouseOverList = [];
+ this.mouseDownMorph = null;
+ this.morphToGrab = null;
+ this.grabOrigin = null;
+ this.temporaries = [];
+ this.touchHoldTimeout = null;
+};
+
+HandMorph.prototype.changed = function () {
+ var b;
+ if (this.world !== null) {
+ b = this.fullBounds();
+ if (!b.extent().eq(new Point())) {
+ this.world.broken.push(this.fullBounds().spread());
+ }
+ }
+
+};
+
+// HandMorph navigation:
+
+HandMorph.prototype.morphAtPointer = function () {
+ var morphs = this.world.allChildren().slice(0).reverse(),
+ myself = this,
+ result = null;
+
+ morphs.forEach(function (m) {
+ if (m.visibleBounds().containsPoint(myself.bounds.origin) &&
+ result === null &&
+ m.isVisible &&
+ (m.noticesTransparentClick ||
+ (!m.isTransparentAt(myself.bounds.origin))) &&
+ (!(m instanceof ShadowMorph))) {
+ result = m;
+ }
+ });
+ if (result !== null) {
+ return result;
+ }
+ return this.world;
+};
+
+/*
+ alternative - more elegant and possibly more
+ performant - solution for morphAtPointer.
+ Has some issues, commented out for now
+
+HandMorph.prototype.morphAtPointer = function () {
+ var myself = this;
+ return this.world.topMorphSuchThat(function (m) {
+ return m.visibleBounds().containsPoint(myself.bounds.origin) &&
+ m.isVisible &&
+ (m.noticesTransparentClick ||
+ (! m.isTransparentAt(myself.bounds.origin))) &&
+ (! (m instanceof ShadowMorph));
+ });
+};
+*/
+
+HandMorph.prototype.allMorphsAtPointer = function () {
+ var morphs = this.world.allChildren(),
+ myself = this;
+ return morphs.filter(function (m) {
+ return m.isVisible &&
+ m.visibleBounds().containsPoint(myself.bounds.origin);
+ });
+};
+
+// HandMorph dragging and dropping:
+/*
+ drag 'n' drop events, method(arg) -> receiver:
+
+ prepareToBeGrabbed(handMorph) -> grabTarget
+ reactToGrabOf(grabbedMorph) -> oldParent
+ wantsDropOf(morphToDrop) -> newParent
+ justDropped(handMorph) -> droppedMorph
+ reactToDropOf(droppedMorph, handMorph) -> newParent
+*/
+
+HandMorph.prototype.dropTargetFor = function (aMorph) {
+ var target = this.morphAtPointer();
+ while (!target.wantsDropOf(aMorph)) {
+ target = target.parent;
+ }
+ return target;
+};
+
+HandMorph.prototype.grab = function (aMorph) {
+ var oldParent = aMorph.parent;
+ if (aMorph instanceof WorldMorph) {
+ return null;
+ }
+ if (this.children.length === 0) {
+ this.world.stopEditing();
+ this.grabOrigin = aMorph.situation();
+ aMorph.addShadow();
+ if (aMorph.prepareToBeGrabbed) {
+ aMorph.prepareToBeGrabbed(this);
+ }
+ this.add(aMorph);
+ this.changed();
+ if (oldParent && oldParent.reactToGrabOf) {
+ oldParent.reactToGrabOf(aMorph);
+ }
+ }
+};
+
+HandMorph.prototype.drop = function () {
+ var target, morphToDrop;
+ if (this.children.length !== 0) {
+ morphToDrop = this.children[0];
+ target = this.dropTargetFor(morphToDrop);
+ this.changed();
+ target.add(morphToDrop);
+ morphToDrop.changed();
+ morphToDrop.removeShadow();
+ this.children = [];
+ this.setExtent(new Point());
+ if (morphToDrop.justDropped) {
+ morphToDrop.justDropped(this);
+ }
+ if (target.reactToDropOf) {
+ target.reactToDropOf(morphToDrop, this);
+ }
+ this.dragOrigin = null;
+ }
+};
+
+// HandMorph event dispatching:
+/*
+ mouse events:
+
+ mouseDownLeft
+ mouseDownRight
+ mouseClickLeft
+ mouseClickRight
+ mouseEnter
+ mouseLeave
+ mouseEnterDragging
+ mouseLeaveDragging
+ mouseMove
+ mouseScroll
+*/
+
+HandMorph.prototype.processMouseDown = function (event) {
+ var morph, expectedClick, actualClick;
+
+ this.destroyTemporaries();
+ this.morphToGrab = null;
+ if (this.children.length !== 0) {
+ this.drop();
+ this.mouseButton = null;
+ } else {
+ morph = this.morphAtPointer();
+ if (this.world.activeMenu) {
+ if (!contains(
+ morph.allParents(),
+ this.world.activeMenu
+ )) {
+ this.world.activeMenu.destroy();
+ } else {
+ clearInterval(this.touchHoldTimeout);
+ }
+ }
+ if (this.world.activeHandle) {
+ if (morph !== this.world.activeHandle) {
+ this.world.activeHandle.destroy();
+ }
+ }
+ if (this.world.cursor) {
+ if (morph !== this.world.cursor.target) {
+ this.world.stopEditing();
+ }
+ }
+ if (!morph.mouseMove) {
+ this.morphToGrab = morph.rootForGrab();
+ }
+ if (event.button === 2 || event.ctrlKey) {
+ this.mouseButton = 'right';
+ actualClick = 'mouseDownRight';
+ expectedClick = 'mouseClickRight';
+ } else {
+ this.mouseButton = 'left';
+ actualClick = 'mouseDownLeft';
+ expectedClick = 'mouseClickLeft';
+ }
+ this.mouseDownMorph = morph;
+ while (!this.mouseDownMorph[expectedClick]) {
+ this.mouseDownMorph = this.mouseDownMorph.parent;
+ }
+ while (!morph[actualClick]) {
+ morph = morph.parent;
+ }
+ morph[actualClick](this.bounds.origin);
+ }
+};
+
+HandMorph.prototype.processTouchStart = function (event) {
+ var myself = this;
+ clearInterval(this.touchHoldTimeout);
+ if (event.touches.length === 1) {
+ this.touchHoldTimeout = setInterval( // simulate mouseRightClick
+ function () {
+ myself.processMouseDown({button: 2});
+ myself.processMouseUp({button: 2});
+ event.preventDefault();
+ clearInterval(myself.touchHoldTimeout);
+ },
+ 400
+ );
+ this.processMouseMove(event.touches[0]); // update my position
+ this.processMouseDown({button: 0});
+ event.preventDefault();
+ }
+};
+
+HandMorph.prototype.processTouchMove = function (event) {
+ if (event.touches.length === 1) {
+ var touch = event.touches[0];
+ this.processMouseMove(touch);
+ clearInterval(this.touchHoldTimeout);
+ }
+};
+
+HandMorph.prototype.processTouchEnd = function (event) {
+ clearInterval(this.touchHoldTimeout);
+ nop(event);
+ this.processMouseUp({button: 0});
+};
+
+HandMorph.prototype.processMouseUp = function () {
+ var morph = this.morphAtPointer(),
+ context,
+ contextMenu,
+ expectedClick;
+
+ this.destroyTemporaries();
+ if (this.children.length !== 0) {
+ this.drop();
+ } else {
+ if (this.mouseButton === 'left') {
+ expectedClick = 'mouseClickLeft';
+ } else {
+ expectedClick = 'mouseClickRight';
+ if (this.mouseButton) {
+ context = morph;
+ contextMenu = context.contextMenu();
+ while ((!contextMenu) &&
+ context.parent) {
+ context = context.parent;
+ contextMenu = context.contextMenu();
+ }
+ if (contextMenu) {
+ contextMenu.popUpAtHand(this.world);
+ }
+ }
+ }
+ while (!morph[expectedClick]) {
+ morph = morph.parent;
+ }
+ morph[expectedClick](this.bounds.origin);
+ }
+ this.mouseButton = null;
+};
+
+HandMorph.prototype.processMouseMove = function (event) {
+ var pos,
+ posInDocument = getDocumentPositionOf(this.world.worldCanvas),
+ mouseOverNew,
+ myself = this,
+ morph,
+ topMorph,
+ fb;
+
+ pos = new Point(
+ event.pageX - posInDocument.x,
+ event.pageY - posInDocument.y
+ );
+
+ this.setPosition(pos);
+
+ // determine the new mouse-over-list:
+ // mouseOverNew = this.allMorphsAtPointer();
+ mouseOverNew = this.morphAtPointer().allParents();
+
+ if ((this.children.length === 0) &&
+ (this.mouseButton === 'left')) {
+ topMorph = this.morphAtPointer();
+ morph = topMorph.rootForGrab();
+ if (topMorph.mouseMove) {
+ topMorph.mouseMove(pos);
+ }
+
+ // if a morph is marked for grabbing, just grab it
+ if (this.morphToGrab) {
+ if (this.morphToGrab.isDraggable) {
+ morph = this.morphToGrab;
+ this.grab(morph);
+ } else if (this.morphToGrab.isTemplate) {
+ morph = this.morphToGrab.fullCopy();
+ morph.isTemplate = false;
+ morph.isDraggable = true;
+ this.grab(morph);
+ this.grabOrigin = this.morphToGrab.situation();
+ }
+ // if the mouse has left its fullBounds, center it
+ fb = morph.fullBounds();
+ if (!fb.containsPoint(pos)) {
+ this.bounds.origin = fb.center();
+ this.grab(morph);
+ this.setPosition(pos);
+ }
+
+ }
+
+/*
+ original, more cautious code for grabbing Morphs,
+ retained in case of needing to fall back:
+
+ if (morph === this.morphToGrab) {
+ if (morph.isDraggable) {
+ this.grab(morph);
+ } else if (morph.isTemplate) {
+ morph = morph.fullCopy();
+ morph.isTemplate = false;
+ morph.isDraggable = true;
+ this.grab(morph);
+ }
+ }
+*/
+
+ }
+
+ this.mouseOverList.forEach(function (old) {
+ if (!contains(mouseOverNew, old)) {
+ if (old.mouseLeave) {
+ old.mouseLeave();
+ }
+ if (old.mouseLeaveDragging && this.mouseButton) {
+ old.mouseLeaveDragging();
+ }
+ }
+ });
+ mouseOverNew.forEach(function (newMorph) {
+ if (!contains(myself.mouseOverList, newMorph)) {
+ if (newMorph.mouseEnter) {
+ newMorph.mouseEnter();
+ }
+ if (newMorph.mouseEnterDragging && this.mouseButton) {
+ newMorph.mouseEnterDragging();
+ }
+ }
+
+ // autoScrolling support:
+ if (myself.children.length > 0) {
+ if (newMorph instanceof ScrollFrameMorph) {
+ if (!newMorph.bounds.insetBy(
+ MorphicPreferences.scrollBarSize * 3
+ ).containsPoint(myself.bounds.origin)) {
+ newMorph.startAutoScrolling();
+ }
+ }
+ }
+ });
+ this.mouseOverList = mouseOverNew;
+};
+
+HandMorph.prototype.processMouseScroll = function (event) {
+ var morph = this.morphAtPointer();
+ while (morph && !morph.mouseScroll) {
+ morph = morph.parent;
+ }
+ if (morph) {
+ morph.mouseScroll(
+ (event.detail / -3) || (
+ event.hasOwnProperty('wheelDeltaY') ?
+ event.wheelDeltaY / 120 :
+ event.wheelDelta / 120
+ ),
+ event.wheelDeltaX / 120 || 0
+ );
+ }
+};
+
+/*
+ drop event:
+
+ droppedImage
+ droppedSVG
+ droppedAudio
+ droppedText
+*/
+
+HandMorph.prototype.processDrop = function (event) {
+/*
+ find out whether an external image or audio file was dropped
+ onto the world canvas, turn it into an offscreen canvas or audio
+ element and dispatch the
+
+ droppedImage(canvas, name)
+ droppedSVG(image, name)
+ droppedAudio(audio, name)
+
+ events to interested Morphs at the mouse pointer
+*/
+ var files = event instanceof FileList ? event
+ : event.target.files || event.dataTransfer.files,
+ file,
+ txt = event.dataTransfer ?
+ event.dataTransfer.getData('Text/HTML') : null,
+ src,
+ target = this.morphAtPointer(),
+ img = new Image(),
+ canvas,
+ i;
+
+ function readSVG(aFile) {
+ var pic = new Image(),
+ frd = new FileReader();
+ while (!target.droppedSVG) {
+ target = target.parent;
+ }
+ pic.onload = function () {
+ target.droppedSVG(pic, aFile.name);
+ };
+ frd = new FileReader();
+ frd.onloadend = function (e) {
+ pic.src = e.target.result;
+ };
+ frd.readAsDataURL(aFile);
+ }
+
+ function readImage(aFile) {
+ var pic = new Image(),
+ frd = new FileReader();
+ while (!target.droppedImage) {
+ target = target.parent;
+ }
+ pic.onload = function () {
+ canvas = newCanvas(new Point(pic.width, pic.height));
+ canvas.getContext('2d').drawImage(pic, 0, 0);
+ target.droppedImage(canvas, aFile.name);
+ };
+ frd = new FileReader();
+ frd.onloadend = function (e) {
+ pic.src = e.target.result;
+ };
+ frd.readAsDataURL(aFile);
+ }
+
+ function readAudio(aFile) {
+ var snd = new Audio(),
+ frd = new FileReader();
+ while (!target.droppedAudio) {
+ target = target.parent;
+ }
+ frd.onloadend = function (e) {
+ snd.src = e.target.result;
+ target.droppedAudio(snd, aFile.name);
+ };
+ frd.readAsDataURL(aFile);
+ }
+
+ function readText(aFile) {
+ var frd = new FileReader();
+ while (!target.droppedText) {
+ target = target.parent;
+ }
+ frd.onloadend = function (e) {
+ target.droppedText(e.target.result, aFile.name);
+ };
+ frd.readAsText(aFile);
+ }
+
+ function readBinary(aFile) {
+ var frd = new FileReader();
+ while (!target.droppedBinary) {
+ target = target.parent;
+ }
+ frd.onloadend = function (e) {
+ target.droppedBinary(e.target.result, aFile.name);
+ };
+ frd.readAsArrayBuffer(aFile);
+ }
+
+ function parseImgURL(html) {
+ var url = '',
+ i,
+ c,
+ start = html.indexOf('<img src="');
+ if (start === -1) {return null; }
+ start += 10;
+ for (i = start; i < html.length; i += 1) {
+ c = html[i];
+ if (c === '"') {
+ return url;
+ }
+ url = url.concat(c);
+ }
+ return null;
+ }
+
+ if (files.length > 0) {
+ for (i = 0; i < files.length; i += 1) {
+ file = files[i];
+ if (file.type.indexOf("svg") !== -1
+ && !MorphicPreferences.rasterizeSVGs) {
+ readSVG(file);
+ } else if (file.type.indexOf("image") === 0) {
+ readImage(file);
+ } else if (file.type.indexOf("audio") === 0) {
+ readAudio(file);
+ } else if (file.type.indexOf("text") === 0) {
+ readText(file);
+ } else { // assume it's meant to be binary
+ readBinary(file);
+ }
+ }
+ } else if (txt) {
+ while (!target.droppedImage) {
+ target = target.parent;
+ }
+ img = new Image();
+ img.onload = function () {
+ canvas = newCanvas(new Point(img.width, img.height));
+ canvas.getContext('2d').drawImage(img, 0, 0);
+ target.droppedImage(canvas);
+ };
+ src = parseImgURL(txt);
+ if (src) {img.src = src; }
+ }
+};
+
+// HandMorph tools
+
+HandMorph.prototype.destroyTemporaries = function () {
+/*
+ temporaries are just an array of morphs which will be deleted upon
+ the next mouse click, or whenever another temporary Morph decides
+ that it needs to remove them. The primary purpose of temporaries is
+ to display tools tips of speech bubble help.
+*/
+ var myself = this;
+ this.temporaries.forEach(function (morph) {
+ if (!(morph.isClickable
+ && morph.bounds.containsPoint(myself.position()))) {
+ morph.destroy();
+ myself.temporaries.splice(myself.temporaries.indexOf(morph), 1);
+ }
+ });
+};
+
+// HandMorph dragging optimization
+
+HandMorph.prototype.moveBy = function (delta) {
+ Morph.prototype.trackChanges = false;
+ HandMorph.uber.moveBy.call(this, delta);
+ Morph.prototype.trackChanges = true;
+ this.fullChanged();
+};
+
+
+// WorldMorph //////////////////////////////////////////////////////////
+
+// I represent the <canvas> element
+
+// WorldMorph inherits from FrameMorph:
+
+WorldMorph.prototype = new FrameMorph();
+WorldMorph.prototype.constructor = WorldMorph;
+WorldMorph.uber = FrameMorph.prototype;
+
+// WorldMorph instance creation:
+
+function WorldMorph(aCanvas, fillPage) {
+ this.init(aCanvas, fillPage);
+}
+
+// WorldMorph initialization:
+
+WorldMorph.prototype.init = function (aCanvas, fillPage) {
+ WorldMorph.uber.init.call(this);
+ this.color = new Color(205, 205, 205); // (130, 130, 130)
+ this.alpha = 1;
+ this.bounds = new Rectangle(0, 0, aCanvas.width, aCanvas.height);
+ this.drawNew();
+ this.isVisible = true;
+ this.isDraggable = false;
+ this.currentKey = null; // currently pressed key code
+ this.worldCanvas = aCanvas;
+
+ // additional properties:
+ this.useFillPage = fillPage;
+ if (this.useFillPage === undefined) {
+ this.useFillPage = true;
+ }
+ this.isDevMode = false;
+ this.broken = [];
+ this.hand = new HandMorph(this);
+ this.keyboardReceiver = null;
+ this.lastEditedText = null;
+ this.cursor = null;
+ this.activeMenu = null;
+ this.activeHandle = null;
+ this.virtualKeyboard = null;
+
+ this.initEventListeners();
+};
+
+// World Morph display:
+
+WorldMorph.prototype.brokenFor = function (aMorph) {
+ // private
+ var fb = aMorph.fullBounds();
+ return this.broken.filter(function (rect) {
+ return rect.intersects(fb);
+ });
+};
+
+WorldMorph.prototype.fullDrawOn = function (aCanvas, aRect) {
+ WorldMorph.uber.fullDrawOn.call(this, aCanvas, aRect);
+ this.hand.fullDrawOn(aCanvas, aRect);
+};
+
+WorldMorph.prototype.updateBroken = function () {
+ var myself = this;
+ this.broken.forEach(function (rect) {
+ if (rect.extent().gt(new Point(0, 0))) {
+ myself.fullDrawOn(myself.worldCanvas, rect);
+ }
+ });
+ this.broken = [];
+};
+
+WorldMorph.prototype.doOneCycle = function () {
+ this.stepFrame();
+ this.updateBroken();
+};
+
+WorldMorph.prototype.fillPage = function () {
+ var pos = getDocumentPositionOf(this.worldCanvas),
+ clientHeight = window.innerHeight,
+ clientWidth = window.innerWidth,
+ myself = this;
+
+
+ if (pos.x > 0) {
+ this.worldCanvas.style.position = "absolute";
+ this.worldCanvas.style.left = "0px";
+ pos.x = 0;
+ }
+ if (pos.y > 0) {
+ this.worldCanvas.style.position = "absolute";
+ this.worldCanvas.style.top = "0px";
+ pos.y = 0;
+ }
+ if (document.body.scrollTop) { // scrolled down b/c of viewport scaling
+ clientHeight = document.documentElement.clientHeight;
+ }
+ if (document.body.scrollLeft) { // scrolled left b/c of viewport scaling
+ clientWidth = document.documentElement.clientWidth;
+ }
+
+ if (this.worldCanvas.width !== clientWidth) {
+ this.worldCanvas.width = clientWidth;
+ this.setWidth(clientWidth);
+ }
+ if (this.worldCanvas.height !== clientHeight) {
+ this.worldCanvas.height = clientHeight;
+ this.setHeight(clientHeight);
+ }
+ this.children.forEach(function (child) {
+ if (child.reactToWorldResize) {
+ child.reactToWorldResize(myself.bounds.copy());
+ }
+ });
+};
+
+// WorldMorph global pixel access:
+
+WorldMorph.prototype.getGlobalPixelColor = function (point) {
+/*
+ answer the color at the given point.
+
+ Note: for some strange reason this method works fine if the page is
+ opened via HTTP, but *not*, if it is opened from a local uri
+ (e.g. from a directory), in which case it's always null.
+
+ This behavior is consistent throughout several browsers. I have no
+ clue what's behind this, apparently the imageData attribute of
+ canvas context only gets filled with meaningful data if transferred
+ via HTTP ???
+
+ This is somewhat of a showstopper for color detection in a planned
+ offline version of Snap.
+
+ The issue has also been discussed at: (join lines before pasting)
+ http://stackoverflow.com/questions/4069400/
+ canvas-getimagedata-doesnt-work-when-running-locally-on-windows-
+ security-excep
+
+ The suggestion solution appears to work, since the settings are
+ applied globally.
+*/
+ var dta = this.worldCanvas.getContext('2d').getImageData(
+ point.x,
+ point.y,
+ 1,
+ 1
+ ).data;
+ return new Color(dta[0], dta[1], dta[2]);
+};
+
+// WorldMorph events:
+
+WorldMorph.prototype.initVirtualKeyboard = function () {
+ var myself = this;
+
+ if (this.virtualKeyboard) {
+ document.body.removeChild(this.virtualKeyboard);
+ this.virtualKeyboard = null;
+ }
+ if (!MorphicPreferences.useVirtualKeyboard) {
+ return;
+ }
+ this.virtualKeyboard = document.createElement("input");
+ this.virtualKeyboard.type = "text";
+ this.virtualKeyboard.style.color = "transparent";
+ this.virtualKeyboard.style.backgroundColor = "transparent";
+ this.virtualKeyboard.style.border = "none";
+ this.virtualKeyboard.style.outline = "none";
+ this.virtualKeyboard.style.position = "absolute";
+ this.virtualKeyboard.style.top = "0px";
+ this.virtualKeyboard.style.left = "0px";
+ this.virtualKeyboard.style.width = "0px";
+ this.virtualKeyboard.style.height = "0px";
+ document.body.appendChild(this.virtualKeyboard);
+
+ this.virtualKeyboard.addEventListener(
+ "keydown",
+ function (event) {
+ // remember the keyCode in the world's currentKey property
+ myself.currentKey = event.keyCode;
+ if (myself.keyboardReceiver) {
+ myself.keyboardReceiver.processKeyDown(event);
+ }
+ // supress backspace override
+ if (event.keyIdentifier === 'U+0008' ||
+ event.keyIdentifier === 'Backspace') {
+ event.preventDefault();
+ }
+ // supress tab override and make sure tab gets
+ // received by all browsers
+ if (event.keyIdentifier === 'U+0009' ||
+ event.keyIdentifier === 'Tab') {
+ if (myself.keyboardReceiver) {
+ myself.keyboardReceiver.processKeyPress(event);
+ }
+ event.preventDefault();
+ }
+ },
+ false
+ );
+
+ this.virtualKeyboard.addEventListener(
+ "keyup",
+ function (event) {
+ // flush the world's currentKey property
+ myself.currentKey = null;
+ // dispatch to keyboard receiver
+ if (myself.keyboardReceiver) {
+ if (myself.keyboardReceiver.processKeyUp) {
+ myself.keyboardReceiver.processKeyUp(event);
+ }
+ }
+ event.preventDefault();
+ },
+ false
+ );
+
+ this.virtualKeyboard.addEventListener(
+ "keypress",
+ function (event) {
+ if (myself.keyboardReceiver) {
+ myself.keyboardReceiver.processKeyPress(event);
+ }
+ event.preventDefault();
+ },
+ false
+ );
+};
+
+WorldMorph.prototype.initEventListeners = function () {
+ var canvas = this.worldCanvas, myself = this;
+
+ if (myself.useFillPage) {
+ myself.fillPage();
+ } else {
+ this.changed();
+ }
+
+ canvas.addEventListener(
+ "mousedown",
+ function (event) {
+ myself.hand.processMouseDown(event);
+ },
+ false
+ );
+
+ canvas.addEventListener(
+ "touchstart",
+ function (event) {
+ myself.hand.processTouchStart(event);
+ },
+ false
+ );
+
+ canvas.addEventListener(
+ "mouseup",
+ function (event) {
+ event.preventDefault();
+ myself.hand.processMouseUp(event);
+ },
+ false
+ );
+
+ canvas.addEventListener(
+ "touchend",
+ function (event) {
+ myself.hand.processTouchEnd(event);
+ },
+ false
+ );
+
+ canvas.addEventListener(
+ "mousemove",
+ function (event) {
+ myself.hand.processMouseMove(event);
+ },
+ false
+ );
+
+ canvas.addEventListener(
+ "touchmove",
+ function (event) {
+ myself.hand.processTouchMove(event);
+ },
+ false
+ );
+
+ canvas.addEventListener(
+ "contextmenu",
+ function (event) {
+ // suppress context menu for Mac-Firefox
+ event.preventDefault();
+ },
+ false
+ );
+
+ canvas.addEventListener(
+ "keydown",
+ function (event) {
+ // remember the keyCode in the world's currentKey property
+ myself.currentKey = event.keyCode;
+ if (myself.keyboardReceiver) {
+ myself.keyboardReceiver.processKeyDown(event);
+ }
+ // supress backspace override
+ if (event.keyIdentifier === 'U+0008' ||
+ event.keyIdentifier === 'Backspace') {
+ event.preventDefault();
+ }
+ // supress tab override and make sure tab gets
+ // received by all browsers
+ if (event.keyIdentifier === 'U+0009' ||
+ event.keyIdentifier === 'Tab') {
+ if (myself.keyboardReceiver) {
+ myself.keyboardReceiver.processKeyPress(event);
+ }
+ event.preventDefault();
+ }
+ },
+ false
+ );
+
+ canvas.addEventListener(
+ "keyup",
+ function (event) {
+ // flush the world's currentKey property
+ myself.currentKey = null;
+ // dispatch to keyboard receiver
+ if (myself.keyboardReceiver) {
+ if (myself.keyboardReceiver.processKeyUp) {
+ myself.keyboardReceiver.processKeyUp(event);
+ }
+ }
+ event.preventDefault();
+ },
+ false
+ );
+
+ canvas.addEventListener(
+ "keypress",
+ function (event) {
+ if (myself.keyboardReceiver) {
+ myself.keyboardReceiver.processKeyPress(event);
+ }
+ event.preventDefault();
+ },
+ false
+ );
+
+ canvas.addEventListener( // Safari, Chrome
+ "mousewheel",
+ function (event) {
+ myself.hand.processMouseScroll(event);
+ event.preventDefault();
+ },
+ false
+ );
+ canvas.addEventListener( // Firefox
+ "DOMMouseScroll",
+ function (event) {
+ myself.hand.processMouseScroll(event);
+ event.preventDefault();
+ },
+ false
+ );
+
+ document.body.addEventListener(
+ "paste",
+ function (event) {
+ var txt = event.clipboardData.getData("Text");
+ if (txt && myself.cursor) {
+ myself.cursor.insert(txt);
+ }
+ },
+ false
+ );
+
+ window.addEventListener(
+ "dragover",
+ function (event) {
+ event.preventDefault();
+ },
+ false
+ );
+ window.addEventListener(
+ "drop",
+ function (event) {
+ myself.hand.processDrop(event);
+ event.preventDefault();
+ },
+ false
+ );
+
+ window.addEventListener(
+ "resize",
+ function () {
+ if (myself.useFillPage) {
+ myself.fillPage();
+ }
+ },
+ false
+ );
+
+ window.onbeforeunload = function (evt) {
+ var e = evt || window.event,
+ msg = "Are you sure you want to leave?";
+ // For IE and Firefox
+ if (e) {
+ e.returnValue = msg;
+ }
+ // For Safari / chrome
+ return msg;
+ };
+};
+
+WorldMorph.prototype.mouseDownLeft = function () {
+ nop();
+};
+
+WorldMorph.prototype.mouseClickLeft = function () {
+ nop();
+};
+
+WorldMorph.prototype.mouseDownRight = function () {
+ nop();
+};
+
+WorldMorph.prototype.mouseClickRight = function () {
+ nop();
+};
+
+WorldMorph.prototype.wantsDropOf = function () {
+ // allow handle drops if any drops are allowed
+ return this.acceptsDrops;
+};
+
+WorldMorph.prototype.droppedImage = function () {
+ return null;
+};
+
+WorldMorph.prototype.droppedSVG = function () {
+ return null;
+};
+
+// WorldMorph text field tabbing:
+
+WorldMorph.prototype.nextTab = function (editField) {
+ var next = this.nextEntryField(editField);
+ if (next) {
+ editField.clearSelection();
+ next.selectAll();
+ next.edit();
+ }
+};
+
+WorldMorph.prototype.previousTab = function (editField) {
+ var prev = this.previousEntryField(editField);
+ if (prev) {
+ editField.clearSelection();
+ prev.selectAll();
+ prev.edit();
+ }
+};
+
+// WorldMorph menu:
+
+WorldMorph.prototype.contextMenu = function () {
+ var menu;
+
+ if (this.isDevMode) {
+ menu = new MenuMorph(this, this.constructor.name ||
+ this.constructor.toString().split(' ')[1].split('(')[0]);
+ } else {
+ menu = new MenuMorph(this, 'Morphic');
+ }
+ if (this.isDevMode) {
+ menu.addItem("demo...", 'userCreateMorph', 'sample morphs');
+ menu.addLine();
+ menu.addItem("hide all...", 'hideAll');
+ menu.addItem("show all...", 'showAllHiddens');
+ menu.addItem(
+ "move all inside...",
+ 'keepAllSubmorphsWithin',
+ 'keep all submorphs\nwithin and visible'
+ );
+ menu.addItem(
+ "inspect...",
+ 'inspect',
+ 'open a window on\nall properties'
+ );
+ menu.addLine();
+ menu.addItem(
+ "restore display",
+ 'changed',
+ 'redraw the\nscreen once'
+ );
+ menu.addItem(
+ "fill page...",
+ 'fillPage',
+ 'let the World automatically\nadjust to browser resizings'
+ );
+ if (useBlurredShadows) {
+ menu.addItem(
+ "sharp shadows...",
+ 'toggleBlurredShadows',
+ 'sharp drop shadows\nuse for old browsers'
+ );
+ } else {
+ menu.addItem(
+ "blurred shadows...",
+ 'toggleBlurredShadows',
+ 'blurry shades,\n use for new browsers'
+ );
+ }
+ menu.addItem(
+ "color...",
+ function () {
+ this.pickColor(
+ menu.title + '\ncolor:',
+ this.setColor,
+ this,
+ this.color
+ );
+ },
+ 'choose the World\'s\nbackground color'
+ );
+ if (MorphicPreferences === standardSettings) {
+ menu.addItem(
+ "touch screen settings",
+ 'togglePreferences',
+ 'bigger menu fonts\nand sliders'
+ );
+ } else {
+ menu.addItem(
+ "standard settings",
+ 'togglePreferences',
+ 'smaller menu fonts\nand sliders'
+ );
+ }
+ menu.addLine();
+ }
+ if (this.isDevMode) {
+ menu.addItem(
+ "user mode...",
+ 'toggleDevMode',
+ 'disable developers\'\ncontext menus'
+ );
+ } else {
+ menu.addItem("development mode...", 'toggleDevMode');
+ }
+ menu.addItem("about morphic.js...", 'about');
+ return menu;
+};
+
+WorldMorph.prototype.userCreateMorph = function () {
+ var myself = this, menu, newMorph;
+
+ function create(aMorph) {
+ aMorph.isDraggable = true;
+ aMorph.pickUp(myself);
+ }
+
+ menu = new MenuMorph(this, 'make a morph');
+ menu.addItem('rectangle', function () {
+ create(new Morph());
+ });
+ menu.addItem('box', function () {
+ create(new BoxMorph());
+ });
+ menu.addItem('circle box', function () {
+ create(new CircleBoxMorph());
+ });
+ menu.addLine();
+ menu.addItem('slider', function () {
+ create(new SliderMorph());
+ });
+ menu.addItem('frame', function () {
+ newMorph = new FrameMorph();
+ newMorph.setExtent(new Point(350, 250));
+ create(newMorph);
+ });
+ menu.addItem('scroll frame', function () {
+ newMorph = new ScrollFrameMorph();
+ newMorph.contents.acceptsDrops = true;
+ newMorph.contents.adjustBounds();
+ newMorph.setExtent(new Point(350, 250));
+ create(newMorph);
+ });
+ menu.addItem('handle', function () {
+ create(new HandleMorph());
+ });
+ menu.addLine();
+ menu.addItem('string', function () {
+ newMorph = new StringMorph('Hello, World!');
+ newMorph.isEditable = true;
+ create(newMorph);
+ });
+ menu.addItem('text', function () {
+ newMorph = new TextMorph(
+ "Ich wei\u00DF nicht, was soll es bedeuten, dass ich so " +
+ "traurig bin, ein M\u00E4rchen aus uralten Zeiten, das " +
+ "kommt mir nicht aus dem Sinn. Die Luft ist k\u00FChl " +
+ "und es dunkelt, und ruhig flie\u00DFt der Rhein; der " +
+ "Gipfel des Berges funkelt im Abendsonnenschein. " +
+ "Die sch\u00F6nste Jungfrau sitzet dort oben wunderbar, " +
+ "ihr gold'nes Geschmeide blitzet, sie k\u00E4mmt ihr " +
+ "goldenes Haar, sie k\u00E4mmt es mit goldenem Kamme, " +
+ "und singt ein Lied dabei; das hat eine wundersame, " +
+ "gewalt'ge Melodei. Den Schiffer im kleinen " +
+ "Schiffe, ergreift es mit wildem Weh; er schaut " +
+ "nicht die Felsenriffe, er schaut nur hinauf in " +
+ "die H\u00F6h'. Ich glaube, die Wellen verschlingen " +
+ "am Ende Schiffer und Kahn, und das hat mit ihrem " +
+ "Singen, die Loreley getan."
+ );
+ newMorph.isEditable = true;
+ newMorph.maxWidth = 300;
+ newMorph.drawNew();
+ create(newMorph);
+ });
+ menu.addItem('speech bubble', function () {
+ newMorph = new SpeechBubbleMorph('Hello, World!');
+ create(newMorph);
+ });
+ menu.addLine();
+ menu.addItem('gray scale palette', function () {
+ create(new GrayPaletteMorph());
+ });
+ menu.addItem('color palette', function () {
+ create(new ColorPaletteMorph());
+ });
+ menu.addItem('color picker', function () {
+ create(new ColorPickerMorph());
+ });
+ menu.addLine();
+ menu.addItem('sensor demo', function () {
+ newMorph = new MouseSensorMorph();
+ newMorph.setColor(new Color(230, 200, 100));
+ newMorph.edge = 35;
+ newMorph.border = 15;
+ newMorph.borderColor = new Color(200, 100, 50);
+ newMorph.alpha = 0.2;
+ newMorph.setExtent(new Point(100, 100));
+ create(newMorph);
+ });
+ menu.addItem('animation demo', function () {
+ var foo, bar, baz, garply, fred;
+
+ foo = new BouncerMorph();
+ foo.setPosition(new Point(50, 20));
+ foo.setExtent(new Point(300, 200));
+ foo.alpha = 0.9;
+ foo.speed = 3;
+
+ bar = new BouncerMorph();
+ bar.setColor(new Color(50, 50, 50));
+ bar.setPosition(new Point(80, 80));
+ bar.setExtent(new Point(80, 250));
+ bar.type = 'horizontal';
+ bar.direction = 'right';
+ bar.alpha = 0.9;
+ bar.speed = 5;
+
+ baz = new BouncerMorph();
+ baz.setColor(new Color(20, 20, 20));
+ baz.setPosition(new Point(90, 140));
+ baz.setExtent(new Point(40, 30));
+ baz.type = 'horizontal';
+ baz.direction = 'right';
+ baz.speed = 3;
+
+ garply = new BouncerMorph();
+ garply.setColor(new Color(200, 20, 20));
+ garply.setPosition(new Point(90, 140));
+ garply.setExtent(new Point(20, 20));
+ garply.type = 'vertical';
+ garply.direction = 'up';
+ garply.speed = 8;
+
+ fred = new BouncerMorph();
+ fred.setColor(new Color(20, 200, 20));
+ fred.setPosition(new Point(120, 140));
+ fred.setExtent(new Point(20, 20));
+ fred.type = 'vertical';
+ fred.direction = 'down';
+ fred.speed = 4;
+
+ bar.add(garply);
+ bar.add(baz);
+ foo.add(fred);
+ foo.add(bar);
+
+ create(foo);
+ });
+ menu.addItem('pen', function () {
+ create(new PenMorph());
+ });
+ if (myself.customMorphs) {
+ menu.addLine();
+ myself.customMorphs().forEach(function (morph) {
+ menu.addItem(morph.toString(), function () {
+ create(morph);
+ });
+ });
+ }
+ menu.popUpAtHand(this);
+};
+
+WorldMorph.prototype.toggleDevMode = function () {
+ this.isDevMode = !this.isDevMode;
+};
+
+WorldMorph.prototype.hideAll = function () {
+ this.children.forEach(function (child) {
+ child.hide();
+ });
+};
+
+WorldMorph.prototype.showAllHiddens = function () {
+ this.forAllChildren(function (child) {
+ if (!child.isVisible) {
+ child.show();
+ }
+ });
+};
+
+WorldMorph.prototype.about = function () {
+ var versions = '', module;
+
+ for (module in modules) {
+ if (modules.hasOwnProperty(module)) {
+ versions += ('\n' + module + ' (' + modules[module] + ')');
+ }
+ }
+ if (versions !== '') {
+ versions = '\n\nmodules:\n\n' +
+ 'morphic (' + morphicVersion + ')' +
+ versions;
+ }
+
+ this.inform(
+ 'morphic.js\n\n' +
+ 'a lively Web GUI\ninspired by Squeak\n' +
+ morphicVersion +
+ '\n\nwritten by Jens M\u00F6nig\njens@moenig.org' +
+ versions
+ );
+};
+
+WorldMorph.prototype.edit = function (aStringOrTextMorph) {
+ var pos = getDocumentPositionOf(this.worldCanvas);
+
+ if (!aStringOrTextMorph.isEditable) {
+ return null;
+ }
+ if (this.cursor) {
+ this.cursor.destroy();
+ }
+ if (this.lastEditedText) {
+ this.lastEditedText.clearSelection();
+ }
+ this.cursor = new CursorMorph(aStringOrTextMorph);
+ aStringOrTextMorph.parent.add(this.cursor);
+ this.keyboardReceiver = this.cursor;
+
+ this.initVirtualKeyboard();
+ if (MorphicPreferences.useVirtualKeyboard) {
+ this.virtualKeyboard.style.top = this.cursor.top() + pos.y + "px";
+ this.virtualKeyboard.style.left = this.cursor.left() + pos.x + "px";
+ this.virtualKeyboard.focus();
+ }
+
+ if (MorphicPreferences.useSliderForInput) {
+ if (!aStringOrTextMorph.parentThatIsA(MenuMorph)) {
+ this.slide(aStringOrTextMorph);
+ }
+ }
+};
+
+WorldMorph.prototype.slide = function (aStringOrTextMorph) {
+ // display a slider for numeric text entries
+ var val = parseFloat(aStringOrTextMorph.text),
+ menu,
+ slider;
+
+ if (isNaN(val)) {
+ val = 0;
+ }
+ menu = new MenuMorph();
+ slider = new SliderMorph(
+ val - 25,
+ val + 25,
+ val,
+ 10,
+ 'horizontal'
+ );
+ slider.alpha = 1;
+ slider.color = new Color(225, 225, 225);
+ slider.button.color = menu.borderColor;
+ slider.button.highlightColor = slider.button.color.copy();
+ slider.button.highlightColor.b += 100;
+ slider.button.pressColor = slider.button.color.copy();
+ slider.button.pressColor.b += 150;
+ slider.silentSetHeight(MorphicPreferences.scrollBarSize);
+ slider.silentSetWidth(MorphicPreferences.menuFontSize * 10);
+ slider.drawNew();
+ slider.action = function (num) {
+ aStringOrTextMorph.changed();
+ aStringOrTextMorph.text = Math.round(num).toString();
+ aStringOrTextMorph.drawNew();
+ aStringOrTextMorph.changed();
+ aStringOrTextMorph.escalateEvent(
+ 'reactToSliderEdit',
+ aStringOrTextMorph
+ );
+ };
+ menu.items.push(slider);
+ menu.popup(this, aStringOrTextMorph.bottomLeft().add(new Point(0, 5)));
+};
+
+WorldMorph.prototype.stopEditing = function () {
+ if (this.cursor) {
+ this.lastEditedText = this.cursor.target;
+ this.cursor.destroy();
+ this.cursor = null;
+ this.lastEditedText.escalateEvent('reactToEdit', this.lastEditedText);
+ }
+ this.keyboardReceiver = null;
+ if (this.virtualKeyboard) {
+ this.virtualKeyboard.blur();
+ document.body.removeChild(this.virtualKeyboard);
+ this.virtualKeyboard = null;
+ }
+ this.worldCanvas.focus();
+};
+
+WorldMorph.prototype.toggleBlurredShadows = function () {
+ useBlurredShadows = !useBlurredShadows;
+};
+
+WorldMorph.prototype.togglePreferences = function () {
+ if (MorphicPreferences === standardSettings) {
+ MorphicPreferences = touchScreenSettings;
+ } else {
+ MorphicPreferences = standardSettings;
+ }
+};