Biography
Alastair grew up in Calgary and later moved to Victoria on Vancouver Island. In August of 1998 Alastair moved to Rome Italy to complete his high school education at the international school Sainto Stefano di Roma (http://www.ststephens.it/). After completing this year abroad Alastair returned to Victoria to begin his post-secondary education. Alastair is currently in his final term of Computer Engineering in the faculty of Engineering at the University of Victoria. He enjoys many physical activities including rollerblading, skateboarding, soccer, ultimate and frisbee golf. Alastair also works with the open source group SPARK (http://www.schemasoft.org) in his free time.
This is a paper on how to create an atom widget that follows the SPARK framework. This FAQ is for people new to the SPARK framework and goes through the basic steps one must follow to create their own customized widgets. It describes the process of defining the functionality, creating the SVG, creating the code and finally on how to skin the SVG. It should help the reader understand the framework and learn how to create a basic widget. The widget that will be created is a button widget that may be moved.
1. Setup
2. Designing the SVG
2.1 SPARK SVG Requirements
2.2 Atom SVG Requirements
2.3 Widget Specific SVG
3. Writing the Code
3.1 Interfaces
3.2 Code Reuse
3.3 Original Code
4. Using the Widget
4.1 SPARKFactory Class
4.2 The Application
4.2.1 SVG
4.2.2 Code
4.3 Decorating the Widget
5. Skinning the Widget
6. The Final Product
This first step to creating a widget using the SPARK framework is to decide the functionality. What are the functions that this widget will perform? Does this widget need to contain other widgets? For this demonstration we will be creating a simple button widget with a twist. As this is a simple button and will be containing no other widgets we may classify it as an Atom widget.
The twist to the button is that we would like the button to have the ability to be moved around. With this in mind the mouse functionality should be well defined before we create the widget. In order to accomplish this desired functionality the mouse events will be as follows:
Another point that must be decided is how to accomplish the movement. From past experience it has been found to be desireable to use a drag square for the movement as opposed to moving the entire widget. Therefore for this demonstration the widget will have the following flow:
Finally, the name of our widget will be "MoveableButton" - suiteably self descriptive.
Once the desired functionality has been designed, the next step is to design the SVG.
The first requirments of the SPARK framework is to satisfy the generic SPARK widget SVG requirments. The generic widget SVG looks as follows:
<g id="uniqueID" class="SPARK widgetType widgetName CSS"> <desc>Required - Widget accessibility description</desc> <metadata>Optional - Model storage location</metadata> ... any svg content ... </g> |
Figure 1: Generic Widget SVG Structure
Take note of the id and class attributes as well as the desc element. These three are all required under the SPARK framework. Note the class attribute is "SPARK widgetType widgetName" - these three are required and the CSS is optional. Also note that initially we have no use for the metadata element. It is not a necessity for this demonstration.
Using our description from Chapter 1 we may classify our widget as an Atom widget. It will not contain any other widgets. The SPARK description of an atom widget is as follows:
In SPARK an atom is considered to be the most basic widget type possible. An atom may contain no other widgets. ... An atom may or may not have a visual representation (View). Finally, an atom should have no direct knowledge of any other widget.
Figure 2: Definition of an Atom Widget
The SVG skeleton of a SPARK Atom widget is as follows (note the class attribute - "SPARK atom WidgetName"):
<g id="uniqueID" class="SPARK atom WidgetName"> <desc>Required - Description</desc> <metadata>Optional - Model</metadata> ... Any SVG content (View)... </g> |
Figure 3: Atom SVG Structure
Therefore, combining the SPARK definition and the Atom definition the following SVG structure is created (the widget will be the only in the application therefore the ID has been designated mb-1 for moveablebutton-1):
<g id="mb-1" class="SPARK atom MoveableButton"> <desc>This is a moveable button. It can be clicked and it can also be moved. To move simply mousedown and drag the button to the desired location.</desc> </g> |
Once the SVG requirements of the SPARK framework have been fulfilled, the widget specific SVG may be created.
The widget will initially have a very simple skin - a simple rectangle with a stroke and a fill. It will also have text that describes the action that this button is associated with.
<rect stroke="black" fill="red" width="100" height="50"/> <text x="25" y="25" stroke="white">Click me!</text> |
Next we must take into account the skeleton rectangle that will be used when dragging the button. We will define this skeleton rectangle as follows (note the id attribute has been set to mb-1-drag - this will be discussed later in Section 3.3 ):
<rect id="mb-1-drag" stroke="black" fill="none" width="100" height="50"/> |
Finally we wish to avoid the cursor mouse action (when the mouse is over text) so an opaque rect will be placed over the widget to gain this effect. This is accomplished as follows:
<rect stroke="black" fill="white" opacity="0" width="100" height="50"/> |
Bringing this all together we arrive with the following completed SVG for the MoveableButton widget.
<g id="mb-1" class="SPARK atom MoveableButton"> <desc>This is a moveable button. It can be clicked and it can also be moved. To move simply mousedown and drag the button to the desired location.</desc> <rect stroke="black" fill="red" width="100" height="50"/> <text x="25" y="25" stroke="white">Click me!</text> <rect id="mb-1-drag" stroke="black" fill="none" width="100" height="50"/> <rect stroke="black" fill="white" opacity="0" width="100" height="50"/> </g> |
Figure 4: MoveableButton SVG
The widget we are creating is an atom widget so therefore we must implement one interface (Observable) and extend one class (Atom). The interfaces are defined as follows (Java definitions):
/** * @file observable.java * * Describes the Observable interface for the SPARK framework. * * @author Alastair Fettes * * @see org.SchemaSoft.dom.SPARK.Observer * @see org.SchemaSoft.dom.SPARK.SPARK */ package org.SchemaSoft.dom.SPARK; public interface Observable { /** * Add an observer to the set of listeners for this widget. * @param in_pObserver The observer to add. */ public void addObserver( Observer in_pObserver ); /** * Remove an observer from the set of listeners for this widget. * @param in_pObserver The observer to remove. */ public void removeObserver( Observer in_pObserver ); } |
Figure 5: Observable Interface (Java)
/** * @file atom.java * * Describes the Atom Widget type from the SPARK framework. * * @author Alastair Fettes * * @see org.SchemaSoft.dom.SPARK.Observable * @see org.SchemaSoft.dom.SPARK.SPARK */ package org.SchemaSoft.dom.SPARK; public abstract class Atom extends SPARK implements Observerable { /** * Get the value that the atom stores. Can be "" * @return The value for this atom. */ public String getValue(); /** * Interface members. */ public void addObserver( Observer in_pObserver ); public void removeObserver( Observer in_pObserver ); } |
Figure 6: Atom Class (Java)
The Atom class extends the SPARK class, defined below.
/** * @file spark.java * * The SPARK class describes basic Widget functionality. All widgets * in the SPARK kit must inherit from the SPARK class either directly * or indirectly through another class such as Atom or Container. * * The SPARK class also contains static helper methods and properties. * * @author Alastair Fettes * * @see java.util.Vector */ import java.util.Vector; package org.SchemaSoft.dom.SPARK; public class SPARK { /** * The document-wide unique id of this widget. */ public final string ID; /** * Retrieve the widget's state. * @return The widgets state value. */ public Object getState(); /** * Set the widgets state. * @param in_state The new state value. */ public void setState( Object in_state ); // Static functions and variables removed // see Helper Classes section for definition. // (These are not neccessary to implement a widget). } |
Figure 7: SPARK.java (Java)
As there are already other atom widgets created (namely the Button widget) most of the work for this part has already been done for us. The following code can be used from the Button class in order to implement the MoveableButton script class.
The following code was reused from the Button widget class. The following is straight from that class with the names changed appropriately. The description and need for each copy is posted along with the code.
The constructor was used to get the general code for doing inheritance. This guarnatees that the ID and CLASS attributes will be set.
/** * MoveableButton constructor. * @param in_node The SVG base node for the moveablebutton widget. */ function MoveableButton( in_node ) { // inheritance this.base = Atom; this.base( in_node.getAttribute( "id" ), Button.CLASS ); } |
Figure 8: Constructor
Theese static variables were used to do two things. First it is used to set the inheritance that MoveableButton extends the Atom class. Second it sets the global static variables for the CLASS and REGEX which are used to distinguish MoveableButton class group SVG elements from other widget types group SVG elements.
/** * public class MoveableButton extends Atom */ MoveableButton.prototype = new Atom; /** * Regular expression defining the class attribute for a MoveableButton * SVGElement root element. */ MoveableButton.REGEX = /^SPARK atom MoveableButton/; /** * Regular expression defining the class attribute for a MoveableButton * SVGElement root element. */ MoveableButton.CLASS = "SPARK atom MoveableButton"; |
Figure 9: Static Variables
The state and value of an Atom widget are largely dependent on the function of the widget itself. These are not part of the superclass Atom due to this fact. Therefore we may implement these at will.
/** * Get the moveablebuttons state. * @return The moveablebuttons state. */ MoveableButton.prototype.getState = function() { return( false ); }; /** * Get the moveablebuttons state. * @return The moveablebuttons state. */ MoveableButton.prototype.setState = function( in_state ) { // do nothing }; /** * Get the moveablebuttons value. * @return The moveablebuttons value. */ MoveableButton.prototype.getValue = function() { return( "" ); }; |
Figure 10: State and Value Functions
This is just a useful function that was copied for simplicity and ease sake.
/** * Notify all observers. */ MoveableButton.prototype.notifyObservers = function() { for( var i = 0; i < this.m_vObservers.length; i++ ) { this.m_vObservers[i].notify( this ); } }; |
Figure 11: notifyObservers()
This we have yet to implement. It should always be present in the case that the application developer requires the ability to create a widget on the fly./
/** * Factory function for creating a new MoveableButton. * @param in_parent The parent SVGElement to this widget. * @return The new MoveableButton. */ MoveableButton.createWidget = function( in_parent, in_id, in_svgView ) { // empty for now }; |
Figure 12: createWidget Factory Function
Now that the necessary code to comply with the framework definition has been completed, the widget's specialized code may be added. The inital functionality to test this out is to have a simple alert box when the button is clicked and to move the button. To do this the following methods must be added:
To support these the constructor must be modified. Also we need some specific member variables to accomodate the movement of the widget. The modifications to the constructor are as follows:
/** * MoveableButton constructor. * @param in_node The SVG base node for the button widget. */ function MoveableButton( in_node ) { // inheritance this.base = Atom; this.base( in_node.getAttribute( "id" ), Button.CLASS ); // MoveableButton members this.anchor = null; // handle for the root g element this.bMoving = false; // boolean flag this.pDragPoint= null; // SVGPoint that will be used to calc the position for( var i = 0; i < in_node.childNodes.length; i++ ) { var node = in_node.childNodes.item(i); if( node.nodeType != 3 ) { if( node.nodeName == "desc" && this.anchor == null ) { this.anchor = node; } } } // to handle the moveable part of the button in_node.addEventListener( "click", this, false ); in_node.addEventListener( "mousedown", this, false ); in_node.addEventListener( "mouseup", this, false ); in_node.addEventListener( "mousemove", this, false ); } |
Figure 13: Modified MoveableButton Constructor
The mouse event handling code must accomplish the tasks listed in Chapter 1 . This is accomplished as follows (see inline documentation):
/** * Handle click events. * @param evt The click event. */ MoveableButton.prototype.click = function( evt ) { // for testing and demo purposes only - remove later alert( "clicked" ); // state was changed (clicked) so notify all this widgets observers this.notifyObservers(); // event was successfully handled evt.stopPropagation(); }; /** * Handle click events. * @param evt The click event. */ MoveableButton.prototype.click = function( evt ) { alert( "clicked" ); // for testing and demo purposes only - remove later this.notifyObservers(); // state was changed (clicked) // event was successfully handled evt.stopPropagation(); }; /** * Handle mousedown events. * @param evt The mousedown event. */ MoveableButton.prototype.mousedown = function( evt ) { if( !this.bMoving ) { this.bMoving = true; // now moving SPARK.MOUSE_FOCUS = this; // request global mouse focus var SVGRoot = this.anchor.ownerDocument.documentElement; this.pDragPoint = SVGRoot.createSVGPoint(); var point = getNodeCoordinates( this.anchor.parentNode, evt.clientX, evt.clientY ); // save the point where the draggin started this.pDragPoint.x = point.x; this.pDragPoint.y = point.y; // event was successfully handled evt.stopPropagation(); } }; /** * Handle mousemove events. * @param evt The mousemove event. */ MoveableButton.prototype.mousemove = function( evt ) { if( this.bMoving ) { // if the button is in the "moving" state // simply adjust the x and y positions of the // skeleton svg rect element. It is assumed that // the skeleton drag rect has an id the same as // the widget with "-drag" added at the end. var point = getNodeCoordinates( this.anchor.parentNode, evt.clientX, evt.clientY ); var SVGDoc = evt.target.ownerDocument; var node = SVGDoc.getElementById( this.ID + "-drag" ); var transformX = point.x - this.pDragPoint.x; var transformY = point.y - this.pDragPoint.y; node.setAttribute( "transform", "translate(" + transformX + "," + transformY + ")" ); // event was successfully handled evt.stopPropagation(); } }; /** * Handle mouseup events. * @param evt The mouseup event. */ MoveableButton.prototype.mouseup = function( evt ) { if( this.bMoving ) { var point = getNodeCoordinates( this.anchor.parentNode, evt.clientX, evt.clientY ); // calculate the differences in the new position versus the old position. var deltaX = point.x - this.pDragPoint.x; var deltaY = point.y - this.pDragPoint.y; // change the translate function on the g element to adjust the location // of the widget var attr = this.anchor.parentNode.getAttribute( "transform" ).split( "," ); var transformX = parseInt( attr[0].substring( 10, attr[0].length )) + deltaX; var transformY = parseInt(attr[1].substring( 0, attr[1].length-1 )) + deltaY; // modify the groups transform with the new position this.anchor.parentNode.setAttribute( "transform", "translate(" + transformX + "," + transformY + ")" ); // reset the drag rectangle var SVGDoc = evt.target.ownerDocument; var node = SVGDoc.getElementById( this.ID + "-drag" ); node.setAttribute( "transform", "translate(0,0)" ); // release mouse focus, no longer moving SPARK.MOUSE_FOCUS = null; this.bMoving = false; this.pDragPoint = null; // event was successfully handled evt.stopPropagation(); } }; |
Figure 14: MoveableButton Event Handlers
In order for our widget to be used there are a number of tasks that must first be accomplished. The SPARKFactory class must be modified to support the widget and the App class must be created to initialize our application. Finally the SPARKDecorator class may be modified (if necessary) to add application specific functionality to the widgets.
The first task is to modify the SPARKFactory class. This class is used to let the application recognize and distinguish SVG nodes that define SPARK widgets. It is also used to instantiate the widgets as they are discovered. There are two places in the SPARKFactory class that must be modified: the init and the createAtom functions. The modifications will look as follows (assuming this is the only widget type that is currently being used):
/** * Registers widget types with the SPARK class. * * NOTE: Must be modified by UI programmer to support required * widget types. Add the following for each widget type you wish * to use: * * SPARK.registerType( WidgetClass.REGEX ); */ SPARKFactory.init = function() { // added to support the MoveableButton class SPARK.registerWidgetType( MoveableButton.REGEX ); }; |
Figure 15: SPARKFactory init Function Modifications
/** * Create a new atom widget from the given SVG element. * * NOTE: Must be modified by UI programmer to support required * widget types. Add the following for each widget type you wish * to use (in pseudo code): * * if( in_node/@class == WidgetClass.REGEX ) * { * widget = new WidgetClass(); * } * * @param in_node The SVG element. * @return The new atom or null. */ SPARKFactory.createAtom = function( in_node ) { var widget = null; var sClass = in_node.getAttribute( "class" ); // added to support the MoveableButton class. if( sClass.match( MoveableButton.REGEX ) ) { widget = new MoveableButton( in_node ); } return( widget ); }; |
Figure 16: SPARKFactory createAtom Function Modifications
The SVG document has been decided to look as follows:
<svg xmlns="http://www.w3.org/2000/svg" width="100%" height="100%" onload="App.init(evt);"> <title>MoveableButton Demonstration Application</title> <!-- script includes here --> <g id="application"> <g id="SPARK" onmousemove="SPARK.handleMouseEvent(evt);" onmouseup="SPARK.handleMouseEvent(evt);" onmousedown="SPARK.handleMouseEvent(evt);" onclick="SPARK.handleMouseEvent(evt);" onkeypress="SPARK.handleKeyboardEvent(evt);" onkeydown="SPARK.handleKeyboardEvent(evt);" onkeyup="SPARK.handleKeyboardEvent(evt);"> <g id="eventCatcherLayer"> <rect height="300%" width="300%" y="-100%" x="-100%" fill="white" opacity="0"/> </g> <!-- widget SVG here --> </g> </g> </svg> |
The applications (demonstrations) SVG has some features that must be explained. First the App.init function is called when the SVG is first loaded. Therefore in the App.init function we must take the opportunity to initalized our base widgets. This may be done however the developer wishes but for this demonstration we will place all widgets inside the group element with the id SPARK.
The SPARK group has been created to hold our application widgets. It is the location of our user interface. This of course is not part of the SPARK framework and may be decided however the developer wishes. For this demonstration it has been used for simplicity sake. The SPARK group has a number of event handlers. These are used, in combination with the eventCatcherLayer to catch all client events (that are not caught and processed) and direct them towards the global SPARK class to handle and distribute the event to the subscribing widget as appropriate. Once again these are not part of the framework - the are simply a useful tool that has been used in previous applications.
The last step with the SVG is to include the script files and insert our widgets. The positions for both the Widgets and the source includes have been listed with comments. We will be inserting the appropriate SVG to these two locations to make our simple demonstration application. The completed SVG looks as follows (note an extra moveablebutton widget has been added for the demonstration):
<svg xmlns="http://www.w3.org/2000/svg" width="100%" height="100%" onload="App.init(evt);"> <title>MoveableButton Demonstration Application</title> <script xlink:href="spark.js"/> <script xlink:href="atom.js"/> <script xlink:href="container.js"/> <script xlink:href="moveablebutton.js"/> <script xlink:href="sparkfactory.js"/> <script xlink:href="sparkdecorator.js"/> <script xlink:href="app.js"/> <g id="application"> <g id="SPARK" onmousemove="SPARK.handleMouseEvent(evt);" onmouseup="SPARK.handleMouseEvent(evt);" onmousedown="SPARK.handleMouseEvent(evt);" onclick="SPARK.handleMouseEvent(evt);" onkeypress="SPARK.handleKeyboardEvent(evt);" onkeydown="SPARK.handleKeyboardEvent(evt);" onkeyup="SPARK.handleKeyboardEvent(evt);"> <g id="eventCatcherLayer"> <rect height="300%" width="300%" y="-100%" x="-100%" fill="white" opacity="0"/> </g> <g id="mb-1" class="SPARK atom MoveableButton" transform="translate(100,50)"> <desc> This is a moveable button. It can be clicked and it can also be moved. To move simply mousedown and drag the button to the desired location. </desc> <rect stroke="black" fill="blue" width="100" height="50"/> <text x="25" y="25" stroke="white">Click me!</text> <rect id="mb-1-drag" stroke="black" fill="none" width="100" height="50"/> <rect stroke="black" fill="white" opacity="0" width="100" height="50"/> </g> <g id="mb-2" class="SPARK atom MoveableButton" transform="translate(100,50)"> <desc> This is a moveable button. It can be clicked and it can also be moved. To move simply mousedown and drag the button to the desired location. </desc> <rect stroke="black" fill="red" width="100" height="50"/> <text x="25" y="25" stroke="white">Click me!</text> <rect id="mb-2-drag" stroke="black" fill="none" width="100" height="50"/> <rect stroke="black" fill="white" opacity="0" width="100" height="50"/> </g> </g> </g> </svg> |
Figure 17: Complete Demonstration SVG
The application code is fairly simple to create. First the SPARKFactory is initalized so that we may use it to create widgets. This init function was modified earlier to add support for the different widget types that will be used. Also the SPARKDecorator and the SPARKFactory.setDecorator call will be discussed Section 4.3 . Next the room of the application is selected. From this root we loop through all the child SVG elements and pass them to the factory to attempt to create a widget with them. The App code looks as follows:
/** * Loads the application only. */ /** * public class App */ function App(){}; /** * This is the root svg element of the application. */ App.ROOT = null; /** * Initialize the application. * @param evt The onload event. */ App.init = function( evt ) { // initialize the factory. SPARKFactory.init(); // set the decorator SPARKFactory.setDecorator( new SPARKDecorator() ); var SVGDoc = evt.target.ownerDocument; App.ROOT = SVGDoc.getElementById( "SPARK" ); for( var i = 0; i < App.ROOT.childNodes.length; i++ ) { var node = App.ROOT.childNodes.item(i); if( node.nodeType != 3 ) { var widget = SPARKFactory.createWidget( node ); } } }; |
Figure 18: Application ECMAScript Code
In an application there will be certain widgets that require custom code. To accomodate this the SPARK framework as the SPARKDecorator class. For our demonstration we will be adding a very simple application specific change to one of our two widgets. This is meant only to show the possibilities for using the SPARKDecorator class.
The change the will be added on the initalization of the application. The desired functionality is to change it such that when the moveablebutton with the id "mb-1" is clicked, it will be returned to the origin. The modifications to the SPARKDecorator class are see below:
/** * @file SPARKDecorator.es * * Used to "decorate" widgets (as they are crated) * with custom logic that does not belong in every instance * of that widget. * * NOTE: Will require modifications by application programmer. * * @see org.SchemaSoft.dom.SPARK.SPARK; * @see org.SchemaSoft.dom.SPARK.SPARKFactory; * * @author Alastair Fettes */ /** * public class SPARKDecorator implements Decorator */ function SPARKDecorator(){} /** * Add custom logic to the input widget. * * NOTE: Must be modified by application programmer with the app * specific logic. * * @param in_widget An Atom or Container widget. */ SPARKDecorator.prototype.decorate = function( in_widget ) { if( in_widget.ID == "mb-1" ) { // whenever the button with id mb-1 is clicked, we will move it back to // the origin in_widget.click = function( evt ) { alert( "Resetting position..." ); this.anchor.parentNode.setAttribute( "transform", "translate(0,0)" ); this.notifyObservers(); evt.stopPropagation(); }; } }; |
Figure 19: SPARKDecorator Class
Now that the widget has been created it may be skinned to appear however one wishes. The only constraints are that it must have the necessary SPARK framework defined SVG (see Chapter 2 ) plus some sort of SVG element or group the has an id that follows the convention uniqueid-drag. This is a simple demonstration of the skinning abilities of the SPARK framework.
<g id="mb-2" class="SPARK atom MoveableButton" transform="translate(300,50)"> <desc>This is a moveable button. It can be clicked and it can also be moved. To move simply mousedown and drag the button to the desired location. </desc> <ellipse cx="50" cy="25" rx="50" ry="25" stroke="black" fill="red"> <animate attributeName="rx" from="50" to="56" dur="0.2s" begin="mb-2.mouseover" fill="freeze"/> <animate attributeName="ry" from="25" to="31" dur="0.2s" begin="mb-2.mouseover" fill="freeze"/> <animate attributeName="rx" from="56" to="50" dur="0.2s" begin="mb-2.mouseout" fill="freeze"/> <animate attributeName="ry" from="31" to="25" dur="0.2s" begin="mb-2.mouseout" fill="freeze"/> </ellipse> <text x="25" y="25" stroke="white">Click me!</text> <g id="mb-2-drag" transform=""> <ellipse cx="50" cy="25" rx="50" ry="25" stroke="black" fill="none"> <animate attributeName="rx" from="50" to="56" dur="0.2s" begin="mb-2.mouseover" fill="freeze"/> <animate attributeName="ry" from="25" to="31" dur="0.2s" begin="mb-2.mouseover" fill="freeze"/> <animate attributeName="rx" from="56" to="50" dur="0.2s" begin="mb-2.mouseout" fill="freeze"/> <animate attributeName="ry" from="31" to="25" dur="0.2s" begin="mb-2.mouseout" fill="freeze"/> </ellipse> </g> <ellipse cx="50" cy="25" rx="50" ry="25" stroke="black" fill="white" opacity="0" /> </g> |
Figure 20: MoveableButton Widget With Alternate Skin
The final product can be found here: /resources/samples/faq/createatom/moveablebutton.svg.
XHTML rendition created by gcapaper Web Publisher v2.1, © 2001-3 Schema Software Inc.