Legacy Documentclose button

Important: The information in this document is obsolete and should not be used for new development.

Previous Book Contents Book Index Next

Inside Macintosh: More Macintosh Toolbox /
Chapter 4 - List Manager / Using the List Manager


Supporting Keyboard Navigation of Lists

This section discusses how your application can support keyboard navigation of lists. In particular, this section first shows how your application can respond to the user's typing to select an item in a text-only list. Second, this section shows how your application can respond to the user's pressing of the arrow keys.

Supporting Type Selection of List Items

To support type selection of list items, your application must keep a record of the characters the user has typed, the time when the user last typed a character, and which list the last typed character affected. For example, the SurfWriter application defines the following four variables to keep track of this information:

VAR
   gListNavigateString:       {current string being searched}
                  Str255;     
   gTSThresh:     Integer;    {ticks before type selection resets}
   gLastKeyTime:  LongInt;    {time in ticks of last click time}
   gLastListHit:  ListHandle; {last list type selection affected}
The gListNavigateString variable stores the current status of the type selection. For example, if the user types 'h' and then 'e' and then 'l' and then 'l' and then 'o', this string should be 'hello'.

The gTSThresh variable stores the number of ticks after which type selection resets. For example, if the user has typed 'hello' but then waits more than this amount of time before typing 'g', the SurfWriter application sets gListNavigateString to 'g', not to 'hellog'. The value of gTSThresh is dependent on the value the user sets for "Delay Until Repeat" in the Keyboard control panel. SurfWriter also resets the type selection if the user begins typing in a different list from the list last typed in. Thus, if the difference between the current tick count and the gLastKeyTime variable is greater than gTSThresh, or if gLastListHit is not equal to the current list, then the SurfWriter application must reset the type selection.

Listing 4-17 shows how the SurfWriter application initializes or resets its type-selection variables.

Listing 4-17 Resetting variables related to type selection

PROCEDURE MyResetTypeSelection;
CONST
   KeyThresh = $18E;    {location of low-memory word}
   kMaxKeyThresh = 120; {120 ticks = 2 seconds}
TYPE
   IntPtr = ^Integer;   {for accessing low memory}
BEGIN
   gListNavigateString := '';          {reset navigation string}
   gLastListHit := NIL;                {remember active list}
   gLastKeyTime := 0;                  {no keys yet hit}
   gTSThresh := 2 * IntPtr(KeyThresh)^;{update type-selection }
                                       { threshold}
   IF gTSThresh > kMaxKeyThresh THEN
      gTSThresh := kMaxKeyThresh;      {set threshold to maximum}
END;
The MyResetTypeSelection procedure defined in Listing 4-17 initializes three of the variables to default values and sets the gTSThresh variable to twice the value of the system global variable KeyThresh, up to a maximum of 120 ticks. By using the same formula as MyResetTypeSelection for computing the type-selection threshold, you make sure your application is consistent with other applications as well as with the Finder. The SurfWriter application calls the MyResetTypeSelection procedure when it starts up and when it wishes to reset the type selection because the type-selection threshold has expired. It also calls the procedure whenever it receives a resume event, because the user might have used the Keyboard control panel, in which case SurfWriter needs to update the value of the type-selection threshold.

Having initialized variables related to type selection, the SurfWriter application needs to respond to appropriate key-down events. Listing 4-18 illustrates an application-defined procedure that does this.

Listing 4-18 Selecting an item in response to a key-down event

PROCEDURE MyKeySearchInList (theList: ListHandle; theEvent: EventRecord);
VAR
   newChar: Char;                      {character to add to search string}
   theCell: Cell;                      {cell containing found string}
BEGIN
   newChar := CHR(BAnd(theEvent.message, charCodeMask));
   IF (gLastListHit <> theList) OR 
            (theEvent.when - gLastKeyTime >= gTSThresh) OR
            (Length(gListNavigateString) = 255) THEN
      MyResetTypeSelection;
   gLastListHit := theList;            {remember list keyed in}
   gLastKeyTime := theEvent.when;      {record time of key-down event}
                                       {set length of string}
   gListNavigateString[0] := Char(Length(gListNavigateString) + 1);
                                       {add character to string}
   gListNavigateString[Length(gListNavigateString)] := newChar;

   SetPt(theCell, 0, 0);
   IF LSearch(@gListNavigateString[1], Length(gListNavigateString),
               @MyMatchNextAlphabetically, theCell, theList) THEN
   BEGIN
                                       {deselect all cells but new cell}
      MySelectOneCell(theList, theCell);     
                                       {make sure new selection is visible}
      MyMakeCellVisible(theList, theCell); 
   END;
END;
The MyKeySearchInList procedure defined in Listing 4-18 first updates variables related to type selection. Then it searches through the list for a cell containing the current search string or for the next cell alphabetically. It searches using the LSearch function in conjunction with a custom match function defined in Listing 4-15 on page 4-34. The procedure also uses the MySelectOneCell procedure defined in Listing 4-9 on page 4-27 and the MyMakeCellVisible procedure defined in Listing 4-10 on page 4-28.

Note
If your compiler enforces range checking, you may need to disable it before using the code in Listing 4-18, because the code accesses the length byte of a string directly. See your development system's documentation for more information on range checking.

Supporting Arrow-Key Navigation of Lists

This section discusses how your application can support the use of arrow keys to move the current selection or to extend the current selection using a simple extension algorithm. For information on implementing a more complex anchor algorithm for extending the selection, read this section and then the next section, beginning on page 4-43.

The following constants define the ASCII character codes for the various arrow keys. These ASCII values for these keys are the same for U.S. and international keyboards.

CONST
   kLeftArrow     = Char(28);             {move left}
   kRightArrow    = Char(29);             {move right}
   kUpArrow       = Char(30);             {move up}
   kDownArrow     = Char(31);             {move down}
To support both the moving of a selection (the user's pressing an arrow key without pressing the Shift key) and the extending of a selection (the user's pressing of an arrow key while pressing the Shift key), your application needs to define a routine that computes a new selection location given an old one. For example, if the user presses Command-Left Arrow, the routine should find the cell as far to the left of the first currently selected cell as possible. Listing 4-19 illustrates an application-defined procedure that does this.

Listing 4-19 Determining the location of a new cell in response to an arrow-key event

PROCEDURE MyFindNewCellLoc 
            (theList: ListHandle; oldCellLoc: Cell;
             VAR newCellLoc: Cell; keyHit: Char;
             moveToExtreme: Boolean);
VAR
   listRows, listColumns: Integer;     {list dimensions}
BEGIN
   WITH theList^^.dataBounds DO
   BEGIN
      listRows := bottom - top;        {number of rows in list}
      listColumns := right - left;     {number of columns in list}
   END;
   newCellLoc := oldCellLoc;
   IF moveToExtreme THEN
      CASE keyHit OF
         kUpArrow: 
            newCellLoc.v := 0;               {move to row 0}
         kDownArrow: 
            newCellLoc.v := listRows - 1;    {move to last row}
         kLeftArrow: 
            newCellLoc.h := 0;               {move to column 0}
         kRightArrow: 
            newCellLoc.h := listColumns - 1; {move to last column}
      END
   ELSE
      CASE keyHit OF
         kUpArrow: 
            IF oldCellLoc.v <> 0 THEN
               newCellLoc.v := oldCellLoc.v - 1;   {row up}
         kDownArrow: 
            IF oldCellLoc.v <> listRows - 1 THEN
               newCellLoc.v := oldCellLoc.v + 1;   {row down}
         kLeftArrow: 
            IF oldCellLoc.h <> 0 THEN
               newCellLoc.h := oldCellLoc.h - 1;   {column left}
         kRightArrow: 
            IF oldCellLoc.h <> listColumns - 1 THEN
               newCellLoc.h := oldCellLoc.h + 1;   {column right}
      END;
END;
The MyFindNewCellLoc procedure defined in Listing 4-19 computes the coordinates of the cell referenced by the newCellLoc parameter based on the coordinates of the oldCellLoc parameter and the direction of the arrow key pressed. The oldCellLoc parameter contains the coordinates of the first or last cell in a selection, depending on which arrow key was pressed. The behavior of MyFindNewCellLoc also depends on the value passed in the moveToExtreme parameter. For example, if the user pressed the Command key while pressing an arrow key, the SurfWriter application passes TRUE; otherwise, it passes FALSE. If moveToExtreme is TRUE, then MyFindNewCellLoc returns in newCellLoc a cell that is as far as possible from the cell specified in oldCellLoc. Otherwise, it returns a cell that is within one cell of oldCellLoc. If a cell cannot be moved in the direction specified by the arrow key, newCellLoc is equivalent on exit to oldCellLoc.

Having defined the MyFindNewCellLoc procedure, it is easy to move or extend a selection in response to an arrow-key event. Listing 4-20 illustrates an application-defined procedure that moves the selection in response to the user's pressing an arrow key without pressing the Shift key.

Listing 4-20 Moving the selection in response to an arrow-key event

PROCEDURE MyArrowKeyMoveSelection (theList: ListHandle;
                                   keyHit: Char; 
                                   moveToExtreme: Boolean);
VAR
   currentSelection:       Cell;
   newSelection:           Cell;
BEGIN
   IF MyGetFirstSelectedCell(theList, currentSelection) THEN
   BEGIN
      IF (keyHit = kRightArrow) OR (keyHit = kDownArrow) THEN
         {find last selected cell}
         MyGetLastSelectedCell(theList, currentSelection);
      {move relative to appropriate cell}
      MyFindNewCellLoc(theList, currentSelection,
                       newSelection, keyHit, moveToExtreme);
      {make this cell the selection}
      MySelectOneCell(theList, newSelection);
      {make sure new selection is visible}
      MyMakeCellVisible(theList, newSelection);
   END;
END;
The MyArrowKeyMoveSelection procedure defined in Listing 4-20 calls the MyFindNewCellLoc procedure defined in Listing 4-19 to find the coordinates of a cell to select. It computes the coordinates of that new cell relative to the first selected cell if the user pressed a Left Arrow or Up Arrow key; otherwise, it computes the coordinates of the new cell relative to the last selected cell. After computing the coordinates of the new cell, MyArrowKeyMoveSelection selects it by calling routines defined in
Listing 4-9 and Listing 4-10.

Listing 4-21 illustrates an application-defined procedure that extends the selection in response to the user's pressing an arrow key while pressing the Shift key.

Listing 4-21 Extending the selection in response to an arrow-key event

PROCEDURE MyArrowKeyExtendSelection (theList: ListHandle;
                                    keyHit: Char; 
                                    moveToExtreme: Boolean);
VAR
   currentSelection: Cell;
   newSelection:     Cell;
BEGIN
   IF MyGetFirstSelectedCell(theList, currentSelection) THEN
   BEGIN
      IF (keyHit = kRightArrow) OR (keyHit = kDownArrow) THEN
                              {find last selected cell}
         MyGetLastSelectedCell(theList, currentSelection);
                              {move relative to appropriate cell}
      MyFindNewCellLoc(theList, currentSelection,
                       newSelection, keyHit, moveToExtreme);
                              {add a new cell to the selection}
      IF NOT LGetSelect(FALSE, newSelection, theList) THEN
         LSetSelect(TRUE, newSelection, theList);
                              {make sure new selection is visible}
      MyMakeCellVisible(theList, newSelection);
   END;
END;
The MyArrowKeyExtendSelection procedure defined in Listing 4-21 works just
like the MyArrowKeyMoveSelection procedure defined in Listing 4-20, but it does not deselect all other cells besides the newly selected cell.

Listing 4-22 shows an application-defined procedure that takes advantage of the
code listings provided in this section. The SurfWriter application calls the procedure in
Listing 4-22 every time it receives an arrow-key event that affects a list.

Listing 4-22 Processing an arrow-key event

PROCEDURE MyArrowKeyInList (theList: ListHandle; theEvent: EventRecord;
                           allowExtendedSelections: Boolean);
BEGIN
   IF (NOT allowExtendedSelections) OR 
         (BAnd(theEvent.modifiers, shiftKey) = 0) THEN
      MyArrowKeyMoveSelection(theList, 
                              CHR(BAnd(theEvent.message, charCodeMask)),
                              BAnd(theEvent.modifiers, cmdKey) <> 0)
   ELSE
      MyArrowKeyExtendSelection(theList, 
                                CHR(BAnd(theEvent.message, charCodeMask)),
                                BAnd(theEvent.modifiers, cmdKey) <> 0);
END;
The MyArrowKeyInList procedure defined in Listing 4-22 takes three parameters, the third of which is a Boolean variable that indicates whether the application supports the use of Shift-arrow key combinations to extend the current selection. If the application does support this and the user held down the Shift key, the MyArrowKeyInList procedure calls the procedure in Listing 4-21 to extend the selection. Otherwise, it calls the procedure in Listing 4-20 to move the selection. Either way, it checks the status of the Command key to determine whether the appropriate procedure should move as far in the direction of the arrow key as possible before selecting a new cell.

Supporting the Anchor Algorithm for Extending Lists With Arrow Keys

This section summarizes how your application can support the anchor method for extending lists with arrow keys. Implementing this method takes a lot of work, but the extra work may pay off if you expect many users of your application's lists to make range selections or if your application uses multicolumn lists. For a comparison between the anchor algorithm and the extension algorithm illustrated in the previous section, see "Extension of a Selection With Arrow Keys" on page 4-10.

To support the anchor algorithm, your application must keep track of several types of information between Shift-arrow key events. Most importantly, your application must store information about which cell in a list is the anchor cell and which cell is the moving cell. In response to a Shift-arrow key event, your application should change the location of the moving cell. It should then highlight all cells in the rectangle whose corners are the anchor cell and the moving cell. This permits the user to use several consecutive Shift-arrow key combinations to move a rectangular range of cells around the anchor cell.

Your application must thus save the location of the anchor cell the first time the user uses a Shift-arrow key combination to affect a certain rectangular range of cells. For example, if the user presses Shift-Right Arrow and the user has not before used a Shift-arrow key combination, then your application should store as the anchor cell the upper-left cell in the rectangular range of cells to be affected. The moving cell is then one cell to the right of what was the lower-right corner of this range.

Your application can determine what rectangular range of cells a Shift-arrow key combination is meant to affect by using the LLastClick function, which returns the coordinates of the last cell that was clicked. (If your application relies on this function, it must always update the lastClick field of the list record in response to keyboard selection of any list item, since keyboard selection of a list item is functionally equivalent to clicking.) Your application must check the selection status of adjacent cells to find as big a rectangular range of selected cells surrounding this cell as possible.

Your application can check whether a Shift-arrow key event is affecting a new range of cells simply by checking the clikTime field of the list record. (Your application must thus also update this field in response to keyboard selection of any list item.) If the last click time changes between Shift-arrow key events, your application knows that the user has clicked the list or used the keyboard to change the selection. In this case, your application must compute a new anchor cell and moving cell based on the LLastClick function and the direction of the arrow key pressed. Otherwise, your application can keep the same anchor cell, move the moving cell in the direction specified by the arrow key, and highlight cells in the rectangular range of the anchor cell and the moving cell.

In summary, if your application is to support the anchor algorithm for extending a list selection, it must keep track of an anchor cell, a moving cell, and the time of the last click in a list. (Your application might store a handle to a relocatable block containing this information in the userHandle field of the list record.) Whenever a Shift-arrow key event is meant to affect a new range of cells, your application updates all three of these variables. Otherwise, it only changes the coordinates of the moving cell from one Shift-arrow key event to the next.


Previous Book Contents Book Index Next

© Apple Computer, Inc.
6 JUL 1996