LOOPE - Lingo Object Oriented Programming Environment by Irv Kalb

Previous Chapter

Table of Contents

Next chapter

 

Section 2 - Behaviors

Chapter 9 - Building a button behavior

 

When people talk about user interface they often talk about the look and feel of a program or system. On a smaller level, each of the components of a user interface has its own look and feel. For example, the buttons in a multimedia program will typically have a consistent look and feel to them. The look is an artistic consideration, but the feel of the buttons is fairly standardized. What a button does can be split up into two parts, we'll call them the "feel" and the "action". The "feel" refers to the way the button reacts to the mouse by showing different states of the button on screen. The "action" refers to what happens when the user successfully pushes a button. While different buttons perform different actions, good user interface design tells us that the feel of all the buttons should be the same.

When the mouse is over a button, the button typically changes to a rolled version of the initial graphic. If you click down on a button, you expect to see a down version of the button. If you click down and roll off, you expect to see the initial graphic again. If you click down then let up on the mouse while you are over a button, you expect that the action of the button will be executed. Further, if the button is in an unavailable state, then rolling over or clicking on the button should not have any effect. So although the action of different buttons may be wildly different, the feel is identical. In Director, whenever sprites act in an identical manner, you have a good candidate for a behavior. This chapter will take us through the thought process and coding of a re-usable button behavior that implements this standardized feel.

First we must make a decision about the naming or placement of cast members. For our button behavior, we will need four different cast members: a normal or "up" state, a roll state, a down state, and an unavailable or "gray" state. We could choose a convention of placement in the cast, e.g., normal in one cast position, roll state in the next position, etc. However, for reasons that will become obvious in a little bit, let's instead choose a naming convention. For each set of four states, there will be a "base" name that is shared by all four button states. The normal state of the button will have just the base name (which may be made up of many words strung together like this: ManyWordsStrungTogether). The roll, down and gray states will have the same base name but will also have the words roll, down, or gray as the second word in their name. As an example, imagine that we create a button and call it "GoHome". In this case, the four button graphics would be named "GoHome", "GoHome down", GoHome roll", and "GoHome gray". Here is an example of a cast that shows the four button states of two buttons, a rect button and a tri button:

By using a naming convention like this, we can write code in our button behavior to find the other related graphics in the cast by name, no matter where or in what order they may exist in the cast. Because the different members will be used at different times in the behavior, in the "on beginSprite" method we will calculate all member references and store each into a property variable:

-- Button behavior 1

property
spriteNum
property pmButtonUp -- member of the up button
property pmButtonDown -- member of the down button
property pmButtonRoll -- member of the roll button
property pmButtonGray -- member of the gray button

on beginSprite me
  -- get the "base name"
  baseName =
sprite(spriteNum).member.name.word[1]
  pmButtonUp = member(baseName)
  pmButtonDown = member(baseName & " down"
)
  pmButtonRoll = member(baseName & " roll")
  pmButtonGray = member(basename & " gray")
end

Notice that the beginSprite method will correctly identify the four members needed for this behavior independent of whether the score contained the up member or the gray member. But it is important to know whether the button was available or not at the start. Certainly, as we write the behavior, we need to know whether or not the button is available in order to decide whether or not to change the states of button graphics, and whether or not to perform the action of the button. A simple way to keep track of the availability of the button is to use a property variable. If we find the gray member in the score then we set a new property called pfAvailable to FALSE, otherwise we set it to TRUE.

 

-- Button behavior 2

property
spriteNum
property pmButtonUp -- member of the up button
property pmButtonDown -- member of the down button
property pmButtonRoll -- member of the roll button
property pmButtonGray -- member of the gray button
property pfAvailable -- TRUE if available, FALSE if not


on
beginSprite me
  -- get the "base name"
  baseName =
sprite(spriteNum).member.name.word[1]
  pmButtonUp = member(baseName)
  pmButtonDown = member(baseName & " down")
  pmButtonRoll = member(baseName & "roll")
  pmButtonGray = member(basename & "gray")
  if
sprite(spriteNum).member = pmButtonGray then
    pfAvailable = FALSE
  else
    pfAvailable = TRUE
  end
if
end

Now we need to think about the flow of this button. What are the system messages that we want it to respond to, and what "custom" messages might we want it to respond to? Let's first get into reacting to the mouse. From our previous discussion, we know that Director issues mouseEnter and mouseLeave messages whenever the mouse rolls onto and off a sprite. We will use these methods to show the roll and up state of the button. At this point, we'll introduce three more system messages that Director sends out: mouseDown, mouseUp, and mouseUpOutside. These names describe their functions very well. A behavior gets a mouseDown message when the user clicks the mouse down on a sprite. Director sends a mouseUp message whenever the user lets go of the mouse button while over a sprite. Finally, Director issues a mouseUpOutside whenever the user lets go of the mouse button while outside of a sprite. Let's add these five methods to our behavior to handle these messages. Notice that we start each method with a check to see if the button is available. If it is not available, then we just return without doing anything. Also notice that we are using our property variables to show the appropriate member for the button in response to a system message:

-- Button behavior 3

property spriteNum
property pmButtonUp
property pmButtonDown
property pmButtonRoll
property pmButtonGray
property pfAvailable

on beginSprite me
  baseName =
sprite(spriteNum).member.name.word[1]
  pmButtonUp = member(baseName)
  pmButtonDown = member(baseName & " down")
  pmButtonRoll = member(baseName & " roll")
  pmButtonGray = member(basename & " gray")
  if
sprite(spriteNum).member = pmButtonGray then
    pfAvailable = FALSE
  else
    pfAvailable = TRUE
  end
if
end

-- For each system message, set the member
-- of the sprite to the appropriate member

on mouseEnter me
  if not(pfAvailable) then
    return
  end
if
  sprite(spriteNum).member = pmButtonRoll
end

on mouseDown me
  if not(pfAvailable) then
    return
  end
if
  sprite(spriteNum).member = pmButtonDown
end

on mouseUp me
  if not(pfAvailable) then
    return
  end
if
  sprite(spriteNum).member = pmButtonRoll
  -- OK, we've clicked on the button,
  -- we'll add code to deal with it soon
  -- For now, just beep
  beep
end

on mouseUpOutside me
  if not(pfAvailable) then
    return
  end
if
  sprite(spriteNum).member = pmButtonUp
end

on mouseLeave me
  if not(pfAvailable) then

    return
  end
if
  sprite(spriteNum).member = pmButtonUp
end

If we drop this behavior on a button and run it, when we roll over the button it works fine. If we click down on the button and then let go of the mouse, it properly shows the down then the roll state and beeps. However, there are two bugs. First, if we click down, roll off, then roll back on, we would expect to see the down state - but what we see is the roll state. Second, there is a problem with multiple sprites. Imagine that you attach this behavior to two different button sprites. You click down on one button, roll off, roll over the second button, and then let go. The second button will get a mouseUp message and the program will do the action of the second button. This should not be the case - the action of a button should only be done if the user clicked down and then up on the same button. We need to keep track of whether or not the user did a mouseDown in the current instance of the behavior. If we know that, then we can code around both problems. What we need is yet another property variable to remember if we clicked down on this button. Here is the modified code:

-- Button behavior 4

property spriteNum
property pmButtonUp
property pmButtonDown
property pmButtonRoll
property pmButtonGray
property pfAvailable
property pfTracking -- set TRUE if user did a mouseDown on this sprite, FALSE otherwise

on beginSprite me
  baseName =
sprite(spriteNum).member.name.word[1]
  pmButtonUp = member(baseName)
  pmButtonDown = member(baseName & " down")
  pmButtonRoll = member(baseName & " roll")
  pmButtonGray = member(basename & " gray")
  if
sprite(spriteNum).member = pmButtonGray then
    pfAvailable = FALSE
  else
    pfAvailable = TRUE
  end
if
  pfTracking = FALSE
end


on
mouseDown me
  if not(pfAvailable) then
    return
  end
if
  sprite(spriteNum).member = pmButtonDown
  pfTracking = TRUE
end

on mouseUp me
  if not(pfAvailable) then
    return
  end
if
  -- If the user didn't do a mouse down on this sprite, don't do anything
  if not(pfTracking) then

    return
  end
if

  -- Now, turn off tracking
  pfTracking = FALSE

  sprite(spriteNum).member = pmButtonRoll
  beep
end

on mouseUpOutside me
  if not(pfAvailable) then
    return
  end
if
  pfTracking = FALSE
  sprite(spriteNum).member = pmButtonUp
end

on mouseEnter me
  if not(pfAvailable) then

    return
  end
if

  if the mouseUp then
    sprite(spriteNum).member = pmButtonRoll
    
  else
-- mouse is down
    if pfTracking then
      sprite(spriteNum).member = pmButtonDown
    end
if
  end
if
end

on mouseLeave me
  if not(pfAvailable) then

    return
  end
if
  sprite(spriteNum).member = pmButtonUp
end


Now we have added the property pfTracking which we initialize to FALSE in the beginSprite method. If the user clicks down on the sprite, in the mouseDown method we set this property TRUE. Remember that each instance of the button behavior gets a copy of all property variables. If you click down on button A, roll off, then roll over button B and release, then only the pfTracking associated with button A will be set TRUE. Button B's mouseUp method will be called but because its copy of pfTracking is FALSE, B's action will not be triggered. (The on mouseUpOutside method of button A will also be called and A's pfTracking gets reset to FALSE there.) In the mouseUp method, if we find that the user clicked down and then up on the current sprite, then we turn the tracking property off, show the roll state of the button, and perform the action of the button.

In the mouseEnter method, we added some extra checking. If the mouse is up when we enter the sprite, then we show the roll state of the button. If the mouse is down when entering and if we did a mouseDown on this sprite, then we show the down state of the button.

 

Intrasprite communication

As we said, you want each button to have the same feel, but you want different buttons to do different actions. Now that we have a behavior that handles the feel of the button, we must come up with a way to make this behavior trigger different actions. In the previous version of the button behavior, we had tentatively placed a beep command where we wanted the action to occur. We could easily replace this beep with different actions. For example, on one button, the beep could be replaced by a "go next", or "go previous", or a call to some handler or method. But doing that implies that we would have multiple copies of this button code. The only difference would be what code gets executed for the action in the mouseUp method. So, if we had n actions, we would have n copies of the same code. However, if we needed to make a change in the way our buttons worked, we would have to make the same change n times. Ideally, we want to only have one copy of this button code, but somehow tie it into different actions. That is the object oriented way of thinking about this, and that is just what we'll do.

You already know that in Director you can attach more than one behavior to a single sprite. Such behaviors are referred to as sibling behaviors. The theory here is that we can attach the button behavior to a sprite to handle the feel, and then attach a second behavior to the same sprite to implement the action of the button. But we need a way for these two behaviors to communicate. When the button behavior successfully completes (i.e., the user clicks down then up on the same button), the button behavior needs to send a message to the action behavior.

Introducing "sendSprite". The sendSprite function in Lingo lets you send a message to (call a method of) a behavior attached to a sprite. The syntax is:

sendSprite(<spriteNumber>, #methodName, <optionalParam1, optionalParam2, ...>)

This will be our key to sending a message between the two behaviors, also called intrasprite communication (communication within the same sprite). When the user successfully pushes a button, the button behavior will send out a "generic" message to all behaviors attached to the same sprite. The action behavior will receive this message and do whatever is required. So, what message should this be? Well, the message name can be anything. Because we expect to use this message a great deal, it would be best to choose something simple. I typically use the message #mHit. There is nothing special about the name mHit, I could just as easilyhave chosen #mxyzziptlc or #aButtonWasHitNowDoWhateverYouWant, but #mHit is both easily remembered and descriptive.

To send the message, the button behavior executes a sendSprite sending the message #mHit. But we must know which sprite number to send the message to. It turns out that this is easy. We want to send a message to the current sprite number. That is, to the same sprite number to which the behavior is currently attached. The number of the current sprite is always kept in the property variable spriteNum. So, the message can be sent by the button behavior using the following line:

sendSprite(spriteNum, #mHit)

The full mouseUp method becomes:

on mouseUp me
  if not(pfAvailable) then
    return
  end if
  if not(pfTracking) then
    return
  end if

  pfTracking = FALSE
  sprite(spriteNum).member = pmButtonRoll

  -- Inform sibling behavior(s)
  sendSprite(spriteNum, #mHit)
end

For the receiving end of the mHit message, the action behavior needs to have an "on mHit" method. In that method it can do whatever it needs to. For example, if you have a button that when pushed, will send the movie to a frame called "end", then the code would be:

-- Go End when button is hit

on mHit me
  go
"End"
end

Assume that we have a button sprite with the button behavior on it, and the above behavior also attached. As the user rolls the mouse onto the sprite and clicks down (optionally rolls off and back onto the button) and then clicks up, the different methods of the button behavior are called and executed. The button behavior's methods implement the feel of the button and the user sees the button graphics change. When the user lifts up on the mouse button, the button behavior sends out an mHit message. This message is received by the action behavior, and the movie goes to the frame "End". Now we have achieved our goal. The button behavior is completely re-usable. It handles the feel of a generic button, then sends out a standard message. We can apply this same behavior to any or all buttons. Very importantly, because all instances of the button behavior execute from a single script, any change we make to the single script will affect all buttons. Further, by agreeing on a standard mHit message, all action behaviors are now extremely easy to write. For each different button, we need only to attach a simple script with an mHit method.

The button behavior is the only behavior attached to the sprite that interprets the real Director messages of mouseDown, mouseEnter, mouseLeave, mouseUp, and mouseUpOutside. Therefore you do not have to worry about the order of attaching behaviors to a sprite with the button behavior. Only after the button is successfully pushed, does the button behavior send out the mHit message. Other behaviors attached to the same sprite only have to deal with the single mHit message.


Adding a few more methods

Early in this chapter, we talked about unavailable buttons - ones that are "grayed out". And we even have a property, pfAvailable, to tell us if the button is available or not. However, as yet we have no way of changing the status of a button from available to unavailable or vice versa. We can easily add this functionality by adding a new method:

on mButton_SetAvailability me, fTrueOrFalse
  if pfAvailable = fTrueOrFalse then
    return -- already in this state
  end if

  pfAvailable = fTrueOrFalse -- set to the new state
  if pfAvailable then -- show the appropriate member
    
sprite(spriteNum).member = pmButtonUp
  else
    
sprite(spriteNum).member = pnmButtonGray
  end
if
end mButton_SetAvailability

We can call this method and pass it a TRUE or FALSE, essentially to say "make this button available" or "make this button unavailable". First, the method checks to see if the button is already in the requested state. If so, it just exits. Then it sets the property pfAvailable to the requested state. Finally it shows the appropriate member. This works because earlier we added a check in each method to not do any work if the button is unavailable. The beauty of this approach is that we are making this modification only in the button behavior. If a button is in an unavailable state, then the button behavior will never send out an mHit message and therefore, the action will not be triggered. No change is required in any action behavior.

The only thing we left out is how to send the availability message to the sprite. As we explained earlier, we send a message to a sprite using sendSprite. But there is a problem here. How do we know which sprite to send the message to? Assume that we have a frame with a number of different buttons in various channels. If we want to make some selected buttons available or unavailable, then we must know what channels they are in. The identification of the channel numbers is the topic of another chapter. For now, we can test this out using the message window. If we had a button with the button behavior and some action behavior attached to a sprite in say, channel 3, we could send it a message like this:

sendSprite(3, #mButton_SetAvailability, FALSE)

If the movie is running, after executing that line the button changes to it's gray state and the button would not respond to the Director messages like mouseEnter, mouseDown or mouseUp. If we then went to the message window and issued a:

sendSprite(3, #mButton_SetAvailability, TRUE)

the button changes back to the up state, and starts responding to the standard Director messages again. If you want to know programmatically whether the button is available or not, we can add a simple accessor method like this:

on mButton_GetAvailability me
  return pfAvailable
end

Finally, there are cases where you want to be able to programmatically hit a button - that is, to simulate the user pushing on a button. As an example, imagine you have a frame with one or more fields set up for user entry and a button labeled "Submit". The user enters text into the fields, and when finished, clicks on the Submit button to indicate that they are done. In cases like this, you might also want to filter for a Return or Enter key to trigger the Submit button. To allow for this, we need a new method that can be called to simulate what would happen if the user pushed the button. Here is the code of such a method:

on mButton_Hit me
  if not(pfAvailable) then
    return
  end if

  me.mouseDown()
  updateStage
-- to show the down state
  endTicks = the
ticks + 15
  -- wait for a quarter of a second
  repeat
while the ticks < endTicks
    nothing
  end
repeat
  me.mouseUp()
  sprite(spriteNum).member = pmButtonUp -- show the up state
end

In this method, we wind up calling the already defined mouseDown and mouseUp methods. In the normal case, when the user clicks on a button and the mouseDown method is called, as soon as the next frame event happens the down state of the graphic would show. However, because here we are inside of a single method (mButton_Hit), no frame event can occur. In order to show the down graphic, we must explicitly issue an updateStage. After that, we wait for a quarter of a second. This gives the user a visual cue that the button is being pushed. Finally, we call the mouseUp method and then show the up state of the button. When we call the mouseUp method, it executes a sendSprite with an mHit message and the action of the button takes place.

Here is the final code of our all-singing, all-dancing, re-usable, professional strength, object oriented button behavior:

-- Button behavior

property spriteNum
property pmButtonUp
property pmButtonDown
property pmButtonRoll
property pmButtonGray
property pfAvailable
property pfTracking

on beginSprite me
  baseName =
sprite(spriteNum).member.name.word[1]
  pmButtonUp = member(baseName)
  pmButtonDown = member(baseName & " down")
  pmButtonRoll = member(baseName & " roll")
  pmButtonGray = member(basename & " gray")
  if
sprite(spriteNum).member = pmButtonGray then
    pfAvailable = FALSE
  else
    pfAvailable = TRUE
  end
if
  pfTracking = FALSE
end


on
mouseDown me
  if not(pfAvailable) then
    return
  end
if
  sprite(spriteNum).member = pmButtonDown
  pfTracking = TRUE
end

on mouseUp me
  if not(pfAvailable) then
    return
  end
if
  if not(pfTracking) then

    return
  end
if

  pfTracking = FALSE
  sprite(spriteNum).member = pmButtonRoll

  -- Inform sibling behavior(s)
  sendSprite(spriteNum, #mHit)
end

on mouseUpOutside me
  if not(pfAvailable) then
    return
  end
if
  pfTracking = FALSE
  sprite(spriteNum).member = pmButtonUp
end

on mouseEnter me
  if not(pfAvailable) then

    return
  end
if

  if the mouseUp then
    sprite(spriteNum).member = pmButtonRoll
    
  else
-- mouse is down
    if pfTracking then
      sprite(spriteNum).member = pmButtonDown
    end
if
  end
if
end

on mouseLeave me
  if not(pfAvailable) then

    return
  end
if
  sprite(spriteNum).member = pmButtonUp
end

------------------------------
-- Externally available methods
------------------------------

on mButton_SetAvailability me, fTrueOrFalse
  if pfAvailable = fTrueorFalse then
    return
-- already in this state
  end
if

  pfAvailable = fTrueOrFalse -- set to the new state
  if pfAvailable then
-- show the appropriate member
    sprite(spriteNum).member = pmButtonUp
  else
    sprite(spriteNum).member = pmButtonGray
  end
if
end mButton_SetAvailability

on mButton_GetAvailability me
  return pfAvailable
end

on mButton_Hit me
  if not(pfAvailable) then
    return
  end
if

  me.mouseDown()
  updateStage
-- to show the down state
  endTicks = the
ticks + 15
  -- wait for a quarter of a second
  repeat
while the ticks < endTicks
    nothing
  end
repeat
  me.mouseUp()
  sprite(spriteNum).member = pmButtonUp -- show the up state
end

Notice also that we are introducing a slight naming convention for method names that are designed to be called externally - that is, from outside the behavior. For methods like this, we continue to use the initial letter "m", then add the basic name of the behavior, an underscore, then the name of the specific function. In our button example, we defined the externally available methods: mButton_SetAvailability, mButton_GetAvailability, and mButton_Hit.



Previous Chapter

Table of Contents

Next chapter