Articles Archive
Articles Search
Director Wiki
 

Hot under the color

August 13, 1998
by James Newton

Hot text is reputedly difficult to do in Director. Newcomers from Authorware and HyperCard often find themselves a little lost when trying to create linked text. The good news is that Director offers you behaviors, so you can have hypertext at the drag-and-drop of a hat.

In this article, I shall show you how to write a behavior that gives you hypertext features for editable text. This means that the end-user can effortlessly create new links to existing fields of text. With a little imagination, you could build a simple hyper-writing environment around my "Hot text" behavior.

If you've never written a behavior before, don't panic. I'll be taking you through the process, step by step, explaining behavior-specific techniques as I go. You'll find that behaviors make a lot of sense.

A demo movie with the full behavior is available for you to study. You can view a shocked version on-line, or download the unprotected version in Mac or PC format.

Hot text expectations

What do the non-programming end-users expect from hypertext?

First of all, they want to be able to see whether a particular chunk of text is hot. So you need to provide some form of visual convention (text color, style, font, or whatever) that indicates this.

They also want to know when the cursor is correctly placed over a hot item. You could do this by altering the text or by altering the cursor, or both.

Finally, when they click on a hot link, they expect a new text to appear. Fast. So we've got to optimize our solution. Remember: text operations on fields can be slow.

And what about the hypertext author (you, or other members of your design team)?

You want to be able to use formatted text. You want to be able to create links as simply as possible. You want to be able to use a group of words (not just single words) to link to a particular text. You want to be able to identify which of your Field Members contain hot text, so that you don't muddle them up with your other (cooler) Field Members.

Designing the behavior

The basic concept of the behavior is very simple: when the user clicks in a certain place in a given field of text, another field appears in its place. You probably already have experience of swapping cast members using a mouseUp handler... and that's just what we'll be doing here, with a few additional handlers to reduce your workload at author-time.

The difficulty comes from defining the "certain place" where the user can click.

Take a moment or two to think about:

Limitations

In order to keep things simple, the "Hot text" behavior makes a few assumptions:

This is not to say that you are obliged to work within these limitations. You're not. It just means that you'll have to adapt the behavior to suit.

Lingo techniques

In building the behavior, you'll discover a variety of techniques:

The basics

If you already have a fair experience of Lingo, you may want to skip this section. (You might miss some interesting tidbits, though).

Strings

A series of words or characters is referred to in programming terms as a string. When Director displays a string in the Message Window, it is always enclosed in double quotation marks or quotes. Like this:


put version
-- "6.0"

(version is a global variable that allows you to check which release of Director your handlers are running in. If you see "5.0" when you type this in the message window, you can stop reading now: you need version 6 to use behaviors). Remember to use the same double quotation marks whenever you create a string yourself.

You can perform a number of operations on strings: cutting them up, pasting them together, counting the number of words, and so on. What we will be doing is testing whether one string appears within another, and if so, where.

To do this we will be using the offset() function. This takes two parameters (let's call them stringToFind and stringToSearch). If it were written in Lingo, it might look something like this (fortunately the native version is much faster):


on lingoOffset stringToFind, stringToSearch
  set firstCharOfOccurence to 1
  set notFound to TRUE
  repeat while notFound
    if stringToSearch starts stringToFind then
      set notFound to FALSE
      -- WILL NOW EXIT REPEAT LOOP
    else
      delete char 1 of stringToSearch
      set firstCharOfOccurence to ¬
        firstCharOfOccurence + 1
      if stringToFind = EMPTY then
        -- NO MORE TEXT TO SEARCH
        exit repeat
      end if
    end if
  end repeat
  if notFound then
    return 0
  else
    return firstCharOfOccurence
  end if
end lingoOffset

Note that the search is not case-sensitive, nor does it check for whole words. Type the following in the Message Window for confirmation:


put offset ("case", "The offset() ¬
  function is not CaSeSensitive")
-- 30

The contains function works in a similar way to offset(), except that it return only TRUE or FALSE, rather than a position. You would use it like this:


put "abcdefghijklm" contains "hi"
-- 1

We're going to have to create our own routine for detecting whole words. (See if you can imagine how we should set about this).

Fields

Director stores editable text in fields. Text stored in fields can be formatted in a variety of ways: font, size style, and so on.

Director can also store text as a string in a variable. However, all formatting is lost in a variable. Besides which, a variable has no screen appearance and thus cannot be edited by the user. Conclusion: we're going to need to work with fields.

The downside of this is that fields, because of their formatting capability, are slow to react. So we'll be converting the formatted field text to plain strings and working with variables behind the scenes.

Fields have a number of properties that you can modify through Lingo. We'll want to decide whether a field should have a scroll bar or not. We'll do this by using the boxType property:


set the boxType of myFieldMember to #fixed
set the boxType of myFieldMember to #scroll

We'll also want to ensure that the field displayed on the screen has a particular shape and size. We'll do this by setting the rect of the Field Member. Director is somewhat idiosyncratic about this. You might expect that you can simply change the rect of the sprite, but this is not the case. Not only that but when you set the rect of the Field Member, you only actually modify its width.

Test this. Create a field and place it on the stage. Type some text into it. Now type this in the Message Window (I'm assuming that the field is member 1: you may have to modify your command):


set the rect of member 1 to rect (0, 0, 16, 16)

If the boxType of the field is set to #adjust (its default value), you will see the on stage sprite blithely ignore the height limitation of 16 pixels. If you set the boxType to #fixed, #limit or #scroll, then the sprite at least will behave as you might expect. (You can alter the boxType without using Lingo by using the Framing pop-up menu in the Field Cast Member Properties dialog: menu Modify >Cast Member >Properties...).

But the Field Member itself is in two minds. Type this into the Message Window:


put the rect of member 1
-- rect(0, 0, 16, 200)

Your values for the fourth dimension may vary. "Bug!" I hear some of you cry. "Feature!" is what the more experienced will reply. The precise value for the height of the member depends in fact on the amount of text in your field. Indeed, we will be using this very feature to determine whether the field requires scroll bar or not.

Lists

Strings are slow to treat, fields are even slower, but lists are very fast. A list is a bit like a map: it shows you where things are, without you actually having to be there. You can run your finger across a map in much less time than it takes to travel the same distance for real.

In fact, a list is more like a magic map. Imagine being able to draw a tiny dot on a world map... and that suddenly a whole new city came into existence. Rub out the dot and the city disappears. Imagine this too: you can make copies of your map and give them to other people. Whenever someone else creates a new city on their map, not only does the city suddenly exist, but it simultaneously appears on your map, and all the other maps too.

In Lingo, a linear list looks something like this:

 [1, "item 2", #item3]
Note:

Property lists

We're going to be a special type of list: the property list.

A property list allows you to give a name to each of its items, like this:


["item 1": "This is the value of ¬
  item 1", #item2: variableContaining ¬
  TheValueOfItem2, 3:#thirdValue]

Note:

Let's practice: type the following into the Message Window. This will:


set myList to [:]
addProp (myList, #property1, "value1")
addProp (myList, "property 2", #value2)
put myList
-- [#property1: "value1", "property 2": #value2]
put getAt (myList, 1)
-- "value1"
put getPropAt (myList, 2)
-- "property 2"
put findPos (myList, "property 2")
-- 2

We'll be using all these functions in our behavior: addProp, getAt, getPropAt, findPos.

Note that findPos() is case sensitive when used on a string. Try this:


put findPos (myList, "PROPERTY 2")
-- Void

No match was found, so findPos() returned Void. (Void} is considered equivalent to zero, FALSE/ and EMPTY), In certain circumstances this is useful, in others it requires a workaround. (You'll find an example in the accompanying movie: the createHotList handler uses a string instead of a list for error checking. Since this is not the main purpose of this article, I won't dwell on it here).

There's another very powerful functions that we will use: findPosNear(). This works only on a sorted list, and it returns the position of the property in the nearest alphanumeric position:


sort myList
put findPosNear (myList, "pro...")
-- 1

Notice that it doesn't care whether the property is a symbol, a string or a number. If you don't sort the list first, findPosNear() is likely to return a number one greater than the number of items in the list:


set anotherList to ["a": 1, "e": ¬
  2, "i": 3, "o": 4, "u": 5]
put findPosNear (anotherList, "b")
-- 6
sort anotherList
put findPosNear (anotherList, "b")
-- 2

Properties

Lists are not the only lingo objects that can have properties. Your behavior script will have a number of properties too. You can think of a script property as a communal pocket that all the handlers can put data into, or can look into see what's already there. Any variables in your behavior that are not declared as properties are strictly personal to the handler that uses them. Director forgets that such local variables ever existed just as soon as the handler has finished executing. You should declare properties at the top of your script, before any handler that refers to them. Here is the declaration of all the properties that you will be using to create your behavior:


property pSprite
property pDimensions
property pFieldMember
property pFullText
property pStandardColor
property pHotColor
property pHotList
property pHotCharList
property pTarget

I'll describe the purpose of each at the appropriate time. I use two conventions that you should note. Your code will work even if you don't follow them, but you will find debugging more difficult.

First, I declare each property on a separate line. You could declare them all in one line like this:


property pSprite, pDimensions, ¬
  pFieldMember, pFullText, ¬
  pStandardColor, pHotColor, ¬
  pHotList, pHotCharList, pTarget

However, this is much less easy to read. More important, it does not let you place a comment after each declaration to explain what the properties purpose is. For example:


property pHotList
-- LIST OF ALL HOT ITEMS IN 
-- THE MOVIE
property pHotCharList
-- ALL OCCURRENCES OF HOT ITEMS 
-- IN THE CURRENT FIELD

Second, each property name starts with "p". When you are reading a handler, it is helpful to know when a variable is local to the handler, and when it is shared with all the others.

All behaviors automatically possess a property which need not be declared: the spriteNum of me. In order to apply the convention across all properties, I prefer to copy the spriteNum into a pSprite property, which I must declare.

A zero by any other name...

Some programming languages require you to explicit about what type of data you are dealing with at all times. Director helpfully allows you to be vague about this. The most striking example is the way the constants Void and FALSE act the same way as the integer zero.

Try this in the Message Window:


if not 0 then beep
if not Void then beep
if not FALSE then beep
put 0 + 1
-- 1
put Void + 1
-- 1
put FALSE + 1
-- 1

Note that the identity between these values is not perfect:


put ilk (Void)
-- #void
put ilk (0)
-- #integer
put ilk (FALSE)
-- #integer

Not only do Void and zero react as if they were FALSE: any positive integer can masquerade as TRUE. As far as integers are concerned, FALSE = 0 and TRUE = NOT FALSE. Try this:

if 911 then beep

You'll find I exploit this feature from time to time. For example:


set positionInList to findPos (aList, aProperty)
if positionInList then
  -- findPos() RETURNED A NON-ZERO
  -- INTEGER: THE PROPERTY EXISTS
  doSomethingWith aProperty
else
  -- findPos() RETURNED void: THE
  -- PROPERTY DOES NOT EXIST
  doSomethingElse
end if

The nitty gritty

Swapping members is not enough

OK, put on your favorite coding music, roll up your sleeves, launch Director and create a new movie. I suggest you set the stageColor to something other than white, so that any fields you use appear contrasted against it. And while you're at it, create a couple of Field Members in the Cast Window. Call them "A" and "B", Type this into field "A":


This is field A.  There's another field called B.

Type this into field "B":

And this is field B.

Now drag field "A" onto the stage. Make sure that it's in channel 1 for the moment. It'll probably be all width and practically no height. Drag one of the corners to make it more square-shaped. Make it a bit too small: make sure that part of the text is hidden. Now type this in the Message Window:


set the member of sprite 1 to member "B"
updatestage

The shape of sprite 1 changes to suit the shape of member "B". We're going to have to create a routine to ensure that the dimensions of the sprite remains constant, regardless of the original dimensions of the field to display.

To do this, create a Score Script Member. Behaviors (in case you hadn't noticed) must be Score Scripts. You can set the #scriptType property of a Script Member by using the menu: Modify > Cast Member > Properties... and selecting "Score" in the Type pop-up menu. Call your behavior "Hot text", and type (or paste) this into it:


property pSprite
property pFieldMember
property pDimensions
on beginSprite me
  set pSprite to the spriteNum of me
  set pFieldMember to the member of sprite pSprite
  set pDimensions to the rect of sprite pSprite
end beginSprite
on mouseUp me
  case the name of pFieldMember of
    "A": set pFieldMember to member "B"
    "B": set pFieldMember to member "A"
  end case
  set the rect of pFieldMember to pDimensions
  if the height of pFieldMember > the ¬
    height of pDimensions then
    set the boxType of pFieldMember to #scroll    
  else
    set the boxType of pFieldMember to #fixed
  end if
  
  set the member of sprite pSprite to pFieldMember
  
end mouseUp

Drag your behavior from the Cast Window and drop it onto the sprite with your field in it.

The on beginSprite handler is triggered automatically when the playback head enters a frame where the sprite first appears. I use it to save the dimensions of the sprite in a shared property called pDimensions.

When you run your movie and click on the sprite, not only does the Field Member change, but the new member's dimensions are fixed to fit the sprite. If the text is too long to appear in full in the available space, the mouseUp handler automatically adds a scroll bar.

Defining the clickable zones

For the moment the user can click anywhere on the field to make the members swap. Let's be more precise about this. Replace the four lines of the case statement with:


set clickedChar to char the mouseChar of ¬
  field pFieldMember
if the number of member clickedChar > 0 then
  set pFieldMember to member clickedChar
else
  exit
end if

What exactly does this do? It looks to see which particular character is under the mouse at the moment that it was clicked. It then looks to see if there is a Cast Member with a corresponding single-character name. If there is, then it displays that Cast Member in the sprite. (If there is no such Cast Member, then the number of member clickedChar will be -1).

This is open to abuse in two ways:

Besides, it forces us to limit our linking items to single characters. However, the technique contains the Seeds of a Good Idea. Let's see how lists can help us out.

Making a list of hot fields

Let's start by making that beginSprite handler work a bit harder. Add this line to it, at the end:

createHotList me 

You'll need to add a new handler:


on createHotList me
  -- CREATES A LIST OF 'HOT' FIELDS
  set pHotList to [:]
  set maxMember to the number of members of castLib 1
  repeat with theMember = 1 to maxMember
    if the type of member theMember <> #field ¬
      then next repeat
    
    set memberName to the name of member theMember
    if word 1 of memberName = "Hot" then
      delete word 1 of memberName
      addProp (pHotList, memberName, member ¬
        theMember)
    end if
  end repeat
  
  -- put pHotList
  -- UNCOMMENT TO SEE pHotList IN 
  -- THE MESSAGE WINDOW
end createHotList

This handler introduces a naming convention: only Field Members whose names start with the word "Hot" (or "hot") will be considered. Your Score Script can be called "Hot text" without causing any upset... because only Cast Members whose type is #field will be added to pHotList.

What does pHotList look like? Start your movie, then look in the Message Window to see:

-- [:]

An empty list. If you don't follow the naming convention for your fields, they won't get added to the list. Change the names of your two fields to: "Hot A" and "Hot B". Now restart your movie:


-- ["A": (member 1 of castLib 1), ¬"B": (member 2 ¬
  of castLib 1)]

Rewrite the first part of the mouseUp handler again, to exploit the list


set clickedChar to char the mouseChar of ¬
  field pFieldMember
set charPosInList to findPos (pHotList, clickedChar)
if charPosInList then
  set pFieldMember to getAt (pHotList, charPosInList)
else
  exit
end if

Now run the movie, and click on the field. What's that? An error alert? In a DOUG article? Outrageous. (If you didn't read the "Basics" section carefully enough, then do so now. Please do not read the following sentence until you have done so). YOU'VE GOT TO DECLARE PROPERTIES IN A BEHAVIOR. If Director is to let you share pHotList between two handlers then you've got to include this line at the beginning of the script:


property pHotList

Try again. Does that work better?

Notice that all the information we want is held in the one list. You don't have to run through the entire cast looking for appropriate Cast Members any more: the createHotList handler did it for you once and for all.

Words

It's a bit limiting to use only a single character as a link. Can't we use whole words, or even several words? The answer is, of course, yes, we can. We just have to be a bit more clever about how we look at what's under the cursor.

First we'll have to learn to identify words.

There is of course a shortcut. Instead of using the mouseChar in the mouseUp handler we could use the mouseWord. This is not only a shortcut. It's also shortsighted. Those who take that approach almost immediately encounter the problem of "How do I create a hot item with more than one word?" The standard answer is: "Use hard spaces". And it works.

So why do I say shortsighted? Because it's a bind to have to use hard spaces in some words and not in others. It means the hypertext author has to stop and think. I'm against making other people think while they create. I'd rather let them just be creative. Unfortunately, this means that we (the programmers) have to think pre-emptively for others. (Well, that's what we get paid for, isn't it?)

So it's time for you to take a break, and come back ready to concentrate. We've come to the tricky bit.

<This portion of Director Online's special presentation of James' Newtons "Quietly One Propety List" was brought to you by Kraft Cheddar. Look for recipe cards in your grocer's dairy section.>

As I was saying: Words

My first assumption, way back at the top of this article was that you would want your links to start and finish with a whole word. I later showed you possible code for a Lingo version of the offset() function. I'll now adapt that code so that it only returns a non-zero result if it finds the stringToFind as whole words. I do this by testing the characters before and after any occurrence of stringToFind to see if they correspond to word delimiters, such as SPACE, QUOTE, comma and so on:


on findWhole stringToFind, stringToSearch
  
  -- HOW LONG IS stringToFind?
  set stringLength to the number ¬
    of chars of stringToFind
  
  -- WHAT PRECEDING AND SUCCEEDING 
  -- CHARACTERS INDICATE A WORD BREAK? 
  set prevWordChars to " /-+=()'"¬
    &RETURN&TAB&QUOTE
  set nextWordChars to prevWordChars&",;:.%*"
  
  set firstCharOfOccurence to 0
  set notFound to TRUE
  repeat while notFound
    set foundPosition to offset ¬
     (stringToFind, stringToSearch)
    if not foundPosition then
      -- stringToFind DOES NOT APPEAR 
      -- IN (WHAT'S LEFT OF) stringToSearch
      exit repeat
    end if
    
    -- IS THE FOUND OCCURENCE A WHOLE WORD?
    if foundPosition = 1 then
      -- NO PRECEEDING CHARACTER
      set wholeWords to TRUE
    else
      -- TEST PRECEDING CHARACTER
      set prevChar to char (foundPosition ¬
        - 1) of stringToSearch 
      set wholeWords to (prevWordChars ¬
        contains prevChar)
    end if
    if wholeWords then
      -- THE OCCURENCE STARTS WITH A 
      -- WHOLE WORD: HOW DOES IT END?
      set nextCharPosition to foundPosition ¬
        + stringLength
      if nextCharPosition > the number of ¬
        chars of stringToSearch then
        -- NO SUCCEEDING CHARACTER
        set wholeWords to TRUE
      else
        -- TEST SUCCEEDING CHARACTER
        set nextChar to char nextCharPosition ¬
          of stringToSearch
        set wholeWords to (nextWordChars ¬
          contains nextChar)
      end if
    end if
    
    set firstCharOfOccurence to firstCharOfOccurence¬
      + foundPosition
    if wholeWords then
      -- THE STRING HAS BEEN FOUND WHOLE
      set notFound to FALSE 
    end if
    
    delete char 1 to foundPosition of ¬
      stringToSearch
  end repeat
  if notFound then
    return 0
  else
    return firstCharOfOccurence
  end if
end

You can use this findWhole() function as it stands in other contexts (See the "Wanted: bug-spotters" section below first, though). With a minor modification to the third last line, it will give us not only the position of the first character, but also the last:

return [firstCharOfOccurence, ¬
  firstCharOfOccurence + stringLength - 1]

We could test if the mouseChar falls between these two limits. If so the mouse is over a link. However, if we rewrite the handler more thoroughly, we can get even more mileage out of it.

Chunks

What we'll do is break the text in each field up into chunks. The first chunk will have no links in it at all. The second chunk will start at the beginning of the first link, the third chunk will start at the beginning of the second link, and so on until we reach the end of the text.


on setHotItems me
  cursor 4 -- WATCH
  
  set prevWordChars to " /-+=()'"¬
   &RETURN&TAB&QUOTE
  set nextWordChars to prevWordChars ¬
   &",;:.%*"
  
  set pHotCharList to [:]
  sort pHotCharList
  set pFullText to field pFieldMember
  -- set the foreColor of pFieldMember¬
    to pStandardColor
  set hotItemCount to count (pHotList)
  repeat with counter = 1 to hotItemCount  
    set hotItem to getPropAt (pHotList,¬
     counter)
    set target  to getAt (pHotList, counter)
    -- REMOVE REFERENCES TO THE FIELD ITSELF
    if the name of pFieldMember = "Hot ¬
       "&hotItem then
      next repeat
    end if
    
    set hotItemLength to the number of ¬
      chars of hotItem
    set firstChar      to 0
    set currentText   to pFullText
    repeat while TRUE
      set hotPosition to offset ¬
        (hotItem, currentText)
      if not hotPosition then
        exit repeat
      end if
      
      -- CHECK FOR WHOLE WORDS
      if hotPosition = 1 then
        set wholeWords to TRUE
      end if
      if not wholeWords  then
        set prevChar to char hotPosition ¬
          - 1 of currentText
        set wholeWords to (prevWordChars ¬
          contains prevChar)
      end if
      if wholeWords  then
        set nextCharPosition to  ¬
           hotPosition + hotItemLength
        set wholeWords to (nextCharPosition ¬
           > length (currentText)
        if not wholeWords  then
          set nextChar to char ¬
            nextCharPosition of currentText
          set wholeWords to (nextWordChars ¬
            contains nextChar)
        end if
      end if
      
      set firstChar to firstChar  + hotPosition
      set lastChar  to firstChar + hotItemLength - 1
      if wholeWords  then
        -- SET THE COLOR OF THE HOT WORD(S)
        -- set the foreColor of char firstChar
        -- to lastChar of field pFieldMember 
        -- to pHotColor
        -- ADD OCCURRENCE TO pHotCharList
        set hotData to [#lastChar: lastChar, ¬
          #target: target]
        addProp (pHotCharList, firstChar, hotData)
      end if
      delete char 1 to hotPosition of currentText
    end repeat
  end repeat
  
  cursor -1
end setHotItems

This handler creates a new list called pHotCharList. (You did remember to declare it as a property, didn't you? What other properties should you declare?)

The setHotItems handler uses two nested repeat loops:

The first indicates the position of the last character of the hot item, and the second indicates the Field Member targeted by the link.

A couple of other points to note:

We don't want to have any circular references in the field to the field itself. The outer repeat loop contains three lines to look for the field's own name in pHotList, and ignore it.

The handler also includes a couple of lines that I have commented out for the moment. These will change the color of the text to make the links visible. Before we can uncomment them, we'll have to study how Director treats text color (tricky bit number 2). Coloring the text may take two lines: the problem is with defining the color.

Add a call to the setHotItems handler in two places: at the end of your beginSprite handler and in your mouseUp handler. We'll now look at how the mouseUp handler will be rewritten to cope with this new technique.

Clicking on a chunk

We need to modify the mouseUp handler (yet) again, so that it can "see" the chunks. Here it is in full as it should now stand (and we still haven't finished with it):


on mouseUp me
  set clickedChar to the mouseChar
  set chunk to findPos (pHotCharList, ¬
    clickedChar)
  if not chunk then
    set chunk to findPosNear (pHotCharList¬
     , clickedChar) - 1
  end if
  if not chunk then
    -- CLICK WAS BEFORE THE 
    -- FIRST HOT ITEM
    exit
  end if
  -- WHERE DOES THE HOT 
  -- PART OF THE CHUNK END?
  set hotData to getAt (pHotCharList, chunk)
  if clickedChar > the lastChar of ¬
    hotData then
    -- THE CLICK WAS NOT IN THE 
    -- HOT PART OF THE CHUNK
    exit
  end if
  set pFieldMember to the target of hotData
  
  set the rect of pFieldMember ¬
    to pDimensions
  if the height of pFieldMember > ¬
    the height of pDimensions then
    set the boxType of pFieldMember ¬
      to #scroll    
  else
    set the boxType of pFieldMember  ¬
     to #fixed
  end if
  set the member of sprite pSprite to ¬
    pFieldMember
  setHotItems me
end mouseUp

Here we use both findPos() and findPosNear() to compare the number of the clicked character with the properties of pHotCharList. (Did you notice that we already sorted pHotCharList in the setHotItems handler so that this would work?)

Why do we need both? Suppose you click directly on the letter "A". You get an immediate match using findPos(). Suppose you click just after the letter "A": findPosNear() will return the character position of the beginning of the following chunk. You want to move back a chunk in the second case, but stay put in the first. Using first findPos() and then findPosNear() lets us distinguish these two cases very simply.

One step forward, no way back

Run your movie now, and click anywhere in field "Hot A". You'll only go to field "Hot B" if you click directly on the letter "B". But once you get to field "Hot B"... there is no way back. Clicking on the first letter of "And"' no longer has any effect.

That's not a problem. All you have to do is add the letter "A" as a whole word to field "Hot B". Remember, one of my assumptions was:

You could avoid doing this by creating a "Back" navigation button. However, that is worth an article in itself.

Unlimited links

There is no longer any need to limit the link anchors to a single character or even to a single word. If you use copy-and-paste, you can even create Field Member names which include RETURN, TAB and other invisible characters. So you can make any chunk of text into a link. In the accompanying movie, I have broken my script up into sections, and used the names of the handlers and properties as the titles of my hot fields.

Coloring text

Coloring text in Director is less than straightforward. The reason is that Director uses any one of three different techniques, depending on the colorDepth of the monitor. And if the monitor is set to 8-bit color (256 colors), then Director changes its mind about what a given color should be called depending on the current palette.

To get round this, the standard technique is to create a field which you reserve for storing colored text in. Whatever the colorDepth or the palette, Director will know what color those words are. If it can't find an exact match in the current palette, Director gives you the nearest thing.

Rewrite your beginSprite handler as follows:


on beginSprite me
  set pSprite to the spriteNum ¬
    of me
  set pFieldMember to the member ¬
    of sprite pSprite
  set pDimensions to the rect of ¬
    sprite pSprite
  set pStandardColor to the forecolor ¬
    of word 1 of field "Text Colors"
  set pHotColor to the forecolor of ¬
    word 2 of field "Text Colors"
  createHotList me
  setHotItems me
end beginSprite

(Anything new to declare?)

This includes two lines to look up the the colors from a field called "Text Colors". You'll need to create this field, enter two words, and color them appropriately. You can now uncomment the two lines in the setHotItems handler... and your behavior is operational.

A cursory point of detail

Operational, but not fully user-friendly. It would be nice to indicate to the user that the mouse is over a hot item. To do this, we'll transfer most of your carefully built mouseUp handler to an on prepareFrame handler. This will tell the behavior regularly where the mouse is. If it happens to be over a link... we'll change the cursor to an open hand. There is a built-in cursor for that: cursor 260 (or you could create your own).

Here's how the contents of the mouseUp handler are carved up:


on prepareFrame me
  if not rollover (pSprite) then exit
  
  set theChar to the mouseChar
  set chunk to findPos (pHotCharList, ¬
    theChar)
  if not chunk then
    set chunk to findPosNear ¬
      (pHotCharList, theChar) - 1
  end if
  if not chunk then
    -- CLICK WAS BEFORE 
    -- THE FIRST HOT ITEM
    forgetTarget
    exit
  end if
  
  -- WHERE DOES THE HOT 
  -- PART OF THE CHUNK END?
  set hotData to getAt (pHotCharList, chunk)
  if theChar > the lastChar of hotData then
    -- THE CLICK WAS NOT IN THE 
    -- HOT PART OF THE CHUNK
    forgetTarget
    exit
  end if
  
  set pTarget to the target of hotData
  cursor 260  -- HAND
  
end prepareFrame

on forgetTarget
  set pTarget to void
  cursor -1  
  -- RETURNS CONTROL 
  -- OF THE CURSOR TO DIRECTOR
end forgetTarget

on mouseUp me
  if voidP (pTarget) then exit
  
  set pFieldMember to pTarget
  
  set the rect of pFieldMember to ¬
    pDimensions
  if the height of pFieldMember ¬
    > the height of pDimensions then
    set the boxType of pFieldMember ¬
      to #scroll    
  else
    set the boxType of pFieldMember ¬
      to #fixed
  end if
  set the member of sprite pSprite ¬
    to pFieldMember
  setHotItems me
end mouseUp

As well as modifying the cursor, the prepareFrame handler sets or forgets a property called pTarget, depending on which link the mouse is current over. If pTarget happens to be void when the mouse is clicked, nothing happens. (You did declare pTarget as a property?)

Tidying loose ends

Your behavior is now fully functional. Congratulations. Was it as difficult to make as you thought it would be?

The accompanying movie takes you on a hypertext tour of my completed behavior. This is different in a number of points of detail. Here are some of the points worth noting:

Working with editable fields

One major difference is that my behavior includes an updateLinks handler. This allows you to work on editable fields: any new links that you type in will appear in the hot color immediately. If you do work on editable fields, you need to press the Command/Control key in order to activate a link. Simply clicking on the text will place the insertion point under the cursor.

Here's the handler:


on updateLinks me
  -- CHECK IF USER HAS 
  --  ADDED NEW LINKS
  if field pFieldMember <> pFullText ¬
    then setHotItems me
  
  -- USE COMMANDKEY TO 
  -- RENDER LINKS ACTIVE
  if the commandDown then
    if the editable of pFieldMember then
      set the editable of pFieldMember to FALSE
    end if
  else if not the editable of pFieldMember then 
    set the editable of pFieldMember to TRUE
  end if
end updateLinks

See if you can incorporate this feature in your own behavior. Where should you place a call to updateLinks? What property will you need to declare, and where will you define its initial value?

Using hypertext on editable fields makes your task at authortime simplicity itself. You simply create a field with the appropriate name for each hot item you intend to use. Then you start up your movie, and type into the on-stage field. You can even add new Field Members as you go (or, better, create a script that lets create a field on the fly. You could write it so that when select a chunk of text and then click on a button, a new field is create with the title "Hot <your selected text here>".

Depending on your project, you could either disable editing at runtime (by making the first field non-editable) or leave it available.

Can you imagine a way to "lock" a given field, so that it can't be edited by mistake? (A clue: one technique would be to exploit the existing naming convention. Remember: all hot Field Members have names that start with "Hot "... or "hot ").

Wanted: bug-spotters

There are a couple of "intentional errors" in the above scripts:

This only occurs the first time the initial field appears. I have left this in the movie on purpose so that you can see for yourself how unseemly it is. (In fact I've been even more malicious: I've cured it, then added a single word to my behavior which sabotages my cure), The problem is caused by the fact that formatting on-screen text is slow. When you jump to another field, the formatting occurs off-stage and fast, before the new field is displayed on screen. When you jump back to the initial field, the formatting also occurs fast off-stage. But in the beginSprite handler, the field is already on the stage. The solution is simple: don't reset the text color from within the beginSprite handler. Can you think of a way to do this?

I have in fact already done it for you. The fixed version of my behavior is what you see in the hyperlinked fields on the stage. You'll have to find the extra word in the script itself and remove it. It shouldn't be that hard to find.

Troubleshooting at authortime

Problem: Hot items don't appear in the appropriate color

Solutions: Ensure that you have colored the second word in your "Text Colors" field

Problem: A particular hot item doesn't appear in the hot color

Possible solutions:

Problem: clicking on a particular hot item takes you to what appears to be the wrong field.

Solution: Check that you haven't created two Field Members with the same name. (My full behavior does this for you).

Where to go from here

Now that you have insight into how Director can handle hypertext, you may want to create a more powerful text navigation engine. Some suggestions are included at the end of the "Hot text description" field in the accompanying movie. If you need help getting any of these ideas into action, drop me a mail at newton / at / planetb.fr.

Acknowledgments

Many thanks to Zav for sharing his D5 solution for hypertext with me, and to Richard Boyle for his pertinent questions.

For James Newton, multimedia development and training is a fairly recent departure. He originally studied to be a journalist, and has won a BBC award for his nature writing. His aptitude for explaining complex subjects to experts and amateurs alike earned him a position as technical communications consultant. He worked with a wide variety of businesses from multinationals to small, highly specialized companies. He now draws on this experience to create interactive presentations and training packages.

James Newton started working with Director 5 in 1997. He wrote many of the behaviors that ship with Director since version 7. James lives in Dunoon, near Glasgow, Scotland. His company, OpenSpark Interactive, is responsible for marketing PimZ OSControl Xtra. When not coding he can be found racing his classic Flying Fifteen around the Holy Loch, making things with his four children, or catching escaped hamsters.

Copyright 1997-2017, Director Online. Article content copyright by respective authors.