LOOPE - Lingo Object Oriented Programming Environment by Irv Kalb

Previous Chapter

Table of Contents

Next chapter

 

Section 2 - Behaviors

Chapter 10 - GetPropertyDescriptionList

 

In chapter 3, we gave an introduction to parent scripts. We used the following example of as the beginnings of simple parent script:

-- BankAccount script
property pPassword
property pBalance


on
new me, password, initialBalance
  pPassword = password
  pBalance = initialBalance
  return
me
end

This example script has two properties, pPassword and pBalance. Notice that in the declaration of the new method, we are able to pass in parameters. Because we know that the new method is the first one executed, the code of this new method essentially just gives initial values to the two properties. To instantiate an object from a parent script, you have to explicitly execute a call to the new method, and you can optionally pass in parameters. For example, you can create a Bank Account object using this line:

oBankAccount = new(script "BankAccount", "xyzzy", 400.00)

You can create a second bank account object with different initial values by passing in a different set of parameters.

oBankAccount2 = new(script "BankAccount", "mypassword", 1000.00)

However, when we are dealing with behaviors, the programmer does not write this kind of explicit statement -Director does it for you. Remember that with a behavior, Director instantiates an object when the play head first enters a sprite span that has an attached behavior. So, how can you assign initial values to a property (or properties) of a behavior? The answer lies in a special handler called "on getPropertyDescriptionList".

Let's use a very simple example. In the previous chapter, we built a re-usable button behavior. Now, let's say we want to build a frame in the score with a number of different navigation buttons. When clicked, we want each of these buttons to send the program to a different label in the score. From the last chapter, we know that when the user successfully uses our button behavior, the button behavior will send out an mHit message. Also from the last chapter, we said that a simple behavior to receive this message and send the program to a particular frame would look like this:

-- Go End when button is hit

on mHit me
  go
"End"
end

However, when you press another button we would want to send the program to a different frame. On another button, if we want to send the program to a frame named "Game", then we would need a separate behavior like this:

-- Go Game when button is hit

on mHit me
  go
"Game"
end

And so on. We would wind up with a number of similar behaviors that would only differ in the name of the frame to go to. What we would really like to do would be to turn the name of the frame into a variable, and be able to specify the name of the frame as a parameter. By doing this, we could consolidate all these nearly identical behaviors into one. Here is the code of such a behavior:

-- Go to a specified frame

property pFrameName

on getPropertyDescriptionList
  lDescription = [:]
  addprop(lDescription, #pFrameName, [\
      #comment:"Frame to go to?", \
      #format:#string, \
      #default:""])
  return lDescription
end

on mHit me
  go pFrameName
end

This behavior declares one property, pFrameName which will be used to store the name of the frame we want to go to. The code of the mHit method is simple enough, it just goes to the specified frame. But how does this behavior get a value into the pFrameName property? Let's look at the getPropertyDescriptionList handler. (Programmers often refer to this by the shorter name: "gpdl").

As its name implies, the job of the getPropertyDescriptionList handler is to create a property list that describes the properties that to which you want to give initial values. To be clear, it does not give the initial values - but rather it describes the types of values that each can be given. The typical code of a getPropertyDescriptionList handler is to create an empty property list, add one or more items to the list, then return the list. In the example above, the first line creates an empty property list, one item is added to the list, and finally the list is returned.

The code within "on getPropertyDescriptionList" runs at author-time, not at run-time. getPropertyDescriptionList is not intended to be available as a method of the behavior to be called by your code. Therefore when talking about gpdl, I will refer to it as a handler, not a method. Because it really is a handler and not a method of the behavior, it is perfectly fine to leave off the "me" reference when writing this handler. However, it will not hurt to add the me reference after the name even though there are no parameters passed in.

When you drop a behavior that has a getPropertyDescriptionList handler onto a sprite, Director calls this handler, receives the resulting property list, and uses it to build a "Parameters" dialog box to present to the user. The Parameters dialog box that Director creates from the above behavior would look like this:

The dialog box allows you to enter the name of the label to go to. At run-time, when the play head enters this sprite span, the string that you typed in to answer the "Frame to go to?" question, is assigned to the property, pFrameName. It is very important to understand that this is a two-step process. The gpdl handler runs at "author-time" - that is, while you are inside Director and your program is not running. The assignment of the resulting value happens at "run-time" when the program is running in Director, as a projector, or as a Shockwave piece.

But what are these items we are adding to the list? You must add an element into the list for each property that you want to be able to give an initial value to. For example, if you had properties named: p1, p2, p3, p4, and p5, but you only wanted to give initial values to p1, p4, p5, then the property list that you build would only have three entries, one for each of the respective properties. (The other two, p2 and p3, would be given the standard value of VOID to show that they were not initialized.)

The property list that you create is actually a property list of property lists. You can think of it this way. For each property that you want to give an initial value, you must give the name of the property (as a symbol), then a property list that describes the attributes of that property. Each property you define in the property list corresponds to one line in the resulting dialog box. For each property, you specify three (or optionally four) items:

#comment - This is the text on the left that prompts the user to choose or type a response.

#format - This describes the type of the allowable input. Valid values for #format are: #integer, #float, #string, #symbol, #member, #bitmap, #filmloop, #field, #palette, #picture, #sound, #button, #shape, #movie, #digitalvideo, #script, #richtext, #ole, #transition, #xtra, #frame, #marker, #ink, and #boolean.

#default - This is an initial value that is presented to answer the prompt.

You can also optionally specify:

#range - This allows the user to choose from a range of answers. The range can be specified in two different ways, either as a linear list of enumerated values, or for numeric values as a property list with minimum and maximum values. Some example of a linear list of values are as follows:

#range: ["lion", "tiger", "elephant", "bear", "eagle"]

#range : [1, 2, 3, 4, 5]

#range : [member("foo"), member("bar"), member("square"), member("rectangle")]

However, for numeric types of #integer or #float, you may give a range by specifying a minimum and maximum value as follows.

#range: [#min:10, #max:40]

#range: [#min:8.40, #max:10.20]

If you have specified a range as a linear list of values, then in the resulting Parameters dialog box Director will display a popup, and each value that you give will appear as an entry in the popup. If you specify a range with a minimum and maximum, Director will display a slider and allow the user to choose a value within the range that you gave.

If you specify a format of #boolean, then Director will display a checkbox. If you give #default value of TRUE, then the checkbox will be on. If you give a #default value of FALSE, the checkbox will intially be off. Here is an example of a getPropertyDescriptionList that uses a variety of different #format and #range values:

-- Sample GPDL

property pVar1
property pColorChoice
property pNumberChoice
property pfBoolean

on getPropertyDescriptionList me
  lDescription = [:]

  addprop(lDescription, #pVar1, [\
      #comment:"Some starting value as a string:", \
      #format:#string, \
      #default:"Some default"])

  addprop(lDescription, #pAnimalChoice, [\
      #comment:"Choose an animal:", \
      #format:#string, \
      #default:"lion",\
      #range:["lion", "tiger", "elephant", "bear", "eagle", "emu"]])

  addprop(lDescription, #pNumberChoice, [\
      #comment:"Pick a number:", \
      #format:#integer, \
      #default:15,\
      #range:[#min:10, #max:40]])

  addprop(lDescription, #pfBoolean, [\
      #comment:"Should this be on:?", \
      #format:#boolean, \
      #default:TRUE])

  return lDescription
end

If you were to drop this behavior onto a sprite, Director would build and show the following Parameters dialog box:

For most of the other types of #format (e.g., #member, #bitmap, #filmloop, #field, #palette, #picture, etc.), Director will build a popup that lists all members of the given type. For example, if you specified a #format value of #sound, Director would build a popup and populate it with all the sound members in all cast files. Imagine that we want to build a behavior to play a sound when a button is clicked. But rather than always playing the same sound, we want to be able to have the choice of a number of different sounds to play. For example, if you had a cast file with the following sound cast members:

Then you can build a gpdl handler as follows :

-- Sample Sound GPDL

property pSound

on getPropertyDescriptionList me
  lDescription = [:]

  addprop(lDescription, #pSound, [\
      #comment:"Choose a sound:", \
      #format:#sound])

  return lDescription
end

When you drop this behavior on a sprite, Director would present a Parameters dialog box that asks the user to choose a sound from a popup list that contains all the sounds found in the cast above.

Where are the choices stored

The choices that you make and strings that you type in to answer a Parameters dialog box are stored with the sprite in the score. Imagine that you have a behavior attached to a sprite in channel 1, and the same behavior is attached to a sprite in channel 2. Further, assume you had filled in answers to a Parameters dialog box for both. If you go to the score and move the sprite in channel 1 to channel 4, then the parameter values you had specified for the behavior in channel 1 get moved along with the sprite to channel 4.

The actual way that these values are stored in the score is unimportant (the standard "we don't know and we don't care" applies here). All we need to know is that when the play head first enters a sprite span, the values that have been assigned via a Parameters dialog box will be assigned to the property variables before the on beginSprite method starts executing. Even if a behavior does not have an on beginSprite method (as in the simple "go to specified frame" behavior earlier in this chapter), you can assume that all property variables will get their initial values before executing any methods in a behavior.

While you are in authoring mode - that is, in Director while the program is not running - you can look at and/or change the values that have been entered into the parameters dialog box. If you click on a sprite with one or more behaviors attached, you can bring up the Behavior Inspector. The Behavior Inspector can have many different forms depending options that you choose. However, in any form it will show you the behaviors that you have attached to the sprite. Here is a typical Behavior Inspector that shows two behaviors attached to a sprite:

In this case, because the names of the behaviors are fairly small and there are few parameters specified, you can see all the information. However, if the names are long, or there are a number of parameters specified, you may not be able to read all the information. To bring up the Parameters dialog box for a specific behavior, you click on the behavior in the list, then click on the "gear" button in the button bar at the top - or alternatively, double click on a line in the Behavior Inspector. (After single clicking on a behavior in the Behavior Inspector, you can click on the script button to bring up a window with the code of the behavior instead.) If you click on the gear button, Director will get the current values that have been saved in the score for this instance of the behavior and bring up the Parameters dialog box with all the current values filled in:

Here you can change any values you wish. Another alternative that some people use is the Behavior tab inside the Property Inspector. Clicking on this tab brings up a window that gives you the same list and same functionality as the Behavior Inspector.

 

GPDL can contain code too

The getPropertyDescriptionList handler is just made up of Lingo statements. We now know that the result of an on getPropertyDescriptionList handler must be a property list, but the code of a gpdl is not restricted. In the above examples, we simply built up a property list by using addProp to add properties to the property list. This is very typical. However, the code to determine the list of choices for a particular property can be as complex as you wish. Here is an example.

Earlier in this chapter we showed how you can use a #format of #sound to have Director find all the sound cast members. An important ramification of just using a member type is that a castlib or multiple castlibs that are heavily populated may cause a long delay while the Parameters dialog box is being constructed. For example, if your casts contained hundreds of sounds, it would take Director a while to search all the casts for all members of type #sound, then create the popup. Further, it might be difficult for you to find the exact sound you want.

But, let's say that you don't want to choose all the sounds, just some of them. Imagine that we are building a program that was a test. When the user answers a question, we might want to play just a "correct" (right) sound or an "incorrect" (wrong) sound. In the cast that we showed earlier, there were actually two different types of sounds. For our testing program, we would like to build a behavior that allowed us to choose from a list of the appropriate type of sounds. In fact, we would like to build the behavior to be so general that it can find the appropriate sound members in the cast automatically. To do this, we'll take advantage of the fact that Lingo allows you to find members in the cast by name easily and quickly. Lets look at a new castlib:

Notice that we have placed special text members to be used as "markers" in the cast. The first member has a special name "RightSoundsStart". The last member in that line is named "RightSoundsEnd". In between, are the sounds that we wish to choose from. A getPropertyDescriptionList handler can easily be written to find markers like these in the cast. Here is the code of a behavior that can take advantage of this placement of markers.

-- Play a right sound when hit

property pSoundToPlay

on getPropertyDescriptionList me
  lDescription = [:]

  -- Find "right" sounds dynamically in the cast
  -- First find the beginning marker
  startingRightNum = member("RightSoundsStart").member.number
  if startingRightNum <= 0
then
    alert("Missing member RightSoundsStart to show where the Right Sounds begin")
  end
if
  -- Next find the ending marker
  endingRightNum = member("RightSoundsEnd").member.number
  if endingRightNum <= 0
then
    alert("Missing member RightSoundsStart to show where the Right Sounds ends")
  end
if
  -- Now build up a list of members in between the markers
  lRightSounds = []
  repeat
with thisMemberNumber = (startingRightNum + 1) to (endingRightNum - 1)
    thisMemberName = member(thisMemberNumber).name
    append(lRightSounds, thisMemberName)
  end
repeat

  addprop(lDescription, #pSoundToPlay, [\
      #comment:"Sound to play?", \
      #format:#string, \
      #default: lRightSounds[1],\
      #range: lRightSounds])
  return lDescription
end


on mHit me
  puppetSound
1, pSoundToPlay
end

In the code of the gpdl handler above, we first find the markers in the cast. The type or content of these markers does not matter - we find them by name in the cast. Making them text or field members allows us to make these markers more visible in the cast window. Then, knowing their positions in the cast, we build a list (lRightSounds) of all the members in between. We can have as many sounds as we want to in between the markers. The code takes the list of members in between the markers and uses it as the list that is presented in the popup in the Parameters dialog. We use the first item in the list (lRightSounds[1]) as the default. The resulting Parameters dialog box looks like this:

If you just wanted the user to pick from a limited number of sounds, using these types of markers in the cast makes it both much quicker, and easier to find the sound you want. Given the layout in the cast, we could also easily write a "Play a wrong sound when hit" behavior by using the second set of markers.

Let's take this a step further. Imagine that you wanted to play an external sound file. We can build a behavior that will search through a folder and present the names of all the files in that folder. In addition, Let's also allow different "triggers" for the sound. That is, the sound will only to play when the user's choice of events happens. Here is the code of such a behavior:

-- Play an external sound

property psymTriggerEvent
property pExternalSound
property pSoundsPath


on
getPropertyDescriptionList
  lDescription = [:]

  -- Build up list of external sound files (in the Sounds folder)
  lExternalSounds = []
  sort(lExternalSounds)
  delim = the
last char of the moviePath
  sSoundsPath = the
moviePath & "Sounds" & delim
  repeat
with i = 1 to the maxInteger
    thisFileName = getNthFileNameInFolder(sSoundsPath, i)
    if thisFileName = EMPTY
then
      exit
repeat
    else
      add(lExternalSounds, thisFileName)
    end
if
  end
repeat

  addProp(lDescription,#pExternalSound, [\
        #default:"",\
        #format:#string,\
        #comment:"Play which external sound?",\
        #range:lExternalSounds])

  -- Create a list of possible trigger events
  lEvents = [#beginSprite, #mouseDown, #mouseUp, \
                  #mouseEnter, #mouseLeave, #mHit]
  addProp(lDescription,#psymTriggerEvent, [\
         #default:#beginSprite,\
         #format :#symbol,\
        #comment:"What is the start triggering event?",\
        #range:lEvents])

  
  return lDescription
end
getPropertyDescriptionList


on
beginSprite me
  delim = the
last char of the moviePath
  pSoundsPath = the
moviePath & "Sounds" & delim
  if psymTriggerEvent = #beginSprite
then
    me.mPlaySound()
  end
if
end


on
endSprite me
  if psymTriggerEvent = #endSprite
then
    me.mStopSound()
  end
if
end
endSprite

on exitFrame me
  if psymTriggerEvent = #exitFrame
then
    me.mPlaySound()
  end
if
end
ExitFrame

on mouseDown me
  if psymTriggerEvent = #mouseDown
then
    me.mPlaySound()
  end
if
end

on mouseUp me
  if psymTriggerEvent = #mouseUp
then
    me.mPlaySound()
  end
if
end

on mouseEnter me
  if psymTriggerEvent = #mouseEnter
then
    me.mPlaySound()
  end
if
end

on mouseLeave me
  if psymTriggerEvent = #mouseLeave
then
    me.mPlaySound()
  end
if
end

on mHit me
  if psymTriggerEvent = #mHit then
    me.mPlaySound()
  end
if
end

on mPlaySound me
  sound
playFile 1, pSoundsPath & pExternalSound
end

There are some important things to notice in this behavior. We have made an assumption that all sounds that we wish to choose from are to be found in a folder called "Sounds" that exists at the same folder level as the program. In the getPropertyDescriptionList handler we build up a list of the contents of the "Sounds" folder using getNthFileNameInFolder. Because gpdl runs at author-time, it will dynamically get the current contents of that folder, and the names of all files will appear in the resulting popup. The list is sorted so that the names appear in alphabetical order.

Notice that we have to build the path name twice; once in the gpdl handler, and then again in the beginSprite method. This is because when the gpdl runs, no object has actually been created for this behavior and therefore, no properties (such as pSoundsPath) can be assigned at this time. As we said earlier, Director creates an instance of an object for this behavior when the play head first enters the sprite span. At that time, Director also gives values to the properties that were set via the Parameters dialog box. Because we cannot save the path to the sounds in the gpdl handler, we must initialize pSoundsPath in the beginSprite handler.

Another thing to notice is that we have built this behavior to allow it to work depending on the user's choice of a number of different events. Most are Director events like mouseUp or mouseDown. However, we have added the choice of our own custom event, mHit, to the list of events. The behavior uses the property psymTriggerEvent to remember which event was selected as the trigger. The method for each of these events is identical and very simple. Each method just checks to see if their event was the one selected by the user. If so, it calls the mPlaySound method to play the sound. If the method is not the chosen event, (e.g., the user chose to play a sound on mouseUp, but we are inside an on mouseDown method) the code doesn't do anything. Finally, when the mPlaySound method is called, it plays the appropriate sound from the sound file in the selected folder.

One final note. The code inside a getPropertyDescriptionList handler may also call code outside of the current script. For example, we could take the part of the code in the gpdl above that gets the contents of a folder, and turn it into a utility routine (in a movie script).

on GetContentsOfFolder sFolderName, fSorted
  lContents = []
  if fSorted then
    sort(lContents)
  end
if
  repeat
with i = 1 to the maxInteger
    thisFileName = getNthFileNameInFolder(sFolderName, i)
    if thisFileName = EMPTY
then
      exit
repeat
    else
      add(lContents, thisFileName)
    end
if
  end
repeat
  return lContents
end

This way, it would be available to any place in the program. Moving this code into a utility handler would this would make the code of the gpdl smaller and easier to read:

-- Play an external sound 2

property psymTriggerEvent
property pExternalSound
property pSoundsPath


on
getPropertyDescriptionList
  lDescription = [:]

  -- Get all the files in the Sounds folder
  delim = the
last char of the moviePath
  sSoundsPath = the
moviePath & "Sounds" & delim
  lExternalSounds = GetContentsOfFolder(sSoundsPath)

  addProp(lDescription,#pExternalSound, [\
        #default:"",\
        #format:#string,\
        #comment:"Play which external sound?",\
        #range:lExternalSounds])

  (etc.)

WARNING: Although this approach of calling movie level handlers from a getPropertyDescriptionList handler does work, there is a bug in Director to be aware of (at least through Director version 8.5).

You are only allowed to call a handler that lives in a movie script cast member that comes before the calling script (in cast order number). For example, if the behavior you were building was in cast member 10, then any movie level handler you call must be in a member before 10. If you try to call a handler that lives in a script which appears later in the cast (or in a later castlib), then when you do a "Recompile All Scripts", you will get the following error message: Script error: Handler not defined. The obvious work-around is to put such utility handlers into low numbered cast members.

 

The currentSpriteNum

By now, you should know about the special variable "spriteNum". As we said earlier, spriteNum will always be set to the value of the current channel where a behavior is attached. However, this is only true at run-time. The variable spriteNum is only given a value when an object has been instantiated from a behavior script.

In this chapter, we have been talking about the special on getPropertyDescriptionList handler. As we have said, this handler runs at author-time, not at run-time. Therefore, spriteNum will not have a valid value when the gpdl handler runs. If we wanted to get the value of the sprite channel to which a behavior is attached, spriteNum cannot be used. Fortunately, Lingo provides another built-in movie property called "the currentSpriteNum" which can be used for this purpose. To ensure that this is correct, we can code the following simple test:

-- Test currentSpriteNum

property spriteNum

on getPropertyDescriptionList
  lDescription = [:]

  if voidp(the currentSpriteNum) then
    put
"the currentSpriteNum is VOID"
  else

    put
"the currentSpriteNum" && string(the currentSpriteNum)
  end
if
  if
voidp(spriteNum) then
    put
"spriteNum is VOID"
  else
    put
"spriteNum" && string(spriteNum)
  end
if

  return lDescription
end
getPropertyDescriptionList

When we drop this test behavior onto a sprite in channel 3, we get the following in the message window:

-- "the currentSpriteNum 3"
-- "spriteNum is VOID"

 

You might want to build a gpdl that allows the user to choose a member to display. So, you can do this easily by writing a line in a gpdl like this:

  addProp(lDescription,#pMemberToShow, [\
        #format:#member,\
        #comment:"Member to show:"])

But what you would like to do is to give a default of the current member in the current sprite. As we have shown, if you try to use spriteNum, you are using a VOID value. As of Director 8, Director has special code to "fail gracefully" in this situation. If you try to generate a member reference using "spriteNum" when spriteNum is VOID, Director returns a special value: member 0 of castlib 0, to indicate that there is no such cast member. For example, if you had coded:

  mStarting = sprite(spritenum).member
  addProp(lDescription,#pMemberToShow, [\
        #default: mStarting,\
        #format:#member,\
        #comment:"Member to show:"])

Then mStarting would be set to member 0 of castlib 0. Because that value is obviously not found in the list of all members, the default is meaningless, and Director chooses the first item in the list as the default. Further, if you were to try to access any property of member 0 of castlib 0, (whether the property should be valid or not), Director will return VOID. Here is an example from the message window:

put (member 0 of castlib 0).width
-- <Void>
put (member 0 of castlib 0).myMadeUpProperty
-- <Void>

Because we are executing code inside of a getpropertyDescriptionList handler, we must use the currentSpriteNum instead of spriteNum. The code would look like this:

  mStarting = sprite(the currentSpritenum).member
  addProp(lDescription,#pMemberToShow, [\
        
#default: mStarting,\
        #format:#member,\

        #comment:"Member to show:"])

When using this approach, Director will put up a Parameters dialog box and will fill in the entry for the "Member to show:" with the proper default - the member in the channel onto which you are dropping the behavior.



Previous Chapter

Table of Contents

Next chapter