#
# $Id: slider.icn,v 1.2 2004/09/18 18:44:58 rparlett Exp $
#
# This file is in the public domain.
#
# Author: Robert Parlett (parlett@dial.pipex.com)
#

package gui
link graphics

$include "guih.icn"

# These values refer to a horizontal slider, the logic is backwards for a vertical one.
$define SLIDER_W 31
$define SLIDER_H 19
$define INNER_H 4
$define TICK_H 10

#
# Component representing the slider area
# @p
class SliderArea : Component(inner_x, inner_y, inner_w, inner_h, slider_x, slider_y, slider_w, slider_h) 
   method display(buffer_flag)
      EraseRectangle(self.cbwin, self.x, self.y, self.w, self.h)
      DrawSunkenRectangle(self.cbwin, self.inner_x, self.inner_y, self.inner_w, self.inner_h)
      EraseRectangle(self.cbwin, self.slider_x, self.slider_y, self.slider_w, self.slider_h)
      DrawRaisedRectangle(self.cbwin, self.slider_x, self.slider_y, self.slider_w, self.slider_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 component is a slider between two set values, with optional tick marks
# and labels.
#
# @example
# @ s := Slider()
# @ s.set_pos("85%", "25%")      
# @ s.set_size(, "40%") # Width defaults for a vertical slider
# @ s.set_range(0,5)
# @ s.set_value(2)
# @ s.set_ticks(1)
# @ s.set_labels(1)
# @ s.set_snaps(1)
# @ self.add(s)
#
class Slider : Component(
   value,                   
   slider_down,
   slider_down_offset,
   slider_area,
   slider_pos,
   slider_area_pos,
   slider_area_size,
   is_horizontal_flag,                                
   discrete_vals,
   ticks,
   labels,
   snaps,
   ticks_pos,
   labels_pos,
   is_paging,
   increment_size,
   repeat_delay,            
   hi,                                
   lo                                 
)

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

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

   #
   # Configure so that on release after a drag, the value will snap to 
   # the nearest multiple of n.
   #
   method set_snaps(n)
      return self.snaps := n
   end

   #
   # Draw ticks at the given interval
   #
   method set_ticks(n)
      return self.ticks := n
   end

   #
   # Draw labels at the given interval
   #
   method set_labels(n)
      return self.labels := n
   end

   #
   # Set the increment_size to increment on a click in the slider area; default is 1.
   # @param x   The increment_size
   #
   method set_increment_size(x)
      self.increment_size := x
      self.invalidate()
   end

   #
   # Get the increment_size
   #
   method get_increment_size()
      return self.increment_size
   end

   #
   # Set the current value
   #
   method set_value(x)
      if /self.x then
         self.value := x
      else {
         self.move_value(x)
         self.set_pos_from_value()
      }
   end

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

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

   method set_pos_from_value()
      if self.hi ~= self.lo then
         self.move_slider_pos(self.slider_area_pos + integer(((self.get_value() - self.lo) * self.slider_area_size / (self.hi - self.lo))))
      else
         self.move_slider_pos(self.slider_area_pos)
   end

   method set_value_from_pos()
      if self.slider_area_size ~= 0 then {
         # For discrete vals, an adjustment is made so that, for example
         # the ranges where the cursor set the position might be
         # 0     1     2     3
         # 0001111112222223333
         # Without this adjustment 0 would only be in the leftmost position, which for
         # small ranges is unattractive.  For non-discrete ranges this behaviour is
         # not appropriate.
         if \self.discrete_vals then
            self.move_value(self.lo + ((self.slider_area_size / 2) + (self.hi - self.lo) * (self.slider_pos - self.slider_area_pos)) / self.slider_area_size)
         else
            self.move_value(self.lo + ((self.hi - self.lo) * (self.slider_pos - self.slider_area_pos)) / self.slider_area_size)
      } else
         self.move_value(self.lo)
   end

   method move_slider_pos(x)
      self.slider_pos := x
      self.slider_pos <:= self.slider_area_pos
      self.slider_pos >:= self.slider_area_pos + self.slider_area_size
      if /self.is_horizontal_flag then
         self.slider_area.slider_y := self.slider_pos - SLIDER_W / 2
      else
         self.slider_area.slider_x := self.slider_pos - SLIDER_W / 2
      self.slider_area.invalidate()
   end

   #
   # Set the range of the slider.  The values may
   # be integer or real.
   #
   # @param lo  The lower bound
   # @param hi  The upper bound
   #
   method set_range(lo, hi)
      self.lo := lo
      self.hi := hi
      if type(self.hi) == type(self.lo) == "integer" then
         self.discrete_vals := 1
      else
         self.discrete_vals := &null
      self.reconfigure()
   end

   method handle_press()
      if (self.slider_area.slider_x <= &x < self.slider_area.slider_x + self.slider_area.slider_w) & (self.slider_area.slider_y  <= &y < self.slider_area.slider_y + self.slider_area.slider_h) then {
         #
         # Click on slider; set flag and save offset between top of slider and pointer position
         #
         self.slider_down := 1
         if /self.is_horizontal_flag then
            self.slider_down_offset := &y - self.slider_pos
         else
            self.slider_down_offset := &x - self.slider_pos
         fire(SLIDER_PRESSED_EVENT, e)     
      } else if (/self.is_horizontal_flag & (self.slider_area.x <= &x < self.slider_area.x + self.slider_area.w) & (self.slider_area.y  <= &y < self.slider_area.slider_y)) | ((self.slider_area.y <= &y < self.slider_area.y + self.slider_area.h) & (self.slider_area.x  <= &x < self.slider_area.slider_x)) then {
         self.move_value(self.get_value() - self.increment_size)
         self.set_pos_from_value()
         start_paging(1)
         fire(SLIDER_PRESSED_EVENT, e)     
      } else  if (/self.is_horizontal_flag & (self.slider_area.x <= &x < self.slider_area.x + self.slider_area.w) & ( self.slider_area.slider_y + self.slider_area.slider_h  <= &y <  self.slider_area.y + self.slider_area.h)) | ((self.slider_area.y <= &y < self.slider_area.y + self.slider_area.h) & ( self.slider_area.slider_x + self.slider_area.slider_w  <= &x <  self.slider_area.x + self.slider_area.w)) then {
         self.move_value(self.get_value() + self.increment_size)
         self.set_pos_from_value()
         start_paging(2)
         fire(SLIDER_PRESSED_EVENT, e)     
      }
   end

   method handle_release()
      if \self.slider_down then {
         #
         # Released; clear flag
         #
         self.slider_down := &null
         if \self.snaps then {
            self.value +:= self.snaps / 2
            self.set_value(self.value - (self.value % snaps))
         }
         fire(SLIDER_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.increment_size)
         else
            self.move_value(self.get_value() + self.increment_size)
         self.set_pos_from_value()
         fire(SLIDER_PRESSED_EVENT, e)     
      }
   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_drag()
      #
      # Slider dragged; compute new position
      #
      if /self.is_horizontal_flag then
         self.move_slider_pos(&y - self.slider_down_offset)
      else
         self.move_slider_pos(&x - self.slider_down_offset)
      self.set_value_from_pos()
      fire(SLIDER_DRAGGED_EVENT, e)     
   end

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

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

   method resize()
      local need_l, need_t

      if /self.is_horizontal_flag then {
         # Work out the amount needed for the labels and ticks (if any)
         need_l := need_t := 0
         if \self.labels then
            need_l := TextWidth(self.cwin, self.hi) + 8
         if \self.ticks then {
            need_t := TICK_H + 5
         }

         if /self.draw_border_flag then {
            /self.w_spec := need_l + need_t + SLIDER_H
            compute_absolutes()
            ticks_pos := self.x + SLIDER_H + 3
            labels_pos := self.x + SLIDER_H + need_t + 3
            slider_area.set_pos(0, 0)
            slider_area.set_size(SLIDER_H, self.h)
         } else {
            /self.w_spec := need_l + need_t + SLIDER_H + 2 * BORDER_WIDTH
            compute_absolutes()
            ticks_pos := self.x + BORDER_WIDTH + SLIDER_H + 3
            labels_pos := self.x + BORDER_WIDTH + SLIDER_H + need_t + 3
            slider_area.set_pos(BORDER_WIDTH, BORDER_WIDTH)
            slider_area.set_size(SLIDER_H, self.h - 2 * BORDER_WIDTH)
         }
         slider_area.resize()
         slider_area.inner_x := slider_area.x + slider_area.w / 2 - INNER_H  / 2
         slider_area.inner_y := slider_area.y + SLIDER_W / 2
         slider_area.inner_w := INNER_H
         slider_area.inner_h := slider_area.h - SLIDER_W
         slider_area.slider_x := slider_area.x
         slider_area.slider_h := SLIDER_W
         slider_area.slider_w := SLIDER_H
         slider_area_pos := slider_area.inner_y
         slider_area_size := slider_area.inner_h
      } else {
         # Work out the amount needed for the labels and ticks (if any)
         need_l := need_t := 0
         if \self.labels then
            need_l := WAttrib(self.cwin, "fheight") + DEFAULT_TEXT_Y_SURROUND
         if \self.ticks then
            need_t := TICK_H + 5

         if /self.draw_border_flag then {
            /self.h_spec := need_l + need_t + SLIDER_H
            compute_absolutes()
            ticks_pos := self.y + SLIDER_H + 3
            labels_pos := self.y + SLIDER_H + need_t + need_l / 2
            slider_area.set_pos(0, 0)
            slider_area.set_size(self.w, SLIDER_H)
         } else {
            /self.h_spec := need_l + need_t + SLIDER_H + 2 * BORDER_WIDTH
            compute_absolutes()
            ticks_pos := self.y + BORDER_WIDTH + SLIDER_H + 3
            labels_pos := self.y + BORDER_WIDTH + SLIDER_H + need_t + need_l / 2
            slider_area.set_pos(BORDER_WIDTH, BORDER_WIDTH)
            slider_area.set_size(self.w - 2 * BORDER_WIDTH, SLIDER_H)
         }

         slider_area.resize()
         slider_area.inner_x := slider_area.x + SLIDER_W  / 2
         slider_area.inner_y := slider_area.y + slider_area.h / 2 - INNER_H  / 2
         slider_area.inner_h := INNER_H
         slider_area.inner_w := slider_area.w - SLIDER_W
         slider_area.slider_y := slider_area.y
         slider_area.slider_w := SLIDER_W
         slider_area.slider_h := SLIDER_H
         slider_area_pos := slider_area.inner_x
         slider_area_size := slider_area.inner_w
      }

      reconfigure()
   end

   method draw_ticks(W)
      local i, xp, yp
      if /self.is_horizontal_flag then {
         i := self.lo
         while i <= self.hi do {
            yp := self.slider_area_pos + integer(((i - self.lo) * (self.slider_area_size) / (self.hi - self.lo)))
            DrawLine(W, ticks_pos, yp, ticks_pos + TICK_H, yp)
            i +:= ticks
         }
      } else {
         i := self.lo
         while i <= self.hi do {
            xp := self.slider_area_pos + integer(((i - self.lo) * (self.slider_area_size) / (self.hi - self.lo)))
            DrawLine(W, xp, ticks_pos, xp, ticks_pos + TICK_H)
            i +:= ticks
         }
      }
   end

   method draw_labels(W)
      local i, xp, yp
      if /self.is_horizontal_flag then {
         i := self.lo
         while i <= self.hi do {
            yp := self.slider_area_pos + integer(((i - self.lo) * (self.slider_area_size) / (self.hi - self.lo)))
            left_string(W, labels_pos, yp, i)
            i +:= labels
         }
      } else {
         i := self.lo
         while i <= self.hi do {
            xp := self.slider_area_pos + integer(((i - self.lo) * (self.slider_area_size) / (self.hi - self.lo)))
            center_string(W, xp, labels_pos, i)
            i +:= labels
         }
      }
   end

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

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

      if \self.draw_border_flag then
         DrawSunkenRectangle(W, self.x, self.y, self.w, self.h)

      if \self.labels then
         draw_labels(W)

      if \self.ticks then
         draw_ticks(W)

      slider_area.display(buffer_flag)

      self.do_shading(W)
   end

   method set_one(attr, val)
      case attr of {
         "is_horizontal" : if test_flag(attr, val) then 
            set_is_horizontal()
         else
            clear_is_horizontal()
         "value" : set_value(numeric_val(attr, val))
         "range" : set_range!numeric_vals(attr, val, 2)
         "ticks" : set_ticks(numeric_val(attr, val))
         "labels" : set_labels(numeric_val(attr, val))
         "snaps" : set_snaps(numeric_val(attr, val))
         "increment_size" : set_increment_size(numeric_val(attr, val))
         default: self.Component.set_one(attr, val)
      }
   end

   initially(a[])
      self.Component.initially()
      self.slider_area := SliderArea()
      self.increment_size := 1
      add(self.slider_area)
      set_fields(a)

end