#
# $Id: scrollbar.icn,v 1.5 2006-07-09 23:43:08 rparlett Exp $
#
# This file is in the public domain.
#
# Author: Robert Parlett (parlett@dial.pipex.com)
#

package gui
link graphics

$include "guih.icn"

$define MIN_BAR_SIZE 8

#
# Component representing the bar area
#
class BarArea : Component( 
   bar_x,                   #             
   bar_y,                   #             
   bar_w,                   #             
   bar_h                    #             
)
   method display(buffer_flag)
      EraseRectangle(self.cbwin, self.x, self.y, self.w, self.h)
      DrawRaisedRectangle(self.cbwin, self.bar_x, self.bar_y, self.bar_w, self.bar_h)
      self.do_shading(self.cbwin)
      if /buffer_flag then
         CopyArea(self.cbwin, self.cwin, self.x, self.y, self.w, self.h, self.x, self.y)
   end
end

#
# This class provides horizontal and vertical scroll bars.
#
# There are two ways to use a scroll bar.  The first way is to
# set a total_size (represented by the whole bar), a page_size
# (represented by the draggable button) and an increment_size
# (being the amount added/subtracted when the top/bottom
# button is pressed).  The value will then range from zero to
# (total_size - page_size) inclusive.  An initial value must
# be set with the {set_value()} method.
# @example
# @ vb := ScrollBar()
# @ vb.set_pos("85%", "25%")      
# @ vb.set_size(20, "40%")
# @ vb.set_total_size(130)
# @ vb.set_page_size(30)
# @ vb.set_increment_size(1)
# @ vb.set_value(0)
# @ self.add(vb)
#
# Alternatively, a scroll bar can be used as a slider which
# ranges over a given range of values.  In this case, the
# range is set with {set_range()}.  It is still necessary to set
# the increment size and the initial value, as above, but
# page_size and total_size should not be set.
#
# Real numbers as opposed to integers can be used for the
# range settings if desired.
# @example
# @ vb := ScrollBar()
# @ vb.set_pos("85%", "25%")      
# @ vb.set_size(20, "40%")
# @ vb.set_range(2, 25)
# @ vb.set_value(10)
# @ vb.set_increment_size(1)
# @ self.add(vb)
#
# A SCROLLBAR_PRESSED_EVENT occurs whenever the buttons are pressed,
# or after a drag, and a SCROLLBAR_DRAGGED_EVENT occurs during the drag.  The
# value can then be retrieved by get_value().  The fact that
# there are two different event types can be used to reduce the
# number of events which are processed by the user's program - just
# listen for SCROLLBAR_PRESSED_EVENT.
#
class ScrollBar : Component(
   value,                   #             
   page_size,               #                 
   increment_size,          #                      
   total_size,              #                  
   hi,                      #          
   lo,                      #          
   bar_down,                #                
   is_paging,               #
   repeat_delay,            #
   bar_down_offset,         #                       
   bar_area,                #
   b1,                      #          
   b2,                      #          
   is_horizontal_flag,      #                          
   bar_pos,                 #  Orientation independent bar pos
   bar_size,                #                
   bar_area_pos,            #  Orientation independent bararea pos
   bar_area_size,           #                     
   is_range_flag            #
   )

   #
   # Make the scroll bar horizontal (default is vertical).
   #
   method set_is_horizontal()
      return self.is_horizontal_flag := 1
   end

   #
   # Make the scroll bar vertical (the default).
   #
   method clear_is_horizontal()
      return self.is_horizontal_flag := &null
   end

   #
   # Set the total size which the scroll bar area represents.
   # @param x   The total size
   #
   method set_total_size(x)
      self.total_size := x
      self.reconfigure()
   end

   #
   # Return the total size.
   #
   method get_total_size()
      return self.total_size
   end

   #
   # Set the size which the bar in the scroll bar area represents.
   # @param x   The size.
   #
   method set_page_size(x)
      self.page_size := x
      self.reconfigure()
   end

   #
   # Get the page size.
   #
   method get_page_size()
      return self.page_size
   end

   #
   # Get the value.
   # @return The value
   #
   method get_value()
      return self.value
   end

   #
   # Set the value representing the top of the bar in the scroll
   # bar.  The value is forced into range if it is not in range already.
   # @param x   The value.
   #
   method set_value(x)
      if /self.x then
         self.value := x
      else {
         self.move_value(x)
         self.set_pos_from_value()
      }
   end

   #
   # Set the amount to increase the value by when one of the
   # buttons is pressed.
   # @param x   The increment size.
   #
   method set_increment_size(x)
      return self.increment_size := x
   end

   #
   # Set the range of the scroll bar.  The values may
   # be integer or real.
   # @param lo  The lower bound
   # @param hi  The upper bound
   #
   method set_range(lo, hi)
      self.is_range_flag := 1
      self.lo := lo
      self.hi := hi
      self.reconfigure()
   end

   method move_bar_pos(x)
      self.bar_pos := x
      self.bar_pos <:= self.bar_area_pos
      self.bar_pos >:= self.bar_area_pos + self.bar_area_size - self.bar_size
      if /self.is_horizontal_flag then
         self.bar_area.bar_y := self.bar_pos
      else
         self.bar_area.bar_x := self.bar_pos        
      self.bar_area.invalidate()
   end

   method move_value(x)
      self.value := x
      self.value <:= self.lo
      self.value >:= self.hi
   end

   method set_pos_from_value()
      if self.hi ~= self.lo then
         self.move_bar_pos(self.bar_area_pos + integer(((self.get_value() - self.lo) * (self.bar_area_size - self.bar_size)) / (self.hi - self.lo)))
      else
         self.move_bar_pos(self.bar_area_pos)
   end

   method set_value_from_pos()
      if self.bar_area_size ~= self.bar_size then
         self.move_value(self.lo + ((self.hi - self.lo) * (self.bar_pos - self.bar_area_pos)) / (self.bar_area_size - self.bar_size))
      else
         self.move_value(self.lo)
   end

   method handle_press(e)
      if (self.bar_area.bar_x <= &x < self.bar_area.bar_x + self.bar_area.bar_w) & (self.bar_area.bar_y  <= &y < self.bar_area.bar_y + self.bar_area.bar_h) then {
         #
         # Click on bar; set flag and save offset between top of bar and pointer position
         #
         self.bar_down := 1
         if /self.is_horizontal_flag then
            self.bar_down_offset := &y - self.bar_area.bar_y
         else
            self.bar_down_offset := &x - self.bar_area.bar_x
         fire(SCROLLBAR_PRESSED_EVENT, e)     
      } else if (/self.is_horizontal_flag & (self.bar_area.x <= &x < self.bar_area.x + self.bar_area.w) & (self.bar_area.y  <= &y < self.bar_area.bar_y)) | ((self.bar_area.y <= &y < self.bar_area.y + self.bar_area.h) & (self.bar_area.x  <= &x < self.bar_area.bar_x)) then {
         self.move_value(self.get_value() - self.page_size)
         self.set_pos_from_value()
         start_paging(1)
         fire(SCROLLBAR_PRESSED_EVENT, e)     
      } else  if (/self.is_horizontal_flag & (self.bar_area.x <= &x < self.bar_area.x + self.bar_area.w) & ( self.bar_area.bar_y + self.bar_area.bar_h  <= &y <  self.bar_area.y + self.bar_area.h)) | ((self.bar_area.y <= &y < self.bar_area.y + self.bar_area.h) & ( self.bar_area.bar_x + self.bar_area.bar_w  <= &x <  self.bar_area.x + self.bar_area.w)) then {
         self.move_value(self.get_value() + self.page_size)
         self.set_pos_from_value()
         start_paging(2)
         fire(SCROLLBAR_PRESSED_EVENT, e)     
      }
   end

   method handle_release(e)
      if \self.bar_down then {
         #
         # Released; clear flag
         #
         self.bar_down := &null
         fire(SCROLLBAR_PRESSED_EVENT, e)     
      } else if \self.is_paging then
         stop_paging()
   end

   method tick()
      if dispatcher.curr_time_of_day() > self.repeat_delay then {
         if self.is_paging === 1 then 
            self.move_value(self.get_value() - self.page_size)
         else
            self.move_value(self.get_value() + self.page_size)
         self.set_pos_from_value()
         fire(SCROLLBAR_PRESSED_EVENT)     
      }
   end

   method start_paging(n)
      self.is_paging := n
      self.repeat_delay := dispatcher.curr_time_of_day() + self.parent_dialog.repeat_delay
      set_ticker(self.parent_dialog.repeat_rate)
   end

   method stop_paging()
      self.is_paging := &null
      stop_ticker()
   end

   method handle_button_up(ev)
      #
      # Button up clicked
      #
      self.move_value(self.get_value() - self.increment_size)
      self.set_pos_from_value()
      fire(SCROLLBAR_PRESSED_EVENT, ev)
   end

   method handle_button_down(ev)
      #
      # Button down clicked
      #
      self.move_value(self.get_value() + self.increment_size)
      self.set_pos_from_value()
      fire(SCROLLBAR_PRESSED_EVENT, ev)
   end

   method handle_drag(e)
      #
      # Bar dragged; compute new position
      #
      if /self.is_horizontal_flag then
         self.move_bar_pos(&y - self.bar_down_offset)
      else
         self.move_bar_pos(&x - self.bar_down_offset)
      self.set_value_from_pos()
      fire(SCROLLBAR_DRAGGED_EVENT, e)     
   end

   method handle_event(e)
      b1.handle_event(e)
      b2.handle_event(e)
      if e === (&lpress | &rpress | &mpress) then 
         handle_press(e)
      else if e === (&lrelease | &rrelease | &mrelease) then 
         handle_release(e)
      else if \self.bar_down & (e === (&ldrag | &rdrag | &mdrag)) then
         handle_drag(e)
   end

   method display(buffer_flag)
      local W
      W := if /buffer_flag then self.cwin else self.cbwin

      EraseRectangle(W, self.x, self.y, self.w, self.h)

      #
      # Draw rectangle around area within which bar moves
      #
      if /self.is_horizontal_flag then
         DrawRaisedRectangle(W, self.x, self.y + self.w, self.w, self.h - 2 * self.w)
      else
         DrawRaisedRectangle(W, self.x + self.h, self.y, self.w - 2 * self.h, self.h)

      #
      # Display two buttons
      #
      b1.display(buffer_flag)
      b2.display(buffer_flag)
      bar_area.display(buffer_flag)
      self.do_shading(W)
   end

   method reconfigure()
      # Don't do anything if we haven't called resized yet.
      if /self.x then
         return

      if /self.is_range_flag then {
         #
         # Check total size and page size
         #
         if /self.total_size then
            fatal("total size not set")
         if /self.page_size then
            fatal("page size not set")
         if self.page_size <= 0 then
            fatal("invalid page size")
      } else {
         if \self.lo >= \self.hi then
            fatal("invalid range")
      }

      #
      # Check increment size, value
      #
      if /self.increment_size then
         fatal("increment size not set")
      if /self.value then
         fatal("value not set")

      if /self.is_range_flag then {
         #
         # Not a range; compute lo, hi and bar_size.
         #
         self.lo := 0
         if self.total_size > self.page_size then {
            self.hi := self.total_size - self.page_size
            self.bar_size := integer((self.bar_area_size * self.page_size) / self.total_size)
         } else {
            #
            # Total <= page; these settings produce an immovable full size bar.
            #
            self.hi := 0
            self.bar_size := self.bar_area_size 
         }
      } else {
         #
         # Range; set bar size proportional to button size, but leave room if bar_area_size is small.
         #
         self.bar_size := (b1.w_spec * 3) / 2
         self.bar_size >:= self.bar_area_size - 8
      }
      #
      # Ensure bar size in range not less than MIN_BAR_SIZE, but must be within
      # bar_area_size.
      #
      self.bar_size <:= MIN_BAR_SIZE
      self.bar_size >:= self.bar_area_size

      if \self.is_range_flag then {
         #
         # For a slider, we still need the page size for clicks in the bar.
         #
         self.page_size := ((self.hi - self.lo) * self.bar_size) / (0 ~= self.bar_area_size) | 0
      }

      #
      # Set bar height/width according to orientation
      #
      if /self.is_horizontal_flag then
         self.bar_area.bar_h := self.bar_size
      else
         self.bar_area.bar_w := self.bar_size

      self.move_value(self.value)
      self.set_pos_from_value()
   end

   method resize()
      compute_absolutes()

      if /self.is_horizontal_flag then {
         #
         # Compute bar area dimensions
         #
         bar_area.set_pos(BORDER_WIDTH + 2, self.w + BORDER_WIDTH + 2)
         bar_area.set_size(self.w - 2 * (BORDER_WIDTH + 2), self.h - 2 * self.w  - 2 * (BORDER_WIDTH + 2))
         bar_area.resize()

         self.bar_area.bar_x := self.bar_area.x
         self.bar_area_pos := self.bar_area.y
         self.bar_area.bar_w := self.bar_area.w
         self.bar_area_size := self.bar_area.h
   
         #
         # Set button positions
         #
         b1.set_pos(0, 0)
         b1.set_size(self.w, self.w)
         b2.set_pos(0, self.h - self.w)
         b2.set_size(self.w, self.w)
         b1.set_img(img_style("arrow_up"))
         b2.set_img(img_style("arrow_down"))
      } else {
         bar_area.set_pos(self.h + BORDER_WIDTH + 2, BORDER_WIDTH + 2)
         bar_area.set_size(self.w - 2 * self. h - 2 * (BORDER_WIDTH + 2), self.h - 2 * (BORDER_WIDTH + 2))
         bar_area.resize()

         self.bar_area_pos := self.bar_area.x
         self.bar_area.bar_y := self.bar_area.y
         self.bar_area_size := self.bar_area.w
         self.bar_area.bar_h := self.bar_area.h

         b1.set_pos(0, 0)
         b1.set_size(self.h, self.h)
         b2.set_pos(self.w - self.h, 0)
         b2.set_size(self.h, self.h)
         b1.set_img(img_style("arrow_left"))
         b2.set_img(img_style("arrow_right"))
      }

      b1.resize()      
      b2.resize()      

      reconfigure()
   end

   method set_one(attr, val)
      case attr of {
         "is_horizontal" : if test_flag(attr, val) then 
            set_is_horizontal()
         else
            clear_is_horizontal()
         "total_size" : set_total_size(int_val(attr, val))
         "page_size" : set_page_size(int_val(attr, val))
         "increment_size" : set_increment_size(int_val(attr, val))
         "value" : set_value(numeric_val(attr, val))
         "range" : set_range!numeric_vals(attr, val, 2)
         default: self.Component.set_one(attr, val)
      }
   end

   initially(a[])
      self.Component.initially()
      self.b1 := IconButton()
      self.b1.connect(self, "handle_button_up", BUTTON_PRESS_EVENT)
      self.b1.connect(self, "handle_button_up", BUTTON_HELD_EVENT)
      self.b1.toggle_draw_border()
      self.b1.clear_accepts_focus()
      add(self.b1)
      self.b2 := IconButton()
      self.b2.connect(self, "handle_button_down", BUTTON_PRESS_EVENT)
      self.b2.connect(self, "handle_button_down", BUTTON_HELD_EVENT)
      self.b2.toggle_draw_border()
      self.b2.clear_accepts_focus()
      add(self.b2)
      self.bar_area := BarArea()
      add(self.bar_area)
      set_fields(a)
end
