DekGenius.com
[ Team LiB ] Previous Section Next Section

22.2 Building the Components

The four elements described in the previous section are all good candidates for components. In each case, the elements are discrete constituents that can be abstracted. For example, the toolbar is composed of multiple buttons that are responsible for different tasks. However, each of the buttons shares basic, core functionality. Likewise, each of the shape units that are drawn by the user may look different—different outline shapes, sizes, and fill colors—but the basic functionality of all shapes is the same. And it is the same with the text units as well. While there is only one color selector instance in the application, it is nonetheless a good candidate for a component because it is a distinct unit that can likely be reused in another application.

22.2.1 Creating the Superclass for the Components

The toolbar buttons, shape units, and text units all share some common functionality. Each of these three types of components needs to have the capability to define callback methods, including callback methods for when the component is selected, deselected, pressed, and released. While it is certainly possible to define the same methods individually in each of the component classes, you can also define a single superclass from which the component classes can inherit. This is advantageous because you can define the methods in one location.

Complete the following steps to define the custom PaintBase class from which three of the components inherit their methods:

  1. In your flashPaint.fla document, create a new movie clip symbol named PaintBaseClass.

  2. On the first frame of the default layer of the PaintBaseClass symbol, add the following code:

    // Enclose the code using #initclip so that it executes before the rest of the
    // code in the movie.
    #initclip 0
    
    function PaintBase (  ) {}
    
    // PaintBase needs to inherit from MovieClip because the classes that extend
    // PaintBase are all component classes.
    PaintBase.prototype = new MovieClip(  );
    
    // The following four methods define callback functions for instances of the
    // class. Each method requires at least one parameter: the name of the callback
    // function as a string. Also, each accepts a second, optional parameter
    // indicating the path to the function. If no path is supplied, the timeline on
    // which the component instance exists is used.
    PaintBase.prototype.setOnPress = function (functionName, path) {
      if (path == undefined) {
        path = this._parent;
      }
      this.onPressCB = functionName;
      this.onPressPath = path;
    };
    
    PaintBase.prototype.setOnRelease = function (functionName, path) {
      if (path == undefined) {
        path = this._parent;
      }
      this.onReleaseCB = functionName;
      this.onReleasePath = path;
    };
    
    PaintBase.prototype.setOnSelect = function (functionName, path) {
      if (path == undefined) {
        path = this._parent;
      }
      this.onSelectCB = functionName;
      this.onSelectPath = path;
    };
    
    PaintBase.prototype.setOnDeselect = function (functionName, path) {
      if (path == undefined) {
        path = this._parent;
      }
      this.onDeselectCB = functionName;
      this.onDeselectPath = path;
    };
    
    // Several of the components should automatically call the onPress(  ) callback
    // function when a press event occurs.
    PaintBase.prototype.onPress = function (  ) {
      this.onPressPath[this.onPressCB](this);
    };
    
    // Likewise, several of the components should automatically call the onRelease(  ) 
    // and onSelect(  ) callback methods when the release event occurs. Additionally,
    // a selected property is set to true so that the component can keep track of its
    // current state.
    PaintBase.prototype.onRelease = function (  ) {
      this.onReleasePath[this.onReleaseCB](this);
      this.selected = true;
      this.onSelectPath[this.onSelectCB](this);
    };
    
    // Several of the components include a deselect(  ) method that calls the
    // onDeselect(  ) callback function.
    PaintBase.prototype.deselect = function (  ) {
      this.onDeselectPath[this.onDeselectCB](this);
    };
    
    #endinitclip

Congratulations! You have just created a superclass that defines some common functionality for any class that extends it. Now, perhaps a little explanation is due.

A class such as PaintBase is sometimes referred to as an abstract class because it contains abstract functionality that can be used by any class that extends it. By itself, PaintBase doesn't do very much. But it does save you from having to define the same methods in multiple classes later on. Instead, each of the component classes inherits from PaintBase in the following way:

ComponentClassName.prototype = new PaintBase(  );

When you create a class that inherits from PaintBase, all the methods of PaintBase are available from instances of that class. For example, if myObj is an instance of MyComponentClass, and MyComponentClass inherits from PaintBase, then you can call any of the PaintBase methods from myObj:

// Define an onRelease(  ) callback function for myObj. Now, 
// whenever myObj is clicked and released the function
// myOnReleaseCallback(  ) defined on _root is invoked automatically.
myObj.setOnRelease("myOnReleaseCallback", _root);

Now, let's look a little more closely at each of the methods of the PaintBase class.

The first line of code is an #initclip directive. In this case, you should follow the directive with the value 0, which instructs the Flash movie to execute all the code contained between the #initclip and #endinitclip directives before anything else, even before other code within #initclip directives. Flash processes all #initclip code before any code on the frame in which the component first exists. If the component is an exported symbol, the #initclip code is processed before the first frame of the main timeline. However, in this case you need to ensure that PaintBase is defined before any of the component classes are defined. You can specify the precedence for processing the #initclip code using the optional order parameter. Code with lower order parameters is processed first, and code with order parameters is processed before code without order parameters. Therefore, we use:

#initclip 0

The PaintBase class is an abstract class for component classes. All components must extend MovieClip directly or indirectly. Therefore, PaintBase must extend MovieClip (which is not to say that all abstract classes must extend MovieClip):

PaintBase.prototype = new MovieClip(  );

The setOnPress( ), setOnRelease( ), setOnSelect( ), and setOnDeselect( ) methods all work in the same manner. The concept is to closely mimic the functionality of the callback-setting methods of many of the predefined ActionScript classes and the UI components. Each of these four methods, therefore, accepts a string specifying the name of the callback function. Additionally, you can specify a path to the function. If no path is specified (that is, if the path parameter is undefined), the value of the timeline on which the component instance resides (given by this._parent) is used. In each case, the function name and path to the function are stored in unique properties, such as onPressCB and onPressPath:

PaintBase.prototype.setOnPress = function (functionName, path) {
  if (path == undefined) {
    path = this._parent;
  }
  this.onPressCB = functionName;
  this.onPressPath = path;
};

There is a very good reason to use callback functions instead of allowing each component to define onPress( ) and onRelease( ) methods directly. If onPress( ) or onRelease( ) methods were to be defined for the component instances, the prototype methods would be wiped out. Working with callback functions allows you to define actions to occur on these events for the instances while not overwriting the functionality defined for the prototype. The PaintBase class's onPress( ) and onRelease( ) methods demonstrate how the callback function is invoked without wiping out any functionality that is already defined within the prototype. ActionScript treats functions as properties (Function datatypes) of the timeline in which they are defined. You can use this fact to invoke the callback functions using array-access notation. In each case, the callback function is passed a reference to the component doing the callback. This is both convenient and in keeping with the conventions of callback functions as they are used throughout ActionScript.

PaintBase.prototype.onPress = function (  ) {
  this.onPressPath[this.onPressCB](this);
};

PaintBase.prototype.onRelease = function (  ) {
  this.onReleasePath[this.onReleaseCB](this);
  this.selected = true;
  this.onSelectPath[this.onSelectCB](this);
};

22.2.2 Creating the Toolbar Button Component

The toolbar comprises eight tools/buttons:

Select

Allows existing shape and text items to be selected and moved.

Line

Allows the user to draw a line.

Rectangle

Allows the user to draw a rectangular outline.

Ellipse

Allows the user to draw an elliptical outline.

Text

Lets the user add text.

Fill

Adds a fill to any shape that is clicked or applies a new color to any text that is clicked. The current color from the color selector is used in both cases.

Back

Moves the selected shape or text back by one depth.

Forward

Moves the selected shape or text forward by one depth.

Each of the toolbar buttons shares common functionality with every other toolbar button, namely:

  • The buttons are drawn using the Drawing API. Each toolbar button is composed of the same basic button that is labeled with a symbol or text (a line on the line tool, "abc" on the text tool, etc.).

  • Each button has a selected and deselected state. When the button is selected, it appears to be pressed in. When the button is deselected, it appears to be raised.

Additionally, there are two kinds of toolbar buttons. The first six buttons are called "stick" buttons because when they are pressed, they remain selected until otherwise deselected. The remaining two buttons (the back and forward buttons) are called "spring" buttons because they spring back after they have been clicked and released.

Perform the following steps to construct the ToolbarButton component:

  1. Create a new movie clip symbol named ToolbarButton.

  2. Edit the linkage properties of the symbol.

  3. Select the Export for ActionScript and Export in First Frame checkboxes.

  4. Set the linkage identifier to ToolbarButtonSymbol.

  5. Click OK.

  6. Edit the new symbol.

  7. Rename the default layer as superclass and add a new layer named toolbarButtonClass.

  8. On the superclass layer, drag an instance of the PaintBaseClass symbol. This ensures that the superclass is defined so that ToolbarButtonClass can extend it.

  9. On the toolbarButtonClass layer, add the following code to the first frame:

    #initclip
    
    // Initialize the component instance to its unselected state.
    function ToolbarButtonClass(  ) {
      this.selected = false;
    }
    
    // This class extends PaintBase. Because PaintBase extends MovieClip, 
    // ToolbarButtonClass is still a valid component class.
    ToolbarButtonClass.prototype = new PaintBase(  );
    
    // The create(  ) method draws the button. The method requires a name that
    // indicates what kind of symbol to draw (line, rectangle, etc.). It also
    // requires a type parameter specifying if the button is a "stick" or "spring"
    // button type.
    ToolbarButtonClass.prototype.create = function (name, type) {
    
      // Set the type as a property. This is used later to determine how the button
      // responds to presses and releases.
      this.type = type;
    
      // Create a movie clip in which the button is drawn. Within this movie clip are
      // three nested movie clips: one for the highlight, one for the button's
      // center, and one for the shadow.
      this.createEmptyMovieClip("btn", this.getNewDepth(  ));
      this.btn.createEmptyMovieClip("btnHighlight", this.btn.getNewDepth(  ));
      this.btn.createEmptyMovieClip("btnShadow", this.btn.getNewDepth(  ));
      this.btn.createEmptyMovieClip("btnCenter", this.btn.getNewDepth(  ));
    
      // The width and height value of 42 and 21 are hardcoded for this application.
      // You could also choose to make this more abstract.
      var w = 42;
      var h = 21;
    
      // Draw the button highlight, center, and shadow.
      with (this.btn.btnHighlight) {
        lineStyle(0, 0x000000, 0);
        beginFill(0xECECEC, 100);
        drawRectangle(w + 2, h + 2);
        endFill(  );
      }
      with (this.btn.btnShadow) {
        lineStyle(0, 0x000000, 0);
        beginFill(0, 100);
        drawRectangle(w + 2, h + 2);
        endFill(  );
        _x += 1;
        _y += 1;
      }
      with (this.btn.btnCenter) {
        lineStyle(0, 0x000000, 0);
        beginFill(0xDFDFDF, 100);
        drawRectangle(w, h);
        endFill(  );
      }
    
      // Create the movie clip into which the button's symbol or label is added.
      this.createEmptyMovieClip("symbol", this.getNewDepth(  ));
    
      // Add the appropriate symbol based on the name value. If "select", draw an
      // arrow. If "line", draw a line. If "rectangle", draw a rectangle. If
      // "ellipse", draw an ellipse. If "text", label the button "abc". If "fill",
      // "back", or "forward", label the button with "fill", "back", or "forward".
      switch (name) {
        case "select":
          with (this.symbol) {
            lineStyle(0, 0x000000, 100);
            beginFill(0, 100);
            drawRectangle(w/4, h/9);
            drawTriangle(2 * h/3, 2 * h/3, 60, 30, -w/4);
            endFill(  );
            _rotation += 30;
            _x += w/8;
            _y += h/8;
          }
          break;
        case "line":
          with (this.symbol) {
            lineStyle(1, 0x000000, 100);
            moveTo(-(w/2) + 6, -(h/2) + 6);
            lineTo((w/2) - 6, (h/2) - 6);
          }
          break;
        case "rectangle":
          with (this.symbol) {
            lineStyle(0, 0x000000, 100);
            drawRectangle(w - 12, h - 6);
          }
          break;
        case "ellipse":
          with (this.symbol) {
            lineStyle(0, 0x000000, 100);
            drawEllipse((w - 12)/2, (h - 6)/2);
          }
          break;
        case "text":
          this.symbol.createTextField("label", this.symbol.getNewDepth(  ),
                 -w/2, -h/2, w, h);
          this.symbol.label.text = "abc";
          var tf = new TextFormat(  );
          tf.align = "center";
          this.symbol.label.setTextFormat(tf);
          break;
        case "fill":
          this.symbol.createTextField("label", this.symbol.getNewDepth(  ),
                 -w/2, -h/2, w, h);
          this.symbol.label.text = "fill";
          var tf = new TextFormat(  );
          tf.align = "center";
          this.symbol.label.setTextFormat(tf);
          break;
        case "back":
          this.symbol.createTextField("label", this.symbol.getNewDepth(  ),
                 -w/2, -h/2, w, h);
          this.symbol.label.text = "back";
          var tf = new TextFormat(  );
          tf.align = "center";
          this.symbol.label.setTextFormat(tf);
          break;
        case "forward":
          this.symbol.createTextField("label", this.symbol.getNewDepth(  ),
                 -w/2, -h/2, w, h);
          this.symbol.label.text = "forward";
          var tf = new TextFormat(  );
          tf.align = "center";
          this.symbol.label.setTextFormat(tf);
      }
    
      // The drawing methods draw the buttons with the registration point at the
      // center of the shapes. To move the shapes so that the registration point of
      // the component is at the upper-left corner, adjust everything down and to the
      // right by half.
      var shiftx = this._height / 2;
      var shifty = this._width / 2;
      this.btn._y += shiftx;
      this.btn._x += shifty;
      this.symbol._y += shiftx;
      this.symbol._x += shifty;
    };
    
    // The select(  ) method adjusts the elements of the button to give the effect of
    // the button being pushed in.
    ToolbarButtonClass.prototype.select = function (  ) {
      this.btn._width += 1;
      this.btn._height += 1;
      this.symbol._width -= 1;
      this.symbol._height -= 1;
      this._x += 1;
      this._y += 1;
      this.btn.btnHighlight._visible = false;
      this.btn.btnShadow._visible = false;
    };
    
    // The deselect(  ) method reverses the effects of the select(  ) method.
    ToolbarButtonClass.prototype.deselect = function (  ) {
      this.btn._width -= 1;
      this.btn._height -= 1;
      this.symbol._width += 1;
      this.symbol._height += 1;
      this._x -= 1;
      this._y -= 1;
      this.btn.btnHighlight._visible = true;
      this.btn.btnShadow._visible = true;
    };
    
    // The toggleDeslect(  ) method deselects the button if it is selected.
    ToolbarButtonClass.prototype.toggleDeselect = function (  ) {
      if (this.selected) {
        this.deSelect(  );
        this.selected = !this.selected;
      }
    };
    
    // When the button is pressed, if it is not already selected, call the select(  ) 
    // method and the onSelect(  ) callback function.
    ToolbarButtonClass.prototype.onPress = function (  ) {
      if (!this.selected) {
        this.select(  );
        this.onSelectPath[this.onSelectCB](this);
      }
    };
    
    // When the button is released, call the deselect(  ) method if the 
    // button is already selected or if it is a "spring" button. Whenever 
    // a "spring" button is released, it automatically springs back to the 
    // original position. "Stick" buttons are deselected only if they had 
    // previously been selected. Additionally, toggle the selected status of 
    // the button if it is a "stick" button.
    ToolbarButtonClass.prototype.onRelease = function (  ) {
      if (this.selected || this.type == "spring") {
        this.deselect(  );
      }
      if (this.type == "stick") {
        this.selected = !this.selected;
      }
    };
    
    // You should register the component class to the name that you gave to the
    // component symbol.
    Object.registerClass("ToolbarButtonSymbol", ToolbarButtonClass);
    
    #endinitclip

The ToolbarButtonClass class is not an overly complex class, yet there are still some parts of it that warrant further examination. Let's take a closer look at parts of the class.

The create( ) method is a long method, but it doesn't need to be intimidating. Most of the code in this method is comprised of drawing methods. First, you create a movie clip for the basic button portion of the component. This movie clip contains three nested movie clips for the button's highlight, center, and shadow. Generally, it is a good idea to create different movie clips for any parts that you draw using the drawing API. Additionally, you should group together any related movie clips into a parent clip, as is done here:

this.createEmptyMovieClip("btn", this.getNewDepth(  ));
this.btn.createEmptyMovieClip("btnHighlight", this.btn.getNewDepth(  ));
this.btn.createEmptyMovieClip("btnShadow", this.btn.getNewDepth(  ));
this.btn.createEmptyMovieClip("btnCenter", this.btn.getNewDepth(  ));

Next, the highlight, shadow, and center of the button are drawn using the drawRectangle( ) method from Recipe 4.4. The center of the button is a gray rectangle. This movie clip has a greater depth than the other two, and so it appears above them. The other two movie clips are also rectangles. Each is drawn slightly larger than the button's center, and the shadow is offset by one pixel in the x and y directions. The size, positioning, and depths of the three movie clips give the illusion of three dimensions to the button.

with (this.btn.btnHighlight) {
  lineStyle(0, 0x000000, 0);
  beginFill(0xECECEC, 100);
  drawRectangle(w + 2, h + 2);
  endFill(  );
}
with (this.btn.btnShadow) {
  lineStyle(0, 0x000000, 0);
  beginFill(0, 100);
  drawRectangle(w + 2, h + 2);
  endFill(  );
  _x += 1;
  _y += 1;
}
with (this.btn.btnCenter) {
  lineStyle(0, 0x000000, 0);
  beginFill(0xDFDFDF, 100);
  drawRectangle(w, h);
  endFill(  );
}

You create the symbol movie clip above the basic button clip and add the appropriate symbol or label to it. You use a switch statement to determine which symbol or label to add, depending on the value of the name parameter that is passed to the create( ) method. If the value is "select", you should draw an arrow (for the selection tool). You can draw an arrow by placing a rectangle and a triangle end to end. Then, in this example, the entire symbol movie clip is rotated 30 degrees so that the arrow is angled.

case "select":
  with (this.symbol) {
    lineStyle(0, 0x000000, 100);
    beginFill(0, 100);
    drawRectangle(w/4, h/9);
    drawTriangle(2 * h/3, 2 * h/3, 60, 30, -w/4);
    endFill(  );
    _rotation += 30;
    _x += w/8;
    _y += h/8;
  }
  break;

You can draw a line, rectangle, or ellipse using the basic drawing techniques covered in Chapter 4.

When the value of name is "text", "fill", "back", or "forward", you should add a text field to the symbol movie clip instead of drawing anything in it. If you create the text field with the width and height of the button, you can format the text so that it is aligned to the center of the button.

case "text":
  this.symbol.createTextField("label", this.symbol.getNewDepth(  ), 
         -w/2, -h/2, w, h);
  this.symbol.label.text = "abc";
  var tf = new TextFormat(  );
  tf.align = "center";
  this.symbol.label.setTextFormat(tf);
  break;

Finally, because the custom drawing methods such as drawRectangle( ) and drawEllipse( ) draw shapes with the registration point at the center, you should shift everything in the toolbar button down and to the right by half the height and half the width such that the component's registration point is in the upper-left corner:

var shiftx = this._height / 2;
var shifty = this._width / 2;
this.btn._y += shiftx;
this.btn._x += shifty;
this.symbol._y += shiftx;
this.symbol._x += shifty;

The select( ) and deselect( ) methods do the reverse of one another. The select( ) method gives the appearance of the button being in a pressed state. You accomplish this by increasing the dimensions of the basic button movie clip while hiding the highlight and shadow. Additionally, the width and height of the symbol are decreased (since we want to give the appearance of it being slightly further away). Then, when deselect( ) is called, these actions are all reversed.

ToolbarButtonClass.prototype.select = function (  ) {
  this.btn._width += 1;
  this.btn._height += 1;
  this.symbol._width -= 1;
  this.symbol._height -= 1;
  this._x += 1;
  this._y += 1;
  this.btn.btnHighlight._visible = false;
  this.btn.btnShadow._visible = false;
};

ToolbarButtonClass.prototype.deselect = function (  ) {
  this.btn._width -= 1;
  this.btn._height -= 1;
  this.symbol._width += 1;
  this.symbol._height += 1;
  this._x -= 1;
  this._y -= 1;
  this.btn.btnHighlight._visible = true;
  this.btn.btnShadow._visible = true;
};

The onPress( ) method needs to check to make sure the button is not selected. If it is selected, then nothing needs to be done. But if the button is not already selected, the select( ) method needs to be called (to make the button look like it is being pressed), and the onSelect( ) callback method should be called as well:

ToolbarButtonClass.prototype.onPress = function (  ) {
  if (!this.selected) {
    this.select(  );
    this.onSelectPath[this.onSelectCB](this);
  }
};

When the button is released, the type of actions depends on the type of button—"stick" or "spring". A spring button should always appear deselected once it is released. Therefore, if the button is a spring button, call the deselect( ) method. On the other hand, if the button is a stick button, there are more decisions that need to be made. If the button had been previously selected, then the deselect( ) method should be called. Also, spring buttons cannot maintain a selected/deselected state—they are selected only when pressed—but stick buttons maintain a selected/deselected state. Therefore, each time a release event occurs on a stick button, the selected state should be toggled:

ToolbarButtonClass.prototype.onRelease = function (  ) {
  if (this.selected || this.type == "spring") {
    this.deselect(  );
  }
  if (this.type == "stick") {
    this.selected = !this.selected;
  }
};

22.2.3 Creating the Shape Component

The Flash Paint application uses shapes as one of the two kinds of units that can be drawn by the user (the other being text units). Here is a list of some of the basic characteristics of a shape:

  • A shape can be a line, a rectangle, or an ellipse.

  • Shapes can be drawn given a width and a height.

  • Once a shape exists, it can be moved.

  • Shapes can be filled (applies to rectangles and ellipses only).

Complete the following steps to create the shape component:

  1. Create a new movie clip symbol named Shape.

  2. Edit the linkage properties of the symbol.

  3. Select the Export for ActionScript and Export in First Frame checkboxes.

  4. Set the linkage identifier to ShapeSymbol.

  5. Click OK.

  6. Edit the new symbol.

  7. Rename the default layer as superclass and add a new layer named shapeClass.

  8. On the superclass layer, drag an instance of the PaintBaseClass symbol. This ensures that the superclass is defined so that ShapeClass can extend it.

  9. On the shapeClass layer, add the following code to the first frame:

    #initclip
    
    // When the component is first instantiated, add fill_mc and outline_mc movie
    // clips to it. Also, create Color objects to target each movie clip.
    function ShapeClass(  ) {
      this.createEmptyMovieClip("fill_mc", this.getNewDepth(  ));
      this.createEmptyMovieClip("outline_mc", this.getNewDepth(  ));
      this.outline_mc.col = new Color(this.outline_mc);
      this.fill_mc.col = new Color(this.fill_mc);
    }
    
    // ShapeClass extends PaintBase.
    ShapeClass.prototype = new PaintBase(  );
    
    // The create(  ) method creates the shape. The method requires a width and
    // height, plus an RGB value for the outline and the name of the shape to draw.
    ShapeClass.prototype.create = function (w, h, outlineRGB, shape) {
      this.shape = shape;
      this.w = w;
      this.h = h;
      this.outline_mc.rgb = outlineRGB;
    
      // Draw the outline without a fill.
      this.draw(this.outline_mc, false);
    
      // If the shape is not a line, then also add a fill. If no color has been 
      // assigned to the fill, it is made transparent.
      if (this.shape != "line") {
        this.draw(this.fill_mc, true);
      }
    };
    
    // The draw(  ) method draws the shape within the specified movie clip (either
    // outline_mc or fill_mc).
    ShapeClass.prototype.draw = function (mc, doFill) {
    
      // Make sure to clear anything that might already be drawn in the movie clip.
      mc.clear(  );
    
      // If doFill is true, set the outline to be transparent. If the fill color has
      // been assigned, then use that color; otherwise, make the fill transparent as
      // well. If doFill is not true, then set the outline to the color that has been
      // assigned to the outline.
      if (doFill) {
        mc.lineStyle(0, 0x000000, 0);
        if (this.fill_mc.rgb == undefined) {
          mc.beginFill(0, 0);
        } else {
          mc.beginFill(this.fill_mc.rgb, 100);
        }
      } else {
        mc.lineStyle(0, this.outline_mc.rgb, 100);
      }
    
      // If the shape is a line, just draw a line and break out of the method.
      if (this.shape == "line") {
        mc.lineTo(this.w, this.h);
        return;
      }
    
      // The drawEllipse(  ) and drawRectangle(  ) methods don't handle negative 
      // numbers. So if the width and height are negative, use the absolute value.
      drawW = Math.abs(this.w);
      drawH = Math.abs(this.h);
    
      // Draw the appropriate shape. These require the DrawingMethods.as methods
      // defined in Chapter 4.
      switch (this.shape) {
        case "ellipse":
          mc.drawEllipse(drawW/2, drawH/2);
          break;
        case "rectangle":
          mc.drawRectangle(drawW, drawH);
      }
    
      // If the method was drawing a fill, make sure to end the fill.
      if (doFill) {
        mc.endFill(  );
      }
    
      // Offset the shape by half the width and height since the custom drawing
      // methods place the registration point at the center. Also, if the width
      // and/or height were negative, this corrects the offset as necessary.
      mc._x = this.w/2;
      mc._y = this.h/2;
    };
    
    // The toggleShowOutline(  ) method reverses the visibility of the outline.
    ShapeClass.prototype.toggleShowOutline = function (  ) {
      this.outline_mc._visible = !this.outline_mc._visible;
    };
    
    // The doFill(  ) method assigns a value to the RGB color for the 
    // fill movie clips. Then, when the draw(  ) method is called, the 
    // fill is drawn with that color.
    ShapeClass.prototype.doFill = function (rgb) {
      this.fill_mc.rgb = rgb;
      this.draw(this.fill_mc, true);
    };
    
    // When the component is deselected, set the outline color back to the original
    // (the color is highlighted blue when it is selected.) Also, set selected to
    // false so the component knows about its current state, and call the deselect(  ) 
    // method of the superclass, PaintBase.
    ShapeClass.prototype.deselect = function (  ) {
      this.outline_mc.col.setRGB(this.outline_mc.rgb);
      this.selected = false;
      super.deselect(  );
    };
    
    // When the user mouses over the component, set the outline color to blue.
    ShapeClass.prototype.onRollOver = function (  ) {
      this.outline_mc.col.setRGB(0x0000FF);
    };
    
    // When the user mouses out of the component, reset the outline color to the
    // original value if the component instance is not selected.
    ShapeClass.prototype.onRollOut = function (  ) {
      if (!this.selected) {
        this.outline_mc.col.setRGB(this.outline_mc.rgb);
      }
    };
    
    // When the component is pressed, call the onPress(  ) method of the superclass
    // and highlight the outline blue if it is not already.
    ShapeClass.prototype.onPress = function (  ) {
      super.onPress(  );
      if (!this.selected) {
        this.outline_mc.col.setRGB(0x0000FF);
      }
    };
    
    // Register the class to the linkage identifier for the symbol.
    Object.registerClass("ShapeSymbol", ShapeClass);
    
    #endinitclip

Now that you've had a chance to see the ShapeClass component class, let's delve into it a little more closely.

The create( ) method directs the drawing of a new shape. To create the shape, the method requires the width and height of the shape, the color to use for the outline, and the type of shape to draw. The possible values for the shape parameter are "line", "rectangle", and "ellipse". The method then proceeds to call the draw( ) method to draw the outline of the shape. If the shape is not a line, the draw( ) method is also called to draw a fill. At this point, no color has been defined for the fill, so the fill is transparent. This might seem to be a bit pointless, but a shape filled with a transparent fill is much easier to select than a shape that is only an outline.

ShapeClass.prototype.create = function (w, h, outlineRGB, shape) {
  this.shape = shape;
  this.w = w;
  this.h = h;
  this.outline_mc.rgb = outlineRGB;
  this.draw(this.outline_mc, false);
  if (this.shape != "line") {
    this.draw(this.fill_mc, true);
  }
};

The draw( ) method contains the core drawing functionality for the shape component. Before the draw( ) method is invoked, the component should already have at least three properties defined by the call to create( ): shape ("line", "rectangle", or "ellipse"), w (the width), and h (the height). Therefore, the draw( ) method does not require any of these values to be passed to it as parameters. However, the draw( ) method does need to know into which movie clip it should draw (outline_mc or fill_mc) and whether to apply a fill:

ShapeClass.prototype.draw = function (mc, doFill) {
   . . . 
};

The draw( ) method can be invoked multiple times for each shape, so it is important that any existing contents in the movie clip are cleared:

mc.clear(  );

Next, the method needs to determine what line styles and what kind of fill (if any) to apply. If the doFill parameter is true, then the outline should be transparent. If the fill color has been defined, it should be used for the fill. Otherwise, the fill should be transparent. On the other hand, if doFill is not true, the outline should be the color that has been set for the rgb property of the outline movie clip.

if (doFill) {
  mc.lineStyle(0, 0x000000, 0);
  if (this.fill_mc.rgb == undefined) {
    mc.beginFill(0, 0);
  } else {
    mc.beginFill(this.fill_mc.rgb, 100);
  }
} else {
  mc.lineStyle(0, this.outline_mc.rgb, 100);
}

The next part of the draw( ) method uses a little programming trick with a return statement. If the shape is a line, then all you need to do is call lineTo( ) once. Therefore, you can use a return statement immediately following that to end the processing of the method.

if (this.shape == "line") {
  mc.lineTo(this.w, this.h);
  return;
}

Finally, if the shape is an ellipse or rectangle, the appropriate drawing method is called. These methods don't accept negative values, so it is important that you convert the width and height of the shape to their absolute values.

drawW = Math.abs(this.w);
drawH = Math.abs(this.h);
switch (this.shape) {
  case "ellipse":
    mc.drawEllipse(drawW/2, drawH/2);
    break;
  case "rectangle":
    mc.drawRectangle(drawW, drawH);
}

The doFill( ) method accomplishes a fill by defining the rgb property of the fill_mc movie clip and then calling the draw( ) method. Once the rgb property is defined, the draw( ) method draws the shape with that fill color.

ShapeClass.prototype.doFill = function (rgb) {
  this.fill_mc.rgb = rgb;
  this.draw(this.fill_mc, true);
};

22.2.4 Creating the Text Component

Text units are the other kind of unit (in addition to shapes) that users can create using Flash Paint. Text units behave similarly to shape units in many ways. Here is a list of the functionality of the text component:

  • Text units can be drawn given a width and a height.

  • Existing text units can be moved.

  • You can apply a new color to existing text.

Complete the following steps to create the text component:

  1. Create a new movie clip symbol named Text.

  2. Edit the linkage properties of the symbol.

  3. Select the Export for ActionScript and Export in First Frame checkboxes.

  4. Set the linkage identifier to TextSymbol.

  5. Click OK.

  6. Edit the new symbol.

  7. Rename the default layer as superclass and add a new layer named textClass.

  8. On the superclass layer, drag an instance of the PaintBaseClass symbol. This ensures that the superclass is defined so that TextClass can extend it.

  9. On the textClass layer, add the following code to the first frame:

    #initclip
    
    function TextClass (  ) {}
    
    // TextClass extends PaintBase.
    TextClass.prototype = new PaintBase(  );
    
    // The create(  ) method creates the text field within the component instance. The
    // method takes three parameters: the width, height, and color of the text.
    TextClass.prototype.create = function (w, h, rgb) {
    
      // Create the text field. The text field should be a multiline, input text
      // field with a white background and a blue border.
      this.createTextField("input_txt", 1, 0, 0, w, h);
      this.input_txt.type = "input";
      this.input_txt.multiline = true;
      this.input_txt.textColor = rgb;
      this.input_txt.border = true;
      this.input_txt.background = true;
      this.input_txt.borderColor = 0x0000FF;
    
      // When the user adds new text for the first time, set the text to autosize.
      this.input_txt.onChanged = function (  ) {
        this.autoSize = true;
        delete this.onChanged;
      }
    
      // Call the deselect(  ) method of the component whenever focus is lost (meaning
      // the user clicked outside of the text field).
      this.input_txt.onKillFocus = function (  ) {
        if (this.text != "") {
          this._parent.deselect(  );
        }
      }
    
      // Set the focus to the text field.
      Selection.setFocus(this.input_txt);
      this.selected = true;
    
      // Invoke the checkEditing(  ) method at an interval to constantly see if the
      // text is being edited.
      this.checkInterval = setInterval(this, "checkEditing", 100);
    };
    
    // The deselect(  ) method first calls the superclass's deselect(  ) method. It then
    // sets other properties of the component so it can keep track of its state.
    // Additionally, the border and background of the text field are turned off.
    TextClass.prototype.deselect = function (  ) {
      super.deselect(  );
      this.editing = false;
      this.input_txt.background = false;
      this.input_txt.border = false;
      this.selected = false;
    };
    
    // The doFill(  ) method sets the text color.
    TextClass.prototype.doFill = function (rgb) {
      this.input_txt.textColor = rgb;
    };
    
    // When the component instance is moused over, 
    // turn on the border only (not the background).
    TextClass.prototype.onRollOver = function (  ) {
      if (!this.selected) {
        this.input_txt.border = true;
      }
    };
    
    // When the user mouses out of the component instance, if it is not otherwise
    // selected, turn off the border.
    TextClass.prototype.onRollOut = function (  ) {
      if (!this.selected) {
        this.input_txt.border = false;
      }
    };
    
    // When the component instance is pressed, check whether the component has been
    // double-clicked. If so, turn on editing and set the focus to the text field.
    // Otherwise, call the onPress(  ) method of the superclass.
    TextClass.prototype.onPress = function (  ) {
      this.input_txt.border = true;
      this.currentTime = getTimer(  );
      if (this.currentTime - this.previousTime < 500) {
        this.editing = true;
        this.input_txt.background = true;
        super.deselect(  );
        Selection.setFocus(this.input_txt);
        Selection.setSelection(this.input_txt.length, this.input_txt.length);
      } else {
        super.onPress(  );
      }
      this.previousTime = this.currentTime;
    };
    
    // The checkEditing(  ) method continually checks to see if the text field is
    // being edited. If so, and if the focus of the text field has been lost, then
    // reset the focus to the text field.
    TextClass.prototype.checkEditing = function (  ) {
      if (this.editing && !(Selection.getFocus(  ) != String(this.input_txt))) {
        Selection.setFocus(this.input_txt);
        Selection.setSelection(this.input_txt.text.length, 
                               this.input_txt.text.length);
      }
    };
    
    // Call the superclass's onRelease(  ) method if the user is not editing the text.
    TextClass.prototype.onRelease = function (  ) {
      if (!this.editing) {
        super.onRelease(  );
      }
    };
    
    // Register the class to the symbol's linkage identifier.
    Object.registerClass("TextSymbol", TextClass);
    
    #endinitclip

The TextClass component class is not overly complex, but it is also not without its own intricacies that are worth exploring in a little more depth.

First of all, notice that both the ShapeClass and the TextClass classes have create( ) methods. Furthermore, the create( ) method of each class accepts similar parameters in a similar order (although ShapeClass.create( ) accepts one additional parameter). This similarity is intentional and allows for text and shape components to be treated more or less identically by the Flash Paint application.

The TextClass.create( ) method includes several points of interest. First of all, notice that the text field is always created with a depth of 1. The create( ) method can be called multiple times for the same text unit, and creating the text field with the same depth each time ensures that any previous text fields are overwritten:

this.createTextField("input_txt", 1, 0, 0, w, h);

Additionally, the create( ) method employs an interesting technique using the onChanged( ) event handler method for the text field. When the text field is first created, it is designed to have a specified width and height. The width and height are maintained until the user first enters text into the text field. At that point the onChanged( ) method is automatically invoked, and the text field is set to autosize. The onChanged( ) method has done its job at that point, so it can delete itself:

this.input_txt.onChanged = function (  ) {
  this.autoSize = true;
  delete this.onChanged;
};

The onKillFocus( ) event handler method of the text field is also used. This method calls the deselect( ) method of the component instance. The purpose of this is to deselect the unit when the user clicks outside of the text field.

this.input_txt.onKillFocus = function (  ) {
  if (this.text != "") {
    this._parent.deselect(  );
  }
};

When a text unit is deselected, set the self-describing editing and selected properties to false. Additionally, the border and background of the text field should be hidden. The TextClass.descelect( ) method calls the deselect( ) method of the superclass, which ultimately invokes the onDeselect( ) callback function to perform necessary housekeeping, such as recording which tool is selected:

TextClass.prototype.deselect = function (  ) {
  super.deselect(  );
  this.editing = false;
  this.input_txt.background = false;
  this.input_txt.border = false;
  this.selected = false;
};

The TextClass.doFill( ) method shares its name with the ShapeClass.doFill( ) method for the same reasons that the create( ) methods are named the same. The doFill( ) method of the TextClass class assigns the new color to the text field:

TextClass.prototype.doFill = function (rgb) {
  this.input_txt.textColor = rgb;
};

The onPress( ) and checkEditing( ) methods are designed to overcome a specific problem that can occur when you place a text field within a movie clip. Normally, when you use the mouse to click on an input text field, Flash brings focus to that text field, and you can type in it. However, if the text field is nested within a movie clip, and the movie clip handles button events (press, release, etc.), the movie clip's event handling takes precedence over the event handling of the nested text field. Therefore, there is not a convenient way to select a text field when it is nested within a movie clip that handles button events (such as with the TextClass component class). To work around this issue, the onPress( ) method checks for double-clicks versus single-clicks. When the component is clicked, the value returned by getTimer( ) is recorded. Then that time is compared with the value of the previous click. If the difference is less than half a second, it constitutes a double-click. Otherwise, it is a single-click. When a double-click occurs, the Selection.setFocus( ) and Selection.setSelection( ) methods are used to bring focus to the text field and to move the cursor to the end of the existing text. When a single-click occurs, the onPress( ) method of the superclass is invoked to allow the default handling for single-clicks, namely dragging the item on the paint canvas.

TextClass.prototype.onPress = function (  ) {
  this.input_txt.border = true;
  this.currentTime = getTimer(  );
  if (this.currentTime - this.previousTime < 500) {
    this.editing = true;
    this.input_txt.background = true;
    super.deselect(  );
    Selection.setFocus(this.input_txt);
    Selection.setSelection(this.input_txt.length, this.input_txt.length);
  } else {
    super.onPress(  );
  }
  this.previousTime = this.currentTime;
};

You might think that the code in the onPress( ) method should be enough to successfully keep the focus on the text field when it is double-clicked. However, the button events keep causing the text field to lose focus. As a workaround for this, the checkEditing( ) method continually brings focus back to the text field if editing is on. You'll need to refer back to the create( ) method to see where we set the interval on which this method is called. The only drawback to this technique is that it does not allow you to do any kind of editing to the text field other than append text or delete text from the end.

Text.prototype.checkEditing = function (  ) {
  if (this.editing && !(Selection.getFocus(  ) != String(this.input_txt))) {
    Selection.setFocus(this.input_txt);
    Selection.setSelection(this.input_txt.text.length, this.input_txt.text.length);
  }
};

22.2.5 Creating the Color Selector Component

You should use the color selector component that you created in Recipe 12.13. If you followed the complete instructions for that program, then the color selector component should be available from the Components panel. If not, download the code from http://www.person13.com/ascb and install it according to the instructions in Chapter 12.

When you have created or installed the color selector, create a copy of the component symbol in the flashPaint.fla Library. You can do this by dragging an instance of the component from the Components panel onto the Stage. Then delete the instance from the Stage. The symbol remains in the Library.

    [ Team LiB ] Previous Section Next Section