Jeanne's World
   House o' HyperCard
       Tricks of the HyperTalk Masters

Jeanne's House o' HyperCard:


Techniques for Stack Development:
Notes from the Scripting Battlefields

from Tricks of the HyperTalk Masters
by Jeanne A. E. DeVoto

Perspectives on HyperTalk
The HyperTalk Zoo
Advanced HyperTalk Topics
HyperTalk Language Style
Tools for Development
Protecting Stacks
Debugging

Please note that this chapter was written in 1989, against HyperCard version 1.22. Many of the limitations discussed in this chapter do not apply to the current version, although I believe most of the basic information is still usable and useful. (By the way, I did not choose the name of this chapter. In fact, they wanted to call it "Wisdoms from the Scripting Battlefields". But I put my foot down. ;-)



Since HyperCard was introduced in August of 1987, many people - by some estimates, as many as 70% of Macintosh owners - have done some programming in the HyperTalk script language. By now, many people are familiar with the basics of HyperTalk. However, most of those people have little or no previous programming experience which might be applied to the task of creating better code and improving their HyperTalk code's speed, efficiency, and clarity. For most HyperCard programmers, much of the power of HyperTalk remains untapped, simply because people are not aware of the interesting and powerful ways they can use the language.

Even clumsy scripters can write HyperTalk programs to perform almost any task; the test of the scripter's skill is in whether or not those programs are efficient, easy to read, and elegant. Programming "elegance" is a quality that's hard to define, but you'll know it when you see it. Elegant programs accomplish a great deal with seeming simplicity. They take advantage of the special features of the language, but they do so in a way that is clear and easily understood by the reader.

This chapter does not attempt to explain all of the HyperTalk language, nor does it discuss basic topics such as how to edit a script or what a message is. Rather, it attempts to present some techniques to help stack development, some ways to do new things in HyperTalk and work around its syntactic limitations, and some guidelines for the perplexed on what constitutes "good HyperTalk programming". This chapter is for those of you who already know the basics, but are looking for more.

Before you tackle the material in this chapter, you should have done some scripting and have a good grasp of the basics of HyperTalk. You should have some familiarity with the message hierarchy, with writing handlers for common system messages such as mouseUp and mouseDown, and with using the message box to send commands. If any of these terms are unfamiliar to you, it will probably be best for you to read a basic HyperTalk book (such as the Waite Group's HyperTalk Bible or Danny Goodman's Complete HyperCard Handbook) before you tackle the material in this chapter.


Perspectives on HyperTalk

Often, beginning scripters who have had some exposure to a procedural language (such as Pascal or BASIC) have trouble with the object-oriented aspect of HyperTalk. This section describes some typical stumbling blocks that may cause problems, and discusses ways to avoid conceptual problems that interfere with your ability to construct functional and efficient scripts.

Mastering Messages

This is the point at which most discussions of HyperTalk introduce a graph to show the message hierarchy. Many HyperTalk guides place great stress on the question of whether messages should be visualized as originating at the bottom of the hierarchy and bubbling upward, or originating at the top and flowing down. However, it's not really important whether you think of messages as flowing downward through the hierarchy or as going up. What does matter is that you have a clear idea of what's going on when a message is sent along the hierarchy. In this chapter, the focus is on the order in which messages are received by HyperCard's objects, rather than the direction in which they are perceived to be going.

Handlers

HyperTalk scripts consist of two kinds of handlers: message handlers and function handlers. When a message (such as mouseUp) is sent, HyperCard searches the message hierarchy to find a script that contains a handler for that particular message, and if such a handler is found, the instructions in it are executed. A function call, on the other hand, searches the message hierarchy for a function handler, which computes a value and returns it to the handler that called the function.

HyperCard has many built-in functions, and the most common messages are system messages (which are sent automatically by HyperCard when the user performs certain actions such as clicking the mouse). You can write both custom functions and custom message handlers.

Message Handlers

A message handler is analogous to a procedure in Pascal, or to a subroutine in BASIC. Message handlers are usually written to handle system messages (such as mouseDown or openCard) that are sent automatically when the user performs certain actions, such as clicking the mouse or going to another card. However, you can cause a script to send a custom message of your own, which will invoke a custom message handler.

Suppose you designed an animation sequence, which is to be performed when the user clicks on any of several buttons in the stack. Instead of duplicating the instructions for the animation sequence in the script of each button, you can place a custom handler called animateButtons in a central location, such as the stack's script:

  on animateButtons
    -- your commands to animate the buttons go here
  end animateButtons

The scripts for the individual buttons look like this:

  on mouseUp
    animateButtons
  end mouseUp

When one of these buttons is clicked, its mouseUp handler sends the message animateButtons. This message is sent through the levels of the message hierarchy until it reaches the stack level. Since the stack script contains a handler for the animateButtons message, the handler is executed.

The use of a custom message handler, like the use of a procedure in other languages, helps eliminate unnecessary duplication of code. It also makes it much easier to alter and enhance your stacks. In the button animation example, for instance, to change the way the animation sequence worked, you would only need to change the animateButtons handler, rather than each button's script.

You can also use message handlers as a shorthand for a sequence of commands. Suppose your scripts often set the lockMessages, lockScreen, and lockRecent properties to true while in a script that goes to another card or stack, in order to save time and avoid user confusion. You can put the following handlers into the script of your stack:

  on lockEverything
    set the lockMessages to true
    set the lockScreen to true
    set the lockRecent to true
  end lockEverything

  on unlockEverything
    set the lockMessages to false
    set the lockScreen to false
    set the lockRecent to false
  end unlockEverything

If these handlers are present in the message hierarchy, you can turn these three properties on or off with just a single line in your scripts.

Function Handlers

HyperCard has many built-in functions, some of which you may already have used in your scripts. For example, the date is a built-in HyperTalk function that returns the current date. You can also define your own functions for special purposes.

Like a message handler, a function handler can be placed anywhere in the hierarchy beyond the calling handler. For instance, if a function is called by a handler in a card script, then the function handler will be found if it is in that same card's script, in the background, stack, or Home stack script.

A note about function calling conventions: A built-in function is either preceded with the or followed by parentheses. For example, the built-in HyperTalk function that returns the date can be written in either of these two ways:

  the date
  date()

However, functions that you write must be called in the second form. If the function requires any input values, they are placed in the parentheses, separated by commas.

The Message Path

Most people who have done any scripting are familiar with the standard message path. The mouseUp message resulting from a click on a button is sent first to the button, then (if no handler for the message is found in that script) to the card, then to the background, to the stack, to the Home stack, and finally to HyperCard itself. This section describes situations in which this model does not hold true: circumstances in which either some or all message are not sent, or the message path changes.

For example, if you click on an unlocked field, mouseDown and mouseUp messages are not sent. But if you hold down the command key while clicking, these messages are sent to the field just as though it were locked. The text you click on is also placed in the message box.

While any tool other than the browse tool is chosen, mouse messages and idle messages are not sent. And while the lockMessages property is set to true, system messages (such as open and close messages and mouse messages) are not sent. Since the lockMessages property is set to false by HyperCard on idle, it can only be set to true while a handler is actually being run.

As far as messages are concerned, stacks other than the current one are "black boxes" - you can send a message to another stack, but you cannot send a message to an object (such as a card or button) within another stack without going to that stack first. Nor can you act on objects in another stack - set object properties, get text in fields, and so on - without going to that stack. (Of course, you can have your script set the lockScreen property to true before going to another stack - this way, the user will not see that you've ever left the current stack.)

This brings up the topic of how the standard message path changes when a script switches to another stack. Suppose the user is in stack A, running a handler that's located in a card script. Normally, any messages sent while the handler is running go through the usual message hierarchy. However, if the handler contains a command to go to another stack, then the current stack is not the same as the stack the handler is running from. Under these circumstances, the message hierarchy expands to include the current stack. To be exact, if a message is not intercepted in the hierarchy of stack A, it is sent next to the current card in stack B, then up through stack B's hierarchy. Only if the message is not intercepted along the way does it then go to the Home stack.

[diagram of the message path]
If a handler contains a command to go to another stack, the message path expands to include the current stack.

In addition to system messages sent when a user does an action such as clicking the mouse button, HyperCard sends a message every time a command is used. Every HyperTalk command sends a message. If the line "visual effect zoom open" is in your script, it sends a visual message (which normally is interpreted at the HyperCard level) every time the handler that contains it is run.

This is possible because the HyperCard application itself is a part of the message hierarchy. The visual message starts at the origin and proceeds through the hierarchy like any other message. If no handler intercepts the message, it is sent to the HyperCard object, and since it is a built-in command (which is just another way of saying that it can be handled by the HyperCard object), it is handled at that level.

This fact has several implications. You can trap any HyperTalk command at any level and modify its function. For example, you can disable all the visual effect commands in a stack by adding the following empty handler to the stack script:

  on visual
  end visual

If this handler is in place, all visual commands will be trapped when they reach the stack level.

Local and Global Variables

A variable is the name of a location that holds a value. You can think of a variable like a grocery bag into which you're allowed to put information: you retrieve the information in the bag by giving the bag's name.

There are two kinds of variables in HyperTalk: local variables and global variables. A local variable is temporary: it lasts only as long as the handler it's in is executing. As soon as the handler ends, the variable is forgotten. Globals, on the other hand, stay around until you quit HyperCard. This means that a local variables is only accessible from the handler in which it's used, whereas a global variable can be accessed from any handler. Global variables are even retained when you move from one stack to another.

You use a local variable by putting something into it; you don't need to declare local variables first, as you must in most other languages. HyperTalk doesn't know about the existence of the variable until you put something into it, so there will be trouble if you try to get the contents of a local variable that you haven't put anything into yet.

A global variables, unlike a local variable, must be declared in every handler you use it in. For example, you might place a value in a global variable in your openStack handler, and then use that value later to determine some action. The two handlers might look like this:

  on openStack
    global timeToPlay, startTime
    ask "How many minutes do you want to play?"
    put it * 60 into timeToPlay  -- convert it into seconds
    put the time into startTime  -- keep a record of the current time
  end openStack

  on idle
    global timeToPlay, startTime
    if startTime + timeToPlay > the time then
      beep
      answer "Time's up!"
      quit
    end if
  end idle

The handler stores the starting time and the amount of time the user wants to play in the global variables startTime and timeToPlay. During play, the idle handler checks whether the specified time has passed; if it has, it quits the game.

Global variables can be declared anywhere in the handler, as long as they are declared before use. Global declarations are usually placed at the beginning of their handlers to make them easier to find.

By the way, the message box assumes all variable names typed into it are those of global variables, so you can use global variables in the message box without a global declaration.

One disadvantage of using global variables is that they continue to use memory space, since they're not forgotten when you leave the handler. This doesn't matter if you use only a few global variables to store small amounts of data, but if you have used a global to hold a large amount of information, it will lock up a significant amount of memory. This memory is unavailable to HyperCard for other operations. While global variables cannot be purged once they are declared (except by quitting HyperCard), they will use less memory if they contain smaller amounts of data. For instance, suppose you use several global variables in a stack to move large amounts of data around. In your closeStack handler, you can remove that data and make each global variable as small as possible, to free up memory:

  on closeStack
  global customerData, officeData, callData
    put empty into customerData
    put empty into officeData
    put empty into callData
  end closeStack

Remember, if this stack contains any scripts that go to another stack and then return, the closeStack handler will be triggered and the global variables will be emptied. It may cause havoc if your handlers expect these global variables to contain the data that has been emptied by the closeStack handler. To avoid problems like this, you can have your script set the lockMessages property to true before it goes to another stack. With the lockMessages property on, the closeStack message is not sent.


The HyperTalk Zoo

This section discusses a few special HyperTalk words. Some are functions, some are variables, and some are neither; what they have in common is that they are very powerful and often misunderstood.

The it variable

HyperCard has a special variable called it which is used to store the results of four commands: answer, ask, get, and read.

The answer command displays a dialog box with up to three buttons. The name of the button clicked by the user is placed in the it variable. For example, if HyperCard displays a dialog box in response to the following line:

  answer "Do you want to play a game?" with "Yes" or "Maybe" or "No"

and the user clicks the "Maybe" button, then the it variable will contain "Maybe". The next lines of the handler might test the it variable:

  if it is "Maybe" then answer "Make up your mind!" with "Yes" or "No"
  if it is "No" then quit

The ask command displays a dialog box for the user to type an answer. The answer goes into the it variable. (If the user clicks the Cancel button, the it variable is empty regardless of whether an answer was typed in.)

The result of the get command is placed in the it variable.

The read command is used to read data from a file. This data is placed in the it variable.

Since the it variable may be changed by any of these commands, you should avoid keeping information in it any longer than necessary.

me and the target

The word me is a special object descriptor: it is the object in whose script the currently executing handler resides. The target, which has no equivalent in languages such as Pascal, is the object which first received the message now being handled.

For example, suppose you click on a button which has no mouseDown handler, but there is a mouseDown handler on the stack level. The me object is the stack because it contains the handler which is executing; the target is the button that originally received the mouseDown message when you clicked on it. If the object that first receives a message has a handler for that message, me and the target are the same.

One common use for the target function is to handle situations in which there is a handler for a certain message at the stack level, but that handler is only intended to deal with messages from certain objects. For instance, suppose there is a mouseUp handler at the stack level that handles only buttons and makes no sense when applied to fields or cards. The target lets you test what kind of object was clicked on:

  on mouseUp
    if word 2 of the name of the target is "button" then
      visual effect zoom open
      go to card the short name of the target
    else
      pass mouseUp
    end if
  end mouseUp

Version 1.2 of HyperCard included new enhancements to both the me descriptor and the target function. If me is a field, you can use me to refer to the contents of that field. For example, if me is a field, this handler will place the first line of the field in a variable:

  put line 1 of me into someVariable

The word "target" now refers to the contents of a field. If the target is a field, then

  get the target  -- places the name of the field in the it variable
  get target      -- places the contents of the field in the it variable

the selection

The selection is a familiar concept for Macintosh users: text is selected by dragging through it (or by double-clicking on a word to select it). The current selection is highlighted.

In HyperTalk, the current selection is a container and can be manipulated like any other container: you can put something into the selection (replacing it), or get the selection (enabling your script to prompt the user to select text to be manipulated).

One tricky use for the selection container is follows. If the user clicks on a locked text field, this mouseDown handler for the field places the word the user clicked on into a variable. This capability enables you to implement some hypertext functions in your stacks: for instance, you can go to a glossary card that gives the meaning of the word that was clicked, or find the next occurrence of that word in the stack. You can put the following handler into the script of any locked field:

  on mouseDown
    set the lockScreen to true
    set the lockText of the target to false
    click at the clickLoc     -- double-click to select a word
    click at the clickLoc     --
    put the selection into myWord
    set the lockText of the target to true
    set the lockScreen to false
    -- [you can now do whatever you want with the selected word]
  end mouseDown

Some actions deselect any text that is currently selected. These actions include clicking in another field, going to another card, and highlighting a button. If one of your scripts requires the user to select some text to be worked on and then click a button, that button's autoHilite property must be turned off.

the result

The result retrieves any error messages that were generated by the last go or find commands. If the result is empty after such a command, it means the command was successful; if the result is not empty, the command failed. Here is an example of using the result to determine whether a find command was successful:

  on mouseUp
    ask "Which title do you want to find?"
    if it is not empty then findTitle it
  end mouseUp

  on findTitle theTitle
    global searchList
    find theTitle in background field "Book Title"
    if the result is not empty then  --- means the find failed
      beep
      answer "Sorry, that title is not in this list."
      exit findTitle
    end if
    put return & background field "Title" after searchList
    ------- the search list variable keeps track of which book entries
    ------- each user has looked at
  end findTitle

By first checking whether the Find was successful, this handler avoids adding extra titles to the user's search list.

The contents of the result function can also help your script determine whether a particular card exists. Try going to it; if the result is not empty, then the go command failed and there's no such card.

  on mouseDown
    go to card "Noodles"
    if the result is not empty then
      --- [the card does not exist]
    else
      go recent   --- go back where you where
      --- [the card does exist]
    end if
  end mouseDown

the

One of the most confusing points for HyperTalk beginners is the use of the word "the". HyperTalk's syntax is fairly forgiving, and often "the" is optional. However, there are a few situations where its use or omission will confuse HyperTalk, and you should be aware of what these situations are. The rules for using the follow:

  1. The word "the" may not be used in front of the name of an object, the name of a variable, or the name of a function that takes parameters. For example:
      go to the first card of this stack
      put "Yes" into the it
      get the sin(22)
    These lines are not legal in HyperTalk. If you use the where it doesn't belong, HyperCard generally interprets the illegal expression as the name of a stack.
  2. The word "the" is necessary in front of functions that do not take a parameter and in front of properties. You can omit the in front of a property in a set command, since it's clear that the word refers to a property. The following lines will cause problems because they omit the:
      put date into field "Current Date"
      if dragspeed is not 0 then set dragspeed to 0
    If you don't include the when referring to a property or function, HyperCard will interprets the word as a variable or, if there is no variable by that name, uses the word as a literal. For example, the first line above will place the word "date" into the field, instead of the current date (unless "date" has previously been used as a variable, in which case it will use the contents of that variable). For HyperCard to recognize that you want the built-in date function, the line must be written either
      put the date into card field  "Current Date"
    or
      put date() into card field "Current Date"

Advanced HyperTalk Topics

This section discusses two topics of interest to advanced scripters: the use of recursion, or self-calling routines, in HyperTalk, and the use of chunking expressions in variables to simulate list and array data types. Some ways to use the Find command are also explored.

Limiting the find Command

The simplest form of the find command is written like this:

  find "thesewords"

This command is invoked in the same form by choosing the Find menu item. It searches for the first card on which the specified strings appear in card or background fields. (A string is any set of characters that doesn't include a space or return.) If you specify more than one string, the find command searches for the first card on which all the strings appear. This simplest form of the find command matches the string only if it appears at the beginning of a word; the find chars command matches the string anywhere within a word.

Although the search algorithm used by the find command has not been made public, it is well-known that the find command works faster if the strings you specify consist of at least three alphanumeric characters. If your stack contains a great deal of free space, searches will be somewhat slower; to speed up your searches, compact the stack often.

The find command can be limited to a single background field as follows:

  find "27G" in background field "Specifications"

Unfortunately, while the find command can be limited to one background field, it cannot be limited to a single background. If you issue the above command and find is unable to locate the search string in the specified background field, it will begin looking in other backgrounds for the search string. Specifically, if the field to which the search has been limited has field number X, the find command will search background field number X in each background.

Fortunately, there are methods to work around this limitation. Since the find command can be limited to a background field, you can make certain that field number X, in all backgrounds except the one to which you want to limit the search, is empty. In other words, in each background except the one to be searched, create a hidden, empty background field, and use the "Bring Closer" and "Send Farther" commands to set the field number of each one to X.

On a successful find, you can check the card on which a match is found, to see whether it is in the background you're looking for:

  on mouseUp
    ask "What do you want to find?"
    set the lockScreen to true
    find it in background field "Entries"
    if the result is empty then     -- means the find was successful
      if the name of this background is not "Order Entry" then go recent
      -- wrong background - so return to the previous card you were on
    end if
    set the lockScreen to false
  end mouseUp

You can also limit the find command to a single card field by first checking to see whether that field contains the word you are looking for. If it does, then a find command issued from that card will certainly first find the word in the card field. This is useful when the user wants to locate a work in a long scrolling card field.

  on mouseUp
    global myWord
    if card field "Noodles" contains myWord then find myWord
  end mouseUp 

Structuring Data

Many languages let you structure variables in special ways. For instance, in Pascal you can declare an array variable that consists of a number of items, each of which can be addressed by variable name and item number. On the surface, HyperTalk seems to lack these powerful data-structuring capabilities. However, it is possible to structure a single variable as a two-, three- or even four-dimensional array.

You are probably familiar with the idea of chunk expressions, which let you deal directly with the individual lines, items, words, and characters of a field. But chunking does not apply just to fields. Information in any container, including variables, can be addressed by chunk. By using chunk expressions, you can structure a variable as a tabular array rather than a simple one-valued item.

There are several useful applications for arrays. For example, a stack that plays tic-tac-toe might store the current state of the game board as an array:

  on startGame
    global gameBoard
    repeat with vertical = 1 to 3
      repeat with horiz = 1 to 3
        put empty into item horiz of line vertical of gameBoard
      end repeat
    end repeat
  end startGame

The three lines of gameBoard correspond to the rows on the tic-tac-toe board. The items of each line correspond to the three squares in a row. When the user or the computer places an X or O on the board, the letter X or O is placed in the corresponding item and line of the gameBoard global variable. The stack can then check the gameBoard variable to see which moves are possible.

Recursion

Many scripters' first encounter with the concept of recursion comes when they see the following dialog box:

[recursion error dialog box]

Recursion is defined as a handler calling itself. For instance, if you have a handler for the doMenu command that itself contains a doMenu command, the command within the handler will in turn call the handler again:

  on doMenu theItem
    if theItem is "Prev" then
      doMenu "Home"  --- this command calls the doMenu handler again
    end if
  end doMenu

Recursion is a valuable tool in programming; however, since HyperTalk has a limit on the number of handlers that can be waiting to complete execution at any one time, too many levels of recursion can cause memory problmes. (The limit on the number of pending handlers varies, depending on the complexity of the handler, the length of its name, and other factors. Usually, HyperTalk will call a halt when there are ten to twenty handlers pending.)

You may look at a script that has caused the "Too much recursion" error and see no obvious places in which you've written in an accidental recursion. The reason is that certain commands can result in other messages being sent. One of the most common mistakes that causes the "Too much recursion" dialog to appear is see in the following handler, which might be present at the stack level script:

  on openCard
    go next card
  end openCard

When this handler goes to the next card, of course, the openCard message is sent. The openCard message has a handler that contains the command go next card, which results in sending an openCard message, which .... and your script is caught in an endless hall of mirrors. It will continue to re-run the handler until HyperCard's limit on pending message handlers is reached, at which point the handler's execution is halted and the error dialog box is posted on the screen.

One way to avoid accidental recursion is by using the send command. For example, suppose you want to intercept all visual effect commands in a stack and replace them by dissolves. (Recall from the earlier discussion of messages that you can intercept any HyperTalk command by writing a handler for it.) You might do it like this:

  on visual
    visual effect dissolve
  end visual

However, it can easily be seen that this handler will involve HyperTalk in an endless recursion the first time it is called. To avoid this problem, rewrite the handler like this:

  on visual
    send "visual effect dissolve" to HyperCard
  end visual

This avoids the problem of recursion by sending the replacement visual effect command directly to the HyperCard object, so that the visual message corresponding to the command is not intercepted at the stack level.

As mentioned before, recursion is a valuable programming tool. One common situation in HyperCard stacks for which recursion is valuable is the task of checking a field (such as a code number) to make certain that it's in the right form. This handler checks the format of a customer code in an order-entry stack. If the code does not have the correct number of letters, the handler asks the user to re-enter the code. The handler then calls itself to re-check the new code.

  on checkCode
    if the number of chars in field "Customer Code" is not 7 then
      ask "Please type in the 7-letter customer code"
      put it into field "Customer Code"
      checkCode
    end if
  end checkCode

The checkField handler can be called on closeField, closeCard, or closeStack, depending on the way data is entered into particular stack.

There are many other possible applications for recursive techniques. A complete discussion of recursive techniques is beyond this chapter's scope; however, most general texts on programming include a discussion of recursion and its uses.


HyperTalk Language Style

Some people, particularly those who have never had to debug someone else's programming code, question the importance of programming style. Some regard good, readable style as a frill - an aspect of programming that teachers of computer programming talk about a lot, but one that's not of much practical use.

The major reason why programming style considerations are of value is that someone, someday, may have to read your code and figure out what it does. If you have written your scripts in such a way that it's easy to follow the flow of logic, they will be infinitely easier to maintain and update in the future. Good style will also tend to make your code more efficient and more easily reusable. What is presented here is a set of basic principles to guide you in designing code that's easy to read and understand. There are always exceptions to any rule, but if you keep these guidelines in mind, you will generally find that you are writing better, more readable HyperTalk code.

Eschew Redundant Objects

Take advantage of HyperCard's message structure in clever ways, rather than cluttering up your stack with extra objects that don't really need to be there. For example, there are many stacks that have a transparent button covering an entire card, to enable users to click anywhere on the screen to proceed. Since mouseDown and mouseUp messages are received by the current card, there's no need for this extra button. Put a mouseDown message in the card script instead. In a similar vein, remember that locked fields receive mouseDown and mouseUp messages - which means there's no need to cover a field with a button.

Avoid Singing a Refrain

Suppose that you have a stack that uses pop-up fields on several cards. Such fields are often used to provide additional information or to caption a diagram or picture. The hidden field is made visible when the user clicks on a certain type of button.

Since all these buttons do more or less the same thing, consider whether it might be better to write a general handler in the stack script. For instance, your handler might look like this:

  on mouseDown
    if word 2 of the name of the target is "button" then
      if the style of the target is "rectangle" then
        show card field the short name of the target
        wait until the mouseClick
        hide card field theField
      end if
    end if
  end mouseDown

This handler assumes that all buttons with style "rectangle" control pop-up fields, and that each of those fields has the same name as the button that controls it. This tactic saves a certain amount of space, but even more important, it allows you to change the behavior of your pop-up fields by changing a single handler. For instance, suppose you decide that instead of each field being shown when its button is clicked, then hiding itself on the next mouse click, the field should appear when the cursor enters the controlling button and disappear when the cursor leaves it. It is a relatively simple matter to rewrite the mouseDown handler as a mouseEnter and mouseLeave handler - certainly much simpler than rewriting all the handlers in all the pop-up buttons in your stack.

When you find you're repeating yourself, it's a good time to think about pulling out the repeated part in a separate handler.

This rule also applies to repetition within a handler. For example, if you are performing some action on each field on a certain card, it's much easier to see what's happening if you use a repeat loop.

Don't Be Too Specific

Think of every routine you write as though you were making a tool. The more specific the tool, the harder it is to adapt to other purposes. Writing a handler in a general way will pay off because you can re-use the same code over and over, instead of reinventing the wheel each time your needs change a little.

For instance, suppose you have a rectangular field that contains the names of several cards, each on a separate line. You want to design a routine that will let the user click on any line and go to that card. You could write the handler for the specific conditions you need at the moment; you might use the fact that the line height of this particular field was 16 to determine what line had been clicked, and so forth. However, the ability to click on one line of a field and take some action based on what that line contains is a very useful one. It would be far better, therefore, to spend a little extra effort and come up with a general-purpose handler that can be used to solve other programming problems.

Be Mindful of the Environment

Not everyone is using the same hardware setup as you have. Some users will have larger screens, less memory, or an earlier version of HyperCard. If your stack will be distributed to other HyperCard users, it is an essential part of the testing process to check the stack's behavior on a variety of hardware and software setups.

If your stack uses commands that are specific to certain versions of HyperCard, use the version function to work around the limits for older versions, if possible. At the very least, if your stack contains commands or functions that are specific to a later version, you should check the version in your openStack handler and alert users who have older versions:

  on openStack
    if the version < 1.2 then
      answer "This stack requires HyperCard 1.2 or later."
      doMenu "Home"
      exit openStack
    end if
    -- [the rest of the commands for openStack]
  end openStack

Also, if you change some part of the environment (like the userLevel), be sure to set it back where it was when you leave the stack. For instance, if you want to set the userLevel to 1 (Browsing) in your stack, rather than simply change the userLevel, first save its original value. (See the section on "Setting the UserLevel" for an example.)

Don't Break the Flow

You can break out of a handler at any time using the exit keyword. For example, look at this mouseUp handler for a Quit button:

  on mouseUp
    answer "Do you really want to leave the stack?" with "Yes" or "No"
    if it is "No" then exit mouseUp   -- breaking out of the handler
    --- [do housekeeping required on leaving the stack]
  end mouseUp 

You can also exit repeat similarly to break out of a repeat loop. These constructions can be very useful; however, you should use them with some caution, since breaking out of a handler or loop makes it hard to follow the logic of the program for reading or debugging. Exit constructions are usually easiest to understand when they are there to handle an exception to the normal course of events. For instance, in the example above, the normal course of events is for the user to confirm that he wants to leave the stack. The exit mouseUp command is used only if the user changes his mind and decides not to leave after all.

Avoid Write-Only Code

When you write scripts, bear in mind the probability that someone - without your knowledge of what they are supposed to do - will need to read them at some point in the future. You can aid this hypothetical reader in several ways: use meaningful names for variables and handlers, avoid abbreviations for HyperTalk terms like "card" and "background", use the names of objects where possible, rather than numbers or objects IDs, and most of all, include plentiful comments that describe what each handler does.

Pay attention to the visual clarity of your scripts, too. If you call a function, place its handler conveniently close to the handlers that call it. If you call a function or message handler that resides at a different level of the stack (for example, if you have a button that sends a message to the stack level), note the location of the destination handler in a comment. Set off comments with white space. And, of course, if you have a script line that goes beyond the right edge of the script window, use the option-return character to split the line into two parts.

Some scripters put blank lines into a long handler to distinguish between different parts of it. However, blank lines slow down HyperTalk's execution of a handler quite a bit(see Chapter 16, "Benchmarking HyperTalk"), and they also may make it difficult to find the end of the handler (individual handlers are usually separated with blank lines). Remember also that, especially on a small screen, the number of lines that can be seen in the script editor at any one time is fairly small. The more code lines are visible at once, the easier it is to grasp the flow of a long handler.

These are common-sense observations and techniques: for more information about program style, look in basic programming textbooks (Pascal textbooks in particular tend to lay emphasis on code readability and good design). Most discussions of programming style can be applied to all high-level languages.

Using these guidelines will help keep people from pulling out their hair in exasperation when they need to modify your scripts in the future. And remember that months or years from now, after the details have grown vague with the passage of time, you may need to re-write scripts of your own. So write well: remember, the hair you save may be your own.


Tools for Development

Two difficulties when programming in HyperTalk are keeping tabs on objects and making quick changes to stacks. This section presents small scripts and other tricks that can be used to make development easier.

Keeping Track of Objects

In any but the smallest stack, it's hard to keep all the objects, their locations, and their names in your head. In many cases, objects such as buttons and fields may get lost. (Remember all the early stacks that were based on the Home stack whose authors forgot to remove the transparent buttons from the Home card?)

In all versions of HyperCard, you can hold down the command and option keys to show the outlines of any visible buttons. Version 1.2 of HyperCard added some new capabilities to this feature: when using the button tool, holding down the command and option keys shows the outlines of all buttons, visible and invisible. Holding down the command, option, and shift keys shows the outlines of all visible fields; when using the field tool, holding down these three keys shows the outlines of all fields, visible and invisible.

However, even with these new capabilities, you still may want to keep a central list of objects in your stack. The following script compiles such a list:

  on makeAList
    lockEverything  --- see handler in section on message handlers
    put the name of this stack into theObjectList
    ----- first get all the background object names -----
    repeat with b = 1 to the number of backgrounds
      go background b
      put return & the name of this background after theObjectList
      repeat with x = 1 to the number of background fields
        put return & the name of background field x after theObjectList
        add 1 to x
      end repeat
      repeat with x = 1 to the number of background buttons
        put return & the name of background button x after theObjectList
        add 1 to x
      end repeat
    end repeat
    ------ now get all the card object names ------
    repeat with b = 1 to the number of cards
      go card b
      put return & the name of this card after theObjectList
      repeat with x = 1 to the number of card fields
        put return & the name of card field x after theObjectList
        add 1 to x
      end repeat
      repeat with x = 1 to the number of card buttons
        put return & the name of card button x after theObjectList
        add 1 to x
      end repeat
    end repeat
    put theObjectList into card field "list" of card 1
    unlockEverything -- see handler in section on message handlers
  end makeAList

This handler gives a simple list of all object names in use. Variations on this basic framework might list object IDs along with names, or make a complete script listing.

Making Quick Changes

Often, you will want to use scripts as an aid in development. Such scripts may include locking or unlocking all fields at once, listing objects, searching through scripts, and so forth. There are a couple of ways to make these quick changes easier. You can type single commands directly into the message box. But the message box (at present) can only handle one line, and many things you want to use to make development easier, such as if/then constructs and repeat loops, take more than one line.

One method is to write small, often-used sequences as custom message handlers and place them in the Home stack script (so they will be accessible whenever you're working in HyperCard). You can then run the handler simply by typing its name into the message box.

If you need to invoke one of these scripts often, consider putting it in a button. Then paste the button into the needed location in your stack. A button makes a convenient little container for a handler, because the button can be easily copied and pasted onto the card where you need it. You can remove the button when you're finished developing the stack.


Protecting Stacks

One of the most controversial issues among HyperTalk scripters is stack protection. The general consensus seems to be that locking other scripters out is not good, when it's done as a means of hiding code -- we all benefit from sharing. This sharing is one of the best things about the HyperCard community. A stack whose scripts are protected is also much less useful than the same stack with open scripts, because an open stack can be easily customized to the user's own needs and preferences. (If you really don't want your routine seen by anyone else, you always have the option of writing it as an XCMD.)

However, there may be situations in which you decide you do want to protect a stack. These fall into several categories: protecting ignorant users, keeping a publicly accessible stack from being modified, and protecting personal information. For example, you might want to "childproof" a stack intended for very young users, or protect your personal financial information from being viewed by others.

This section provides a selection of techniques for protecting access to a stack, and also provides information on defeating the various protection methods. However, note well that making use of other people's code in your stacks without acknowledging their contribution is not a good thing to do, even if the code you're using is in the public domain. (If it's copyrighted, it's illegal as well.) If you find a routine you want to use, contact the author and ask permission - most likely, he will be glad to give it and gratified you had the courtesy to ask. And always give credit on your stack's About card to stacks and authors that helped you, even if you didn't borrow from them directly.

Several levels and types of stack protection are discussed below; however, they can all be defeated by a persistent and knowledgeable person.

Trapping Menu Commands

Since a user's selection of a menu item sends the doMenu message to the current card, any menu item can be trapped by a handler for the doMenu message. This means that you can selectively disable any menu item a user might choose. For example, to let a user choose any menu item except those allowing movement within the current stack, you would place this handler in the stack's script:

  on doMenu theItem
    if theItem is "First" or theItem is "Last" or theItem is "Prev" or theItem is "Next" then
      beep
    else
      pass doMenu     ----------- VERY IMPORTANT! -------------
    end if
  end doMenu

If you accidentally omit the "pass doMenu" line, you're in trouble: all menu items will be disabled, including their command-key equivalents. If this happens to you, if the message box is visible (or the blindTyping property is set to true), you can type the command

  edit script of this stack

The script editor will appear and you can then add the "pass doMenu" line.

If you are unable to type into the message box because it is hidden and blindTyping is set to false, try to find a button in the stack that will take you to another stack (such as Home) or allow you to quit from HyperCard. From another stack, you can type this command into the message box:

  edit script of stack "My Stack"

In fact, you can hide the entire menu bar (although this does not disable the command-key equivalents to the menu commands). In a script, this can be done with the hide menubar and show menubar commands. You can also press the space bar while holding down the command key to toggle the menu bar on and off.

If you are trapping a menu command, it is a good idea to let the command work normally if a certain key is being held down. For instance, you could write the handler above like this:

  on doMenu theItem
    if theItem is "First" or theItem is "Last" or theItem is "Prev" or theItem is "Next" then
      if the option key is not down then
        beep
        exit doMenu
      end if
      pass doMenu     ----------- VERY IMPORTANT! -------------
    end if
  end doMenu

This version of the handler will trap the four menu items Next, Prev, First, and Last, unless the option key is held down. This makes it easier for you to navigate within the stack while you are developing it.

The "Protect Stack" Menu Item

The "Protect Stack" menu item lets you set an upper limit on the userlevel available in a stack. Since this command is not available in the shortened File menu that appears when the userlevel is set to 1 or 2, it is effective against users who are not very knowledgeable about HyperCard.

However, holding down the command key while pulling down the File menu gives users access to the full-length File menu, even at a lower userLevel. They can then set the upper limit to whatever they prefer. Many users will be aware of this trick, since it appears in the HyperCard User's Manual.

There is an additional disadvantage to using "Protect Stack" to set the userLevel. In some stacks, you may want a script to temporarily increase the userLevel in order to perform some task. For instance, you may have a script that lets users create pop-up notes by clicking a button. To create the field associated with the note, you will need to temporarily set the userLevel at least to level 4 (Authoring). However, if the stack protection is set lower than 4, your script will not be able to override the protection and set the userLevel high enough.

Password Protection

The Protect Stack menu item gives you the option to set a password. Users cannot access the Protect Stack dialog box without the password. You can also set a password to control access to the stack: HyperCard will not let you open the stack at all until you type in the password.

To remove password protection, you need an XCMD such as the Deprotect XCMD (available on many electronic bulletin boards). It is thus a little more difficult (though by no means impossible) to get around password protection. Even in the absence of an XCMD capable of removing a password, you should be aware that the text and scripts of a stack can still be read with a disk editing program such as fEdit or MacSnoop.

Setting the Userlevel

You can set the userLevel in an openStack handler. To be courteous, if you change the userLevel (or any other global setting) within your stack, save the original userlevel in a variable and restore it when the user leaves your stack:

  on openStack
    global theLevel
    put the userlevel into theLevel
  end openStack

  on closeStack
    global theLevel
    set the userlevel to theLevel
  end openStack

Some especially annoying stacks reset the userLevel on idle, presumably to prevent users from simply using the message box to reset the level to their own preferred one. Faced with such a stack, you may need to edit the stack script from outside the stack (using the edit script command described previously) in order to remove the offending command.

Hiding the Message Box

Many stack protection schemes can be thwarted if the user is able to type commands into the message box (for example, the userLevel can be set from the message box). Knowing this, some stack authors have set up elaborate tricks to make certain the message box is inaccessible. For instance, the Message menu item can be trapped and the blindTyping property, which allows users to type commands into the message box even when it's hidden, can be set to false.

This type of stack protection is suitable only if you must be concerned about vandalism by knowledgeable users; for instance, if you are designing a demonstration stack that will be on a trade show or sales floor, accessible to visitors.

Using the cantModify Property

Needs for stack protection can often be met by using the cantModify property introduced in version 1.2 of HyperCard. The cantModify property, when set to true, prevents users from making any changes to the stack. It is unlike simply setting the userLevel to 1 (which also does not allow any changes to be made) in that, if the cantModify property is true, users can still copy text and graphics from the stack. When the cantModify property is set to true, a padlock icon will appear in the stack's menu bar, if the menu bar is visible.

The appeal of the cantModify property is that spectators look at scripts, but the stack is protected. However, like any HyperCard property, the cantModify property can be set from the message box. If your stack is on public display and you want to keep mischievous individuals from altering the stack, you can put the following into the stack's script:

  on idle
    if the cantModify is false then
      set the cantModify to true
      play "DragnetTheme"  --- or any loud, attention-getting sound
      answer "Please don't modify me" with "I'm sorry"
    end if
  end idle

You can modify this stack in spite of the above idle handler. The trick is to recall that idle messages are only sent when the browse tool is selected. If the stack's userLevel is set to 3 (Painting) or higher, you can disable the idle check simply by choosing another tool.

If all else fails, after removing the stack's password protection, you can edit the script from another stack by typing the following line into the message box (from any stack in which you can set the userlevel to 5):

  edit script of stack "Obnoxious Protection"

A Vaccine Handler

Lately there has been a great deal of talk about computer viruses, and there has even been one HyperTalk virus which modified the script of the user's Home stack. While those who write and distribute such things are reprehensible in the extreme, viruses are fortunately very uncommon. More annoying than deliberate viruses are stacks which modify the script of your Home stack without notifying you. For some protection against such stacks, you may want to place this small "vaccine" handler in your Home stack:

  on set
    if the params contains "script" then
      answer the params with "Yes" or "No"
      if it is "No" then exit set
    end if
    pass set
  end set

This handler will notify you whenever a stack attempts to change a script and ask you whether you want to allow the change.


Debugging

Many times, you will find that a carefully coded script isn't doing what you expected it to do. Such times can be very frustrating as you search for the problem, and one of the most frustrating aspects is the fact that you may not know where to start - everything looks as though it should work, and you may have no idea where the problem is. These debugging techniques will not solve the problem, but they will help you narrow down the problem area to the point where you know what's wrong and can work on it.

A problem with your script may show itself at any of several stages in script development. The script may fail to indent properly in the editor - a clue that there is something wrong with the script's syntax. The script may indent properly but fail to run, posting an error message when you try it out. Or the script may run without generating any error messages or other untoward events, but fail to produce the results you expected. At this point, it's time to get out the debugging tools and narrow down the problem to the point where you know exactly what's going wrong and where.

Catching Problems Before They Start

Clearly, the best time to squash a bug is before you put it into your script. To avoid problems at the beginning, you need to have a clear idea of what you want to accomplish with a script and how you are going to do it. For simple tasks, deciding on the technique you'll use is trivially easy. For more complex tasks, though, you may need to take a long, careful look at the task and make certain you understand it completely before you fire up the script editor.

One good rule to follow is to get the script working first, before you attempt to do any speed optimization or other tweaking. When you're sure you understand the task and its solution completely, you'll be able to change the script without causing it to break.

Script Indentation Problems

HyperCard's built-in debugging tools are not nearly as extensive as those provided with some development systems, but they can be quite useful nevertheless. The automatic indentation built in to the script editor checks some parts of the syntax for you. Specifically, it checks for the correct use of HyperTalk keywords. HyperTalk has thirteen keywords:

  on      function  if
  then    else      repeat
  exit    send      pass
  global  next      return
  end

The most common problems that cause the script editor to fail to indent your script correctly are:

If the script is indented correctly, then all the keywords are being used legally. However, it is very important to remember that you can write a handler that is completely correct from a syntactic standpoint, yet has a flaw in the logic that will cause it to produce an undesired result. In other words, just because the editor formats your script correctly does not mean it will do what you want it to do.

Some people prefer to write their own script editor because the built-in editor does not allow you to access the menus and the message box, and has no search-and-replace feature. Most such editors place the script that's being edited in a field. However, if you use such an editor, you can still take advantage of the editor's automatic indentation by placing the following lines in an appropriate script of your custom script editor:

  on closeField
    set the script of this card to card field "My Script"
    ------ use any handy empty script
    put the script of this card into card field "My Script"
  end closeField

In other words, whenever you click outside the field (or press the Tab key), the contents of the field are placed in a script, then brought back out and placed in the field. The trick comes from the fact that anything placed in a script is automatically indented. This handler, then, automatically formats your script.

Runtime Errors

Most scripting problems will not be apparent until you actually try to execute the script. At this point, the handler may completely fail to run, or it may produce a result other than the one you intended. If the script editor's indentation failed to indicate a problem, then your script may be syntactically sound but contain a mistake that prevents the program from producing the desired result. Errors that crop up while a handler is being run are called, logically enough, runtime errors.

If HyperCard finds a statement or instruction it can't interpret during script execution, it displays a dialog box to tell you what it couldn't understand.

[HyperTalk 'can't understand' dialog box]

If the userLevel property is set to 5 (Scripting), there will be a "script" button in the error dialog box; clicking this button opens the problem script, and usually puts the insertion point at the place in the script where HyperCard is having trouble. Occasionally, HyperCard is so confused that it cannot locate the problem, and simply puts the insertion point at the beginning of the script.

The usual causes for the "can't understand" dialog box include spelling errors, using variables you haven't yet put a value into, and other fairly easy-to-correct problems.

Another problem that may cause a run-time error is unlimited recursion. See the previous section on recursion for an explanation of this error and how to avoid it.

If your script keeps running and never seems to stop, you may have inadvertently set up an endless loop. This can usually be traced to a problem with a repeat structure. (Remember, you can always stop any script by typing command-period.)

A script that runs, yet does not produce the desired result, is a tougher nut to crack. One of the most useful principles for dealing with problems of this type is "I got this far." In other words, you want to determine exactly where the script is going off the rails. This part of debugging largely consists of looking behind the scenes to find out whatyour script is really doing.

There are several methods you can use. One of the simplest is to place a beep command at a strategic point in the handler under investigation. If you hear the beep, you know that the handler reached the point in the script where you put the beep command.

Another method makes use of the message box. If you suspect that a variable value is not what you expected, place this line at an opportune point in your script:

  put myVariable

Such lines allow you to keep close track of the values of variables, properties, and anything else you fear might be causing problems. Remember, the whole point of debugging is to narrow down the problem to the point where you can deal with it.

You can also use dialog boxes to announce that the script has reached a certain milestone. The advantage of using dialog boxes is that each dialog must be explicitly dismissed by clicking "OK" before the script can continue; if you directed several messages to the message box, on the other hand, each one would overwrite the previous one, and there might not be time for you to read each one. This makes dialog boxes easier to use when you need to post multiple messages to yourself.

To simplify a complicated handler for debugging purposes, you can temporarily remove some lines by making them into comments. Suppose you have a problem with a handler that computes a name and puts it into a variable, goes to a card by that name, and collects data on the card. you can comment out the part of the handler that computes the name and place a dummy name in the variable. This technique can help you isolate the problem.

Testing

After you think the stack is bug-free, it's time to test it. You can do some of the testing yourself. However, the writer of a program can never test it adequately - you know it too well. The ideal program is one in which no normal user action can cause an error message, but you, as the stack's designer, know it so well that you are likely to automatically avoid performing any action that may cause a problem. Ironically, these blind spots makes it impossible for you to completely test your own work. Find at least one or two people who were not involved in the making of the stack and invite them to do their worst.

If you plan to distribute a stack you've designed, make sure it is tested first with an unmodified copy of the HyperCard application and the Home stack, with a stock system folder. If you have customized your Home stack by putting useful message handlers and functions into it, stacks that call those handlers won't work with another person's Home stack. Some people also place custom resources such as icons and XCMDs into their Home stacks, and these will likewise not be available to other users unless you remember to put them into the stack before you distribute it. If you use any fonts other than the standard set, you should place those in the stack as well. In general, any stack you distribute should be able to stand alone, without requiring special resources in the System file or the Home stack.

It's important to test common user actions for their impact on your scripts. For example, there is a bug in the Home stack distributed with HyperCard 1.01. If you click on the Weekly button and HyperCard cannot find the DateBook stack, it will present a dialog asking you where it is. If you click Cancel instead of opening the stack, the script will complain that it "can't understand goWeekly". GoWeekly is a handler in the DateBook stack; the handler is called in the Weekly button script, after the command to go to the DateBook stack. Of course, if you have not opened the DateBook stack, the HyperTalk interpreter can't find the goWeekly handler, and presents an error message to the understandably-confused user.



You may think that some of these matters are only for "serious stack developers" or those who design HyperCard stacks for a living. However, if any of your stacks will be used by anyone besides you, then you too are (drum roll) a HyperCard developer. You owe it to your users to be careful and attentive. The steps of design, debugging, and testing do not have to be extensive, especially for a simple stack, but they should not be omitted.

As you learn more about HyperCard and HyperTalk, share what you've learned. Join a local user group. If you have a modem, call an electronic bulletin-board service to exchange news and information with other HyperCard users. You will find that the more you share your ideas, the more you'll learn about HyperCard.


Jeanne A. E. DeVoto jaed@jaedworks.com