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:
- why simply swapping cast members might not be enough
- how we can use Lingo to define the clickable zones.
Limitations
In order to keep things simple, the "Hot text" behavior makes a few assumptions:
- you want your links to start and finish with a whole word
- you do not need to change the link color to indicate that a link has been followed
- you want to show that the mouse is over a link by modifying the cursor
- all occurrences of the same hot item will lead to the same linked text
- different hot items will lead to different linked texts
- you will ensure that all return links are included manually in each text
- you do not need to mix editable and non-editable linked fields
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:
- How to detect where the mouse is in relation to a the text in a field. There are several similar ways of doing this: we'll concentrate on the most flexible.
- How to modify the cursor. This will let us indicate to the user when the mouse is over an active link.
- How Director handles text color. This will allow us to use color to make the hot links visible, regardless of the current palette or the colorDepth.
- How to find all the occurrences of a string in a field of text. You'll need to do this in order to find the links before you color them. We'll also look at how to determine whether the found string is a part of a word is a whole word in its own right.
- How to resize a field through Lingo, and to provide a scroll bar on the fly if one is needed.
- How property lists can help us speed up operations.
- How to handle authoring errors (not yours: the errors of those you share your behavior with)
- How to organize your handlers for future improvements.
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:
- the square brackets
- the commas separating the items
- that you can use different types of data (numbers, strings, symbols,...)
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:
- the colon which separates the property from its value
- that you can use different types of data (numbers, strings, symbols,...) for both the property and its value.
Let's practice: type the following into the Message Window. This will:
- create an empty property list,
- add two values to it
- show you what a particular value or property is
- tell you where in the list a given property appears
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 enoughOK, 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:
- there is no check on whether the Cast Member in question is a hot Field Member.
- there is no check on whether the character you clicked is appropriate for a link. Indeed, to get back to field "A", you have to click on the first letter of the word "And" in field "B".
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"E 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"E 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 outer loop runs through our original pHotList list, one item at a time, and creates two local variables - hotItem and target - from the property and the value of the current item. It also places a copy of the text of the current field in another local variable: currentText. We need to start with a fresh copy of the entire text each time we search for a new hot item in it.
- The inner loop searches for occurrences of the hotItem in currentText, and adds the position of the first character of each occurrence to pHotCharList. But it then goes a step further: it creates a subordinate list called hotData which contains two properties: #lastChar and #target.
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 will ensure that all return links are included manually in each text
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:
- I wrote my behavior so that it can easily be adapted at a later date. For instance, if I want to use a custom scroll bar instead of the built-in one, or if wish to modify the word-delimiter characters. To make such modifications easier, I have created independent handlers for each feature. That way, I can simply click on the "Go to handler" pop-up menu in the Script Window, and jump directly to the code I need to change.
- I have included a certain amount of error checking, to facilitate authoring. For example, I have deviated the on getPropertyDescriptionList handler so that it warns authors if they drop the behavior on a sprite which doesn't contain a Field Member. I also check for duplicate fields with the same name/hot item.
- My createHotList handler scans all castLibs for Field Members. This is probably overkill in most circumstances, but it does allow you to organize your Field Members as you wish.
- I have commented my code extensively. Next week or in two years time, when I find I need to modify the handlers for a new project, those comments will remind me how my code works.
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:
- There's a tiny loophole in my findWhole() and setHotItems handlers. Will you be the first to spot it? Here's a clue: repetition of a single character. And can you work out a fix (before you look at my complete behavior script in the accompanying movie)?
- When you start my movie, you may notice that the hot text colors vanish, then reappear slowly.
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:
- the Field Member name does not begin with the word "Hot"
- the rest of the Field Member name does not correspond to your hot item
- the occurrence of the hot item is preceded or followed by a character that you did not include in the list of word delimiters
- the hot item iself ends with a word-delimiter character, but the occurrence is not followed by one.
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.
Copyright 1997-2024, Director Online. Article content copyright by respective authors.