Jeanne's World
   House o' HyperCard
       Scripts of the Month

Jeanne's House o' HyperCard:


SlickScripts!
The HyperCard Mailing List Challenge

Back in March, the HyperCard mailing list was discussing the possibility of a scripting contest - the slickest, fastest, crispest script to perform some given task. Jon Pugh came up with the challenge:

An index field which, when pasted onto a card, adds handlers to the background which allow it to get a name for each card (this handler is customized after pasting the field in, but can default to "line 1 of fld 1" or something) and do additions and subtractions from the index as cards are added and deleted, ideally completely automatically. Then it also needs to provide a way of building/rebuilding the index.

The criteria were: functionality, elegance, speed, brevity, clarity, originality, and the gut-reactions of the judges.


After arduous judging by the honorable judging panel of Jon Pugh (since it was his challenge), Catherine Kunicki (since the competition was her idea in the first place), and Jacqueline Landman Gay (whose name needs no parenthesized explanation), this space is proud to present the winning script, written by Jonathon Ashwell. You can either download the stack (8K) with the winning field, or look at the script itself below.

-- AutoIndex, by Jonathan Ashwell
-- Westing Software, www.westinginc.com
--
-- To use:
--
-- 1. Copy and paste this text (the whole thing, including
--    comments) into the script of a new field (probably a
--    scrolling field, but that's up to you)
--
-- 2. Copy and paste that field into one or more backgrounds of a stack
--
-- 3. You're done.
--
--
-- Notes:
--
-- When pasted into the background layer of a stack, a field containing
-- these scripts will append newCard, deleteCard, and doMenu handlers to
-- the background script and do the following:
--
-- 1. Maintain a list of the cards in the same background as the field.
--
-- 2. List the cards by id's or names. The type of view is shown on the
--    first line of the list (List by..., in italics). Clicking on the
--    first line (List by ID or List by Name) toggles the list to the
--    other type. [Note: when listing by Name, unnamed cards are listed
--    as card id xxxx]. You can extend this to list *any* info. See comments
--    in the doListView function below for details.
--
-- 3. Lists of card names are updated on-the-fly when card names are
--    entered or edited.If you have a doMenu handler already in the
--    background, you will have to merge it with the doMenu handler
--    inserted by these scripts. I could circumvent this by assigning a
--    menuMsg to "Card Info..." and putting a handler in the stack script,
--    but I haven't because it's pretty intrusive.
--
-- 4. Clicking on the id or name of a card will take you to that card.
--    (This was not part of the original challenge, and the code in the
--    mouseUp handler can be commented out if desired.) The card name/id
--    does not stay hilited in the list (see comments below for reason).
--    This can be changed by commenting out one line.
--
-- 5. If the cards in the bg are reordered (by being sorted, for example), you can
--    update the list by clicking on the first line of the field (the List by... line)
--
-- 6. The scripts between the "-----------" dividers below are copied to the bg
--    script when this field is pasted into a stack.
--    It would be easy to implement the reverse (remove the AutoIndex scripts from
--    the bg script when the field is deleted by trapping for the deleteField message),
--    but I decided not to clutter up the scripts with this feature.
--
--7.  No global variables were harmed in the making of these scripts.

------------------------------------------------------------------------
-- Slick Script entry AutoIndex, the background scripts
-- by Jonathan Ashwell
-- Westing Software, www.westinginc.com

on newCard
  -- add this new card's name/id to the list
  
  if there is a fld " Index " then -- make sure the field wasn't deleted from the stack
    -- get the number of this cd (in this bg, *not* in the stack)
    get number of this cd - number of cd 1 of this bg + 1
    -- insert it into the Index field (in sequential order)
    
    if "name" is in line 1 of fld " Index " then -- insert cd name
      put return & short name of this cd after line it of fld " Index "
    else -- insert cd id
      put return & short id of this cd after line it of fld " Index "
    end if
    
  end if
  
  pass newCard
end newCard

on deleteCard
  -- remove the card id or name from the list
  
  if the number of cds in this bg > 1 then
    -- don't delete last item from fld if this is the last card in the bg
    
    if there is a fld " Index "  then
      
      if "name" is in line 1 of fld " Index " then -- look for the name
        get offset (return & short name of this cd & return, fld " Index ")
      else -- look for the id
        get offset (return & short id of this cd & return, fld " Index ")
      end if
      
      if it > 0 then
        -- calculate the line number of the entry
        get number of lines in char 1 to it of fld " Index "
        delete line it + 1 of fld " Index "
      end if
      
    end if
  end if
  
  pass deleteCard
end deleteCard

on doMenu what
  -- trap for "Card Info..." so we can detect when a card's name is changed
  
  if what = "Card Info..." then  -- Æ ? may need localization for non-English systems
    
    if there is a fld " Index "  then
      
      if " name" is in line 1 of fld " Index " then
        -- it is list is by name (don't bother to update on-the-fly if list is by id).
        
        -- check for change in card name. If changed, update the list!
        put short name of this cd into oldName
        send "doMenu" && what to HyperCard -- let the user do whatever in the Card Info dialog box
        
        if short name of this cd <> oldName then -- Name was changed. Update the list!
          get offset (return & oldName & return, fld " Index ")
          if it > 0 then
            -- calculate the line number of the entry
            get number of lines in char 1 to it of fld " Index "
            -- replace the old name with the new one.
            put short name of this cd into line it + 1 of fld " Index "
          end if
          
        end if
        
        exit doMenu
      end if
      
    end if
    
  end if
  
  pass doMenu
end doMenu

-- end Slick Script AutoIndex background scripts

------------------------------------------------------------------------

on newField -- executed when the script is pasted into a stack
  
  -- first, make sure we're in a bg. If not, offer to move there for the user.
  -- second, set field properties
  -- third, generate the list of cards by name
  -- fourth, append the scripts listed above to the background script
  
  if word 1 of name of me = "card" then -- Yikes, I was pasted into the card layer!
    beep
    answer "My scripts will not work in the card layer. Should I move myself to the background layer for you?" with "No" or "Yes"
    if it = "Yes" then -- move to bg
      doMenu "Clear Field"  -- remove me
      doMenu "Background"
      doMenu "Paste Field"  -- paste me again, into bg this time
      doMenu "Background"
      exit newField
      -- another newField message has been generated, and will pass through this handler shortly
    else -- remove myself
      doMenu "Cut Field"
      pass newField
    end if
  end if
  
  set name of me to " Index " -- this should be changed (throughout)
  -- in the unlikely event you already have a fld with this name
  
  -- set my display properties. Edit to taste.
  set the textFont of me to "geneva"
  set the textSize of me to "10"
  
  -- don't change these
  set the lockText of me to "true"
  set the dontWrap of me to "true"
  set the autoSelect of me to "true"
  set the sharedText of me to "true"
  
  -- place "view by name" on first line and make list of card names
  doListView "name"
  
  -- copy the newCard, deleteCard, and doMenu scripts above to the background script
  -- see if we're already installed in the bg script
  if offset("-- Slick Script entry AutoIndex", script of this bg) = 0 then
    put script of me into myScript
    
    get offset("-- Slick Script entry AutoIndex, the background scripts", myScript)
    put number of lines in char 1 to it of myScript into startLine
    
    get offset("-- end Slick Script AutoIndex background scripts", myScript)
    put number of lines in char 1 to it of myScript into endLine
    
    put return & return & line startLine to endLine of myScript into slickScripts
    get script of this bg
    put slickScripts after it
    
    if length of it > 29900 then
      beep
      answer "Background script is too long. Can't add AutoIndex scripts!" with "Bother!"
    else
      set script of this bg to it
    end if
  end if
  
  pass newField
end newField

on mouseUp  -- handle a click in the list
  if word 2 of the selectedLine of me = 1 then
    -- clicked on the List by... line
    
    -- change the view (id vs. name)
    if " Name" is in the selectedText of me then
      doListView "id"
    else
      doListView "name"
    end if
    
  else -- go to the card clicked on.
    -- Comment this part out if you *don't* want a mouse click in the
    -- list to go to the selected card
    
    put the scroll of me into saveScroll  -- save scroll value of this list
    get the selectedText of me
    
    lock screen -- so we can set the scroll below without flicker
    
    if " id" is in line 1 of fld " Index " then
      -- it is id view. Go to the card of this id.
      
      go cd id it
      
    else
      -- it is name view. Get the name and go to that cd.
      -- (Note, if two cards in this bg have the same name, you will cycle between them.)
      
      get the selectedText of me
      if word 1 to 2 of it = "card id" then
        go it
      else
        go cd it in this bg
      end if
      
    end if
    
    -- Saving scroll above and restoring it here prevents the list from
    -- "jumping" when going from cd to cd.
    -- You may want to implement something similar in your closeCard and
    -- openCard handlers.
    set the scroll of name of me to saveScroll
    
    -- HI decision here. I elect to *unhilite* the selected card name/id. I do this
    -- because the selected card might *not* be the actual card the user is on
    -- (because one can go from cd to cd many ways other than clicking on this list).
    -- If you want the card name/id to stay hilited, comment out the next line.
    select line 0 of me
    
    unlock screen
  end if
  
  pass mouseUp
end mouseUp

on doListView listType   -- generate the list of card id's or names
  
  -- "listType" should be "id" or "name". Default is "name"
  -- Note that you can list *any* info this way. For example, you could list
  -- the contents of the field "foo" on each card. If you do so, you should also
  -- modify the mouseUp handler so that it goes to the correct cd when you click
  -- on it, such as:
  -- get the selectedText of me
  -- go cd where fld "foo" = it
  
  -- Tip. Build the list in memory (the variable theIndex, here).
  -- Only set up the field at the end. Much faster.
  
  if there is a fld " Index " then
    lock screen
    
    if listType <> "id" then -- list by name
      put "        List by Name" & return into theIndex  -- set first line
      
      -- collect the card names here
      repeat with i = 1 to number of cds in this bg
        set cursor to busy -- bit bit of visual feedback
        put short name of cd i of this bg after theIndex
        put return after theIndex
        -- if you combine these lines, it doesn't work! HC bug.
      end repeat
      
    else -- list by id
      put "        List by ID" & return into theIndex -- set first line
      
      -- collect the card id's here
      repeat with i = 1 to number of cds in this bg
        set cursor to busy -- bit bit of visual feedback
        put short id of cd i of this bg after theIndex
        put return after theIndex
        -- if you combine these lines, it doesn't work! HC bug.
      end repeat
    end if
    
    put theIndex into fld " Index "
    set the textstyle of line 1 of fld " Index " to italic
    
    unlock screen
  end if
end doListView

-- end of AutoIndex scripts

Congratulations!


Jeanne A. E. DeVoto jaed@jaedworks.com