/* ---------------------------------------------------------- */
/* Tablet -- use the composite tab gif [tablet.gif]           */
/*                                                            */
/* Use as:                                                    */
/*                                                            */
/*   <applet code="Tablet.class" width=100% height=40>        */
/*   <param name=tab1 value="News nrnews.htm">                */
/*   <param name=tab2 value="Documentation nrdocs.htm">       */
/*   <param name=show value="2">                              */
/*   </applet>                                                */
/*                                                            */
/* The 'name' parameters define the data for each tab:        */
/*                                                            */
/*   word 1 -- Text to show on the tab.  Use an underscore    */
/*             ('_') to include a blank.                      */
/*                                                            */
/*   word 2 -- URI to be shown when the tab is clicked.       */
/*             This is relative to the directory in which the */
/*             current document appears.                      */
/*                                                            */
/*   rest   -- (not yet implemented) bubble-help for the tab. */
/*                                                            */
/* The 'show' parameter either gives the number of the tab    */
/* that will be brought to the front (numbered 1-n, from the  */
/* left), or will be 0 (meaning show the lower edge of the    */
/* 'card index').  The default show is 1.                     */
/* For show=0, the height in the applet tag should be 15.     */
/* ---------------------------------------------------------- */
options binary      -- optional; runs a bit faster if set

class Tablet extends Applet

 properties constant -- private

  -- Positions in the 'original' image (proto) of the pieces
  -- that we want; note the two risers must be the same width.
  -- The parts up, down, and downb are extended (repeated) to
  -- form tabs and shoulders.
  -- For each slice we need the start offset (x) and width (w)
  xrise =  0; wrise =14  -- rise   (rise left tab)
  xup   = 14; wup   = 6  -- up     (tab top)
  xdrop = 20; wdrop =14  -- drop   (drop from top to shoulder)
  xdown = 34; wdown = 6  -- down   (flat shoulder)
  xterm = 40; wterm =14  -- term   (right shoulder; termination)
  xstar = 54; wstar =15  -- start  (left shoulder)
  xrism = 69; wrism =14  -- rism   (rise middle)
  xstarb= 83; wstarb=15  -- starb  (start bottom)
  xdownb= 98; wdownb= 6  -- downb  (down bottom)
  xtermb=104; wtermb=14  -- termb  (termination bottom)
  xfin  =128             -- finish offset (= total width)
  -- Heights of the full tabs and 'bottom' pieces
  yproto =40             -- full size pieces
  ybottom=15             -- bottom pieces

 properties private

  tabfont  ='Helvetica'  -- font face for tab text
  tabweight=Font.BOLD    -- weight
  tabcol   =Color.black  -- color
  tabcolhi =Color.blue   -- color, when highlit
  tabcollo =Color.gray   -- color, when current (fronttab)
  tabpoints=int          -- size for desired text height [points]
  tabheight=16           -- desired text height [pels]
  tabbasey =22           -- text baseline
  tabbasex = 0           -- text baseline x adjustment

  shadow=Image           -- the Image to draw (null if no good)
  proto =Image           -- the Image prototype
  wid   =0               -- working tabs width
  hig   =0               -- working tabs height
  xscale=float           -- scale factor in x
  yscale=float           -- scale factor in y

  -- data
  tabs     =int          -- number of tabs
  wtab     =int[]        -- tab widths array
  otab     =int[]        -- tab body offsets array
  fronttab =int 0        -- tab focus number [0->tabs-1]
  mousetab =int(-1)      -- tab under mouse  [0->tabs-1, or -1]
  mouselast=int(-1)      -- tab under mouse  [0->tabs-1, or -1]
  tabword  =String[]     -- String for the tab
  taburl   =String[]     -- URL for the tab
--tabhelp  =String[]     -- help String for the tab [nyi]

 /* ---------------------------------------------------------- */
 /* init is called once the applet is loaded.  It sets up the  */
 /* master 'tabs' image, using pieces from the prototype.      */
 /* Note that if the prototype image used to form the tabs     */
 /* cannot be loaded for some reason, we'll still get the      */
 /* text segments, and they'll work as expected.               */
 /* ---------------------------------------------------------- */
 method init
  s='Copyright (c) IBM, 1997'

  /* Get the parameters */
  -- Determine the most forward, if any
  s=getParameter('show') -- the number of the front tab (default 1)
  if s\=null then do
    i=Integer.parseInt(s, 10)
    if i<0 then signal NumberFormatException
    fronttab=i-1         -- OK, it's valid; -1 will mean bottom-edge
  catch NumberFormatException
    System.out.println('Bad show parameter:' s)
    end

  /* Get the prototype image.  It has a transparent background. */
  /* The prototype is of height yproto. */
  proto=getgif('tablet.gif')       -- get the template
  -- [will be null if not loaded, but we still get Text]

  /* Take quick path if just drawing bottom edge */
  if fronttab<0 then do
    hig=ybottom                    -- bottom edge is less high than full
    wid=getSize.width              -- fixed image size
    shadow=createImage(wid, hig)   -- working image
    draw=shadow.getGraphics        -- for graphics
    draw.setColor(Color.white)     -- background
    draw.fillRect(0, 0, wid, hig)  -- ..
    if proto\=null then drawunder  -- draw using pieces from prototype
     else /* missing image */ do
      draw.setColor(Color.gray)    -- make a default line
      draw.fillRect(0, 2, wid, 4)  -- ..
      end
    return
    end

  /* real tabs */
  -- Count the tabs
  tabs=0
  loop n=1 by 1
    s=getParameter('tab'n)
    if s=null then leave
    tabs=tabs+1
    end
  if tabs=0 then return  -- give up (will display 'no tabs')
  if fronttab>=tabs then fronttab=tabs-1     -- clamp
  -- Parse the tabs
  tabword=String[tabs]
  taburl =String[tabs]
  loop n=1 to tabs
    c=getParameter('tab'n).toCharArray
    /* [This is where we really *need* NetRexx parse, or Rexx.word(x) :-)] */
    -- First word is for tab [we allow multi-word with '_']
    loop i=0 to c.length-1 while c[i] =' '; end   -- i to first non-blank
    loop j=i to c.length-1 while c[j]\=' '        -- j to next blank
      if c[j]=='_' then c[j]=' '                  -- [translate underscore]
      end j
    if j<=i then tabword[n-1]='?'
            else tabword[n-1]=String(c, i, j-i)
    -- Second word is URL
    loop i=j to c.length-1 while c[i] =' '; end   -- i to next non-blank
    loop j=i to c.length-1 while c[j]\=' '; end   -- j to next blank
    if j<=i then taburl[n-1]='?'
            else taburl[n-1]=String(c, i, j-i)
    -- Remainder would be help [currently ignored]
    -- say 'tab'n': "'tabword[n-1]'" "'taburl[n-1]'"'
    end n

  /* Prepare the drawing area, and measure the tab text */
  wid=getSize.width                     -- default image size
  hig=yproto                            -- full height
  shadow=createImage(wid, hig)          -- working image
  draw=shadow.getGraphics               -- for graphics

  -- measure the height of the font, and choose size to match
  draw.setFont(Font(tabfont, tabweight, 36)) -- measure at this size
  h36=draw.getFontMetrics.getAscent     -- height at size '36'
  tabpoints=int(36*(tabheight/h36)+0.5) -- size for required height (round)
  draw.setFont(Font(tabfont, tabweight, tabpoints)) -- set actual font
  fm=draw.getFontMetrics                -- find out about font
  -- say 'Font' tabfont':' tabheight h36 tabpoints fm.getAscent

  -- measure the width of each text segment; save in wtab array
  wtab=int[tabs]                        -- width of each tab body
  otab=int[tabs]                        -- offset of each tab body
  wtotal=0                              -- total of all bodies
  loop t=0 to tabs-1
    otab[t]=wtotal+wrise*(t+1)          -- offset of body start
    wtab[t]=fm.stringWidth(tabword[t])  -- actual width
    wtotal=wtotal+wtab[t]
    -- say 'wtab['t']:' wtab[t]
    end t
  minwid=wtotal+wrise*tabs+wdrop+wterm  -- minimum width needed
  if minwid>wid then do                 -- requested width is wider
    wid=minwid
    shadow=createImage(wid, hig)        -- need new, wider image
    draw=shadow.getGraphics             -- for graphics
    end

  xscale=wid/getSize.width              -- record scale factors
  yscale=hig/getSize.height             -- ..
  -- say 'wid scales:' wid xscale yscale

  draw.setColor(Color.white)            -- background
  draw.fillRect(0, 0, wid, hig)         -- ..

  drawtabs                              -- lay out the tabs

 /* ---------------------------------------------------------- */
 /* Update a tab's text [and front shoulder, if needed].       */
 /* ---------------------------------------------------------- */
 method updatetext(t=int)
  drawtext(otab[t], tabword[t], t=mousetab, t\=fronttab)
  --- draw shoulder in front if not front tab
  if t=fronttab then return
  x=otab[t]
  wdowns=wtab[t]                        -- how many downs to fill
  loop while wdowns>0
    if wdowns>wdown then w=wdown
                    else w=wdowns
    drawslice(x, xdown, w)
    wdowns=wdowns-w
    x=x+w
    end
  return

 /* ---------------------------------------------------------- */
 /* Draw an under-image (the bottom of an index card).         */
 /* ---------------------------------------------------------- */
 method drawunder
  drawslice(0, xstarb, wstarb)               -- start
  x=wstarb
  wdowns=wid-(wstarb+wtermb)                 -- how many downs to fill
  loop while wdowns>0
    if wdowns>wdownb then w=wdownb
                     else w=wdowns
    drawslice(x, xdownb, w)
    wdowns=wdowns-w
    x=x+w
    end
  drawslice(x, xtermb, wtermb)               -- terminator
  this.repaint                               -- ensure onscreen

 /* ---------------------------------------------------------- */
 /* Update all the tab drawings.                               */
 /* ---------------------------------------------------------- */
 method drawtabs
  /* Display all the tabs, from hindmost to front.  Only the front tab
     will have shoulders, added after the tab body. */
  loop t=tabs-1 to 0 by -1
    n=t+fronttab              -- tab to display
    if n>=tabs then n=n-tabs  -- ..
    -- draw riser [note different if tab 0]
    x=otab[n]-wrise
    if n=0 then xr=xrise
           else xr=xrism
    drawslice(x, xr, wrise)
    x=x+wrise
    -- draw uppers
    wups=wtab[n]
    loop while wups>0
      if wups>wup then w=wup
                  else w=wups
      drawslice(x, xup, w)
      wups=wups-w
      x=x+w
      end
    -- draw drop
    drawslice(x, xdrop, wdrop)
    -- draw the text for the tab
    drawtext(otab[n], tabword[n], n=mousetab, n\=fronttab)
    end t
  /* shoulders of the front tab */
  if fronttab>0 then do label leftshoulder
    drawslice(0, xstar, wstar)               -- start
    x=wstar
    wdowns=otab[fronttab]-wrise-x            -- how many downs to fill
    loop while wdowns>0
      if wdowns>wdown then w=wdown
                      else w=wdowns
      drawslice(x, xdown, w)
      wdowns=wdowns-w
      x=x+w
      end
    end leftshoulder
  /* always a right shoulder */
  x=otab[fronttab]+wtab[fronttab]+wdrop      -- right edge of focus tab, +1
  wdowns=wid-wterm-x                         -- how many downs to fill
  loop while wdowns>0
    if wdowns>wdown then w=wdown
                    else w=wdowns
    drawslice(x, xdown, w)
    wdowns=wdowns-w
    x=x+w
    end
  drawslice(x, xterm, wterm)                 -- terminator
  this.repaint                               -- ensure onscreen

 /* ---------------------------------------------------------- */
 /* Draw a vertical slice from the prototype into the shadow.  */
 /* Note for 1.0.x we don't have setClip, so we must keep      */
 /* creating Graphics contexts.                                */
 /* ---------------------------------------------------------- */
 method drawslice(x=int, xfrom=int, wfrom=int)
  if proto=null then return                  -- nothing to draw
  draw=shadow.getGraphics                    -- needed to reset clip
  draw.clipRect(x, 0, wfrom, hig)            -- where to draw
  draw.drawImage(proto, x-xfrom, 0, this)    -- trickily shift the source

 /* ---------------------------------------------------------- */
 /* Draw the text for a tab.                                   */
 /* ---------------------------------------------------------- */
 method drawtext(x=int, s=String, high=boolean, enabled=boolean)
  draw=shadow.getGraphics                    -- needed to reset clip
  draw.setFont(Font(tabfont, tabweight, tabpoints))
  select
    when \enabled then draw.setColor(tabcollo)
    when high     then draw.setColor(tabcolhi)
    otherwise          draw.setColor(tabcol)
    end
  draw.drawString(s, x+tabbasex, tabbasey)

 /* ---------------------------------------------------------- */
 /* The following methods respond to mouse movements and       */
 /* actions.                                                   */
 /* MouseEnter and MouseExit are not called on some platforms, */
 /* but are allowed for in any case.                           */
 /* ---------------------------------------------------------- */
 /* mouseDown takes an action if over a tab */
 method mouseDown(e=Event, x=int, y=int) returns boolean
  t=hit(e, x, y)
  if t<0 then return 0
  if t=fronttab then return 0      -- this one's inactive
  -- don't change the current drawing; browser may be lazy
  do
    this.getAppletContext.showDocument(URL(this.getDocumentBase, taburl[t]))
  catch MalformedURLException
    System.out.println('Bad URL:' taburl[t])
  end
  return 1

 /* Actions to take as mouse moves */
 method mouseMove(e=Event, x=int, y=int) returns boolean
  t=hit(e, x, y)
  if mouselast\=t then do     -- change noted
    mousetab=t
    if mouselast>=0 then
      updatetext(mouselast)   -- will revert old tab
    if t>=0 then
      updatetext(t)           -- will highlight new tab
    mouselast=t
    this.repaint
    end
  return 1

 /* Drag is treated just like Move (redraws button if leaves button) */
 method mouseDrag(e=Event, x=int, y=int) returns boolean
   return mouseMove(e, x, y)
 /* Enter and exit call our mouseMove to ensure known state */
 method mouseEnter(e=Event, x=int, y=int) returns boolean
   return mouseMove(e, x, y)
 method mouseExit(e=Event, x=int, y=int) returns boolean
   return mouseMove(e, x, y)

 /* Returns number of tab/card we are over, or -1 if none */
 method hit(e=Event, x=int, y=int) returns int
   if e=null   then return -1                -- no event
   x=(x*xscale)%1                            -- apply scaling in X
   y=(y*yscale)%1                            -- .. and Y
   if x<5      then return -1
   if x>wid-5  then return -1
   if y<5      then return -1                -- above tabs
   if y>hig-5  then return -1                -- edge detector
   -- next effect isn't very nice; disable
   -- if y>31  then return fronttab          -- below shoulder must be front
   o=0
   loop t=0 to tabs-1
     if t=fronttab then do
       if x>=o then
        if x<o+wrise+wtab[t]+wdrop then return t
       end
      else /* behind tab */ do
       if x>=o+wrise/2 then
        if x<o+wrise+wtab[t]+wdrop/2 then return t
       end
     o=o+wrise+wtab[t]
     end t
   return -1

 /* ---------------------------------------------------------- */
 /* Utility routine to load a GIF and await its arrival.       */
 /* The argument is a URI (relative to the document base).     */
 /* ---------------------------------------------------------- */
 method getgif(gif=String) returns Image
  newimage=getImage(getDocumentBase(), gif)  -- get the image
  tracker=MediaTracker(this)                 -- 'this' is the "observer"
  tracker.addImage(newimage, 0)              -- track image arrival, ID=0
  do
    tracker.waitForID(0)                     -- wait for image 0 to complete
  catch InterruptedException                 -- something stopped us
    return null                              -- can do no more
  end
  -- Good images have useful dimensions, bad images have -1 or 0 in X or Y
  if newimage.getWidth(this) <=0 then return null
  if newimage.getHeight(this)<=0 then return null
  return newimage                            -- looks good

 /* ---------------------------------------------------------- */
 /* paint(g) draws the whole image to size, if available.      */
 /* ---------------------------------------------------------- */
 method update(g=Graphics)                   -- override Applet's update
  paint(g)                                   -- method to avoid flicker
 method paint(g=Graphics)
  if shadow=null
   then g.drawString('[no tabs]', 2, 12)     -- alternative
   else g.drawImage(shadow, 0, 0, getSize.width, getSize.height, this)

