#
# $Id: selectablescrollarea.icn,v 1.12 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"

#
# This class extends LineBasedScrollArea to provide selection on rows,
# event handling and selection handling.
#
class SelectableScrollArea : LineBasedScrollArea(
   contents,                #
   checked,                 #
   select_one,              #
   select_many,             #
   cursor,
   old_cursor,
   selchange,
   going_up,
   prev_cursor,
   is_held,
   highlight,
   draggable_cursor,
   motion_cursor,
   selection_on_key_moves
   )

   method get_line_count()
      return *self.contents
   end

   #
   # Set the data to be displayed.
   # @param x  The list of data.
   #
   method set_contents(x)
      self.contents := x
      contents_changed()
   end

   #
   # Call this method if the contents list, previously
   # set with {set_contents()}, has changed.
   #
   method contents_changed()
      #
      # Expand/contract list if necessary
      #
      while *self.checked < *self.contents do put(self.checked)
      while *self.checked > *self.contents do pull(self.checked)

      constrain_cursor()

      compute_and_invalidate()
   end

   #
   # Keep the cursor within the bounds of the contents.
   # @p
   method constrain_cursor()
      if *self.contents = 0 then
         self.cursor := &null
      else {
         /self.cursor := 1
         \self.cursor >:= *self.contents
      }
   end

   #
   # Set the checked (highlighted) lines.
   # @param l   A list.  Each element in the list corresponds to an element in
   # @ the data.  If the element is not {&null}, the line is checked.
   #
   method set_checked(l)
      self.checked := l
      invalidate()
   end

   #
   # Get the checked lines.
   # @return A list corresponding to the data.  If an element is not {&null},
   # @ then the corresponding line is checked.
   #
   method get_checked()
      return self.checked
   end

   #
   # Clear the checked selections.
   #
   method clear_selections()
      self.checked := list(*contents)
      invalidate()
      return
   end

   #
   # Configure the object so that drags move the cursor (precludes using d&d
   # with the component).
   #
   method set_draggable_cursor()
      self.draggable_cursor := 1
   end

   #
   # Configure the object so that drags don't move the cursor
   #
   method clear_draggable_cursor()
      self.draggable_cursor := &null
   end

   #
   # Configure the object so that mouse motion moves the cursor.
   #
   method set_motion_cursor()
      self.motion_cursor := 1
   end

   #
   # Configure the object so that mouse motion doesn't move the cursor
   #
   method clear_motion_cursor()
      self.motion_cursor := &null
   end

   #
   # Configure the object so that moving the cursor via the keyboard does not
   # alter the selection.
   #
   method clear_selection_on_key_moves()
      self.selection_on_key_moves := &null
   end

   #
   # Configure the object so that moving the cursor via the keyboard does
   # alter the selection (the default behaviour).
   #
   method set_selection_on_key_moves()
      self.selection_on_key_moves := 1
   end

   #
   # Configure the object so that only one line may be highlighted
   #
   method set_select_one()
      self.select_one := 1
      self.select_many := &null
   end

   #
   # Configure the object so that several lines may be highlighted
   #
   method set_select_many()
      self.select_one := &null
      self.select_many := 1
   end

   #
   # Configure the object so that no lines may be highlighted (this
   # is the default).
   #
   method set_select_none()
      self.select_one := &null
      self.select_many := &null
   end

   #
   # Return item currently under the clicked cursor
   # @return The item number
   #
   method get_cursor()
      return \self.cursor
   end

   #
   # Return object currently under the clicked cursor
   # @return The object
   #
   method object_get_cursor()
      return self.contents[\self.cursor]
   end

   #
   # Return item currently under the dnd highlight
   # @return The item number
   #
   method get_highlight()
      return \self.highlight
   end

   #
   # Return object currently under the dnd highlight
   # @return The object
   #
   method object_get_highlight()
      return self.contents[\self.highlight]
   end

   #
   # Return the item previously under the clicked cursor
   # @return The item number
   #
   method get_prev_cursor()
      return \self.prev_cursor
   end

   #
   # Return object currently under the clicked cursor
   # @return The object
   #
   method object_get_prev_cursor()
      return self.contents[\self.prev_cursor]
   end

   #
   # Return a list of items checked (highlighted)
   # @return A list of items currently checked
   #
   method get_selections()
      local r, i
      r := []
      every i := 1 to *self.checked do
         if \self.checked[i] then
            put(r, i)
      return r
   end

   #
   # Return a list of objects checked (highlighted)
   # @return A list of objects currently checked
   #
   method object_get_selections()
      local r, i
      r := []
      every i := 1 to *self.checked do
         if \self.checked[i] then
            put(r, self.contents[i])
      return r
   end

   #
   # Set the current selections to the list l, which is a list of
   # item numbers.
   # @param l   The list of item numbers.
   #
   method set_selections(l)
      self.checked := list(*self.contents)
      every self.checked[!l] := 1
      invalidate()
   end

   #
   # Set the current selections to the list l, which is a list of objects
   # @param l  The list of objects.
   #
   method object_set_selections(l)
      local e
      self.checked := []
      every e := !self.contents do
         put(self.checked, if e === !l then 1 else &null)
      invalidate()
   end

   #
   # Set the cursor to the given object.  Has no effect if o is not
   # in the contents list.
   #
   method object_set_cursor(o)
      local i
      every i := 1 to *self.contents do {
         if self.contents[i] === o then {
            set_cursor(i)
            break
         }
      }
   end

   #
   # Set the cursor to the given object
   #
   method set_cursor(row)
      self.cursor := row
      invalidate()
   end

   #
   # Return the contents of the {ScrollArea}
   #
   method get_contents()
      return self.contents
   end

   #
   # Delete lines from content
   # @param l the list of lines in ascending order.
   #
   method delete_rows(l)
      local i
      every i := 1 to *l do {
         delete(self.contents, l[i] - i + 1)
         delete(self.checked, l[i] - i + 1)
      }

      constrain_cursor()

      compute_and_invalidate()
   end

   #
   # Insert rows into content at pos n
   # @param l the rows
   # @param n the position
   #
   method insert_rows(l, n)
      local i, e
      if n > *self.contents then {
         every e := !l do {
            put(self.contents, e)
            put(self.checked)
         }
      } else {
         every i := 1 to *l do {
            insert(self.contents, i + n - 1, l[i])
            insert(self.checked, i + n - 1)
         }
      }
      compute_and_invalidate()
   end

   #
   # Move the given list of rows to the given position.
   # @param l the rows
   # @param n the position
   #
   method move_rows(l, n)
      local t1, t2, e, n2
      every e := !l do {
         t1 := self.contents[e]
         t2 := self.checked[e]
         delete(self.contents, e)
         delete(self.checked, e)
         if n > e then
            n2 := n - 1
         else
            n2 := n
         if n2 > *self.contents then {
            put(self.contents, t1)
            put(self.checked, t2)
         } else {
            insert(self.contents, n2, t1)
            insert(self.checked, n2, t2)
         }
      }
      compute_and_invalidate()
   end

   # @p
   method move_cursor_on_key(row)
      local i, j, l

      row <:= 1
      row >:= *self.contents
      self.cursor := self.prev_cursor := row
      if \selection_on_key_moves & (\select_one | \select_many) then {
         l := get_selections()
         if not (&shift & \select_many) then
            self.checked := list(*self.contents)
         self.checked[self.cursor] := 1
         if not lang::equals(get_selections(), l) then
            selchange := 1
      }
      i := get_first_line()
      j := get_last_line()
      if row < i then
         goto_pos(row)
      else if row > j then
         goto_pos(i - j + row)
      else
         self.refresh(1)
   end

   # @p
   method start_handle(e)
      self.old_cursor := self.cursor
      selchange := &null
   end

   # @p
   method end_handle(e)
      if self.cursor ~=== self.old_cursor then
         fire(CURSOR_MOVED_EVENT, e)

      if \selchange then
         fire(SELECTION_CHANGED_EVENT, e)
   end

   method handle_return(e)
      local l

      if /self.cursor | (/self.select_one & /self.select_many) then
         return

      start_handle(e)
      l := get_selections()
      if not (&shift & \select_many) then
         self.checked := list(*self.contents)
      self.checked[self.cursor] := 1
      self.refresh(1)
      if not lang::equals(get_selections(), l) then
         selchange := 1
      end_handle(e)
   end

   method handle_key_page_up(e)
      local i
      start_handle(e)
      if i := (1 < get_cursor()) then {
         move_cursor_on_key(i - get_max_lines())
      }
      end_handle(e)
   end

   method handle_key_page_down(e)
      local i
      start_handle(e)
      if i := (*self.contents > get_cursor()) then {
         move_cursor_on_key(i + get_max_lines())
      }
      end_handle(e)
   end

   method handle_key_up(e)
      local i
      start_handle(e)
      if i := (1 < get_cursor()) then {
         move_cursor_on_key(i - 1)
      }
      end_handle(e)
   end

   method handle_key_down(e)
      local i
      start_handle(e)
      if i := (*self.contents > get_cursor()) then {
         move_cursor_on_key(i + 1)
      }
      end_handle(e)
   end

   method handle_key_left(e)
      local i
      start_handle(e)
      if i := (\self.hsb).get_value() then {
         self.hsb.set_value(i - self.hsb.increment_size)
         self.refresh()
      }
      end_handle(e)
   end

   method handle_key_right(e)
      local i
      start_handle(e)
      if i := (\self.hsb).get_value() then {
         self.hsb.set_value(i + self.hsb.increment_size)
         self.refresh()
      }
      end_handle(e)
   end

   method handle_key_home(e)
      start_handle(e)
      move_cursor_on_key(1)
      end_handle(e)
   end

   method handle_key_end(e)
      start_handle(e)
      move_cursor_on_key(*self.contents)
      end_handle(e)
   end

   method handle_press(e)
      local l
      start_handle(e)
      if l := get_line_under_pointer() then {
         self.prev_cursor := self.cursor
         self.cursor := l
         self.is_held := 1
         self.refresh(1)
      }
      end_handle(e)
   end

   method handle_move(e)
      local l
      start_handle(e)
      if \self.motion_cursor & /self.is_held & self.view.in_region() then {
         l := (&y - self.view.y) / self.line_height + self.get_first_line()
         l >:= self.get_last_line()
         if self.cursor ~= l then {
            self.cursor := l
            self.refresh(1)
         }
      }
      end_handle(e)
   end

   method handle_drag(e)
      local old_down, l

      start_handle(e)
      if \self.draggable_cursor & \self.is_held then {
         #
         # Mouse drag - save present marked line
         #
         old_down := self.cursor

         if &y < self.view.y then {
            self.going_up := 1
            is_ticking() | set_ticker(30)
         } else if &y > self.view.y + self.view.h then {
            self.going_up := &null
            is_ticking() | set_ticker(30)
         } else {
            l := (&y - self.view.y) / self.line_height + self.get_first_line()
            l >:= self.get_last_line()
            self.cursor := l
            stop_ticker()
         }

         #
         # Refresh if line changed
         #
         if old_down ~=== self.cursor then
            self.refresh(1)
      }
      end_handle(e)
   end

   method tick()
      start_handle()
      if \going_up then {
         self.cursor := self.get_first_line() - 1
         self.cursor <:= 1
         (\self.vsb).set_value(self.vsb.get_value() - self.vsb.increment_size)
      } else {
         self.cursor := self.get_last_line() + 1
         self.cursor >:= *self.contents
         (\self.vsb).set_value(self.vsb.get_value() + self.vsb.increment_size)
      }
      self.refresh(1)
      end_handle()
   end

   method handle_release(e)
      local l

      start_handle(e)
      if \self.is_held then {
         #
         # Mouse released after being held down.  Clear flag
         #
         self.is_held := &null
         stop_ticker()
         #
         # Clear flag, refresh, return event
         #
         if (e === (&lrelease|&rrelease|&mrelease)) &
            (\self.select_one | \self.select_many) &
            (get_line_under_pointer() = self.cursor) then {

            l := get_selections()
            if \self.select_many & (&shift | &control) then {
               if &control then
                  self.checked[self.cursor] := if /self.checked[self.cursor] then 1 else &null
               else {
                  #
                  # &shift
                  #
                  if \self.prev_cursor then {
                     if self.prev_cursor > self.cursor then
                        every self.checked[self.cursor to self.prev_cursor] := 1
                     else
                        every self.checked[self.prev_cursor to self.cursor] := 1
                  } else
                     self.checked[self.cursor] := 1
               }
            }
            else {
               self.checked := list(*self.contents)
               self.checked[\self.cursor] := 1
            }
            self.refresh(1)
            if not lang::equals(get_selections(), l) then
               selchange := 1
            }
         }
      end_handle(e)
   end

   method handle_event(e)
      (\self.vsb).handle_event(e)
      (\self.hsb).handle_event(e)

      if e === (&lpress | &rpress | &mpress) then
         handle_press(e)
      else if e === (&ldrag) then
         handle_drag(e)
      else if e === -12 then
         handle_move(e)
      else if e === (&lrelease | &rrelease | &mrelease) then
         handle_release(e)
      else if \self.has_focus then {
         case e of {
            Key_Home : handle_key_home(e)
            Key_End : handle_key_end(e)
            Key_PgUp : handle_key_page_up(e)
            Key_PgDn : handle_key_page_down(e)
            Key_Up : handle_key_up(e)
            Key_Down : handle_key_down(e)
            Key_Left : handle_key_left(e)
            Key_Right : handle_key_right(e)
            "\r" | "\l": handle_return(e)
         }
      }
   end

   method keeps(e)
      return e === (Key_Left | Key_Right | Key_Up | Key_Down)
   end

   method object_get_gesture_selections()
      \self.cursor | fail
      if \self.checked[self.cursor] then
         return object_get_selections()
      else
         return [self.contents[self.cursor]]
   end

   method get_gesture_selections()
      \self.cursor | fail
      if \self.checked[self.cursor] then
         return get_selections()
      else
         return [self.cursor]
   end

   method draw(subject_x, subject_y, vx, vy, vw, vh)
      local rev, first_line, last_line, xp, yp, i, selection_cw, cursor_cw, highlight_cw

      #
      # Which lines to draw
      #
      first_line := get_first_line()
      last_line := get_last_line()
      last_line >:= get_line_count()

      #
      # Where to draw them
      #
      yp := vy + self.line_height / 2

      #
      # Left offset
      #
      xp := vx - subject_x

      rev := Clone(self.cbwin, "drawop=reverse")

      #
      # Write the lines
      #
      every i := first_line to last_line do {
         # Setup cloned windows
         selection_cw := if \self.checked[i] then rev else &null
         if i = \self.cursor then {
            if /self.has_focus then
               cursor_cw := Clone(self.cbwin, "fg=gray", "fillstyle=masked", "pattern=gray")
            else
               cursor_cw := Clone(self.cbwin, "fg=red", "fillstyle=masked", "pattern=gray")
         }
         if i = \self.highlight then {
            highlight_cw := Clone(self.cbwin, "fg=blue", "fillstyle=masked", "pattern=gray")
         }

         # Draw the line
         draw_line(xp, yp, i, selection_cw, cursor_cw, highlight_cw)

         # Uncouple cloned windows.
         if \cursor_cw then {
            Uncouple(cursor_cw)
            cursor_cw := &null
         }
         if \highlight_cw then {
            Uncouple(highlight_cw)
            highlight_cw := &null
         }

         yp +:= self.line_height
      }

      Uncouple(rev)
      rev := &null
   end

   method set_one(attr, val)
      case attr of {
         "contents" : set_contents(val)
         "select_one" : set_select_one()
         "select_many" : set_select_many()
         "select_none" : set_select_none()
         "draggable_cursor" :
            if test_flag(attr, val) then
               set_draggable_cursor()
            else
               clear_draggable_cursor()
         "motion_cursor" :
            if test_flag(attr, val) then
               set_motion_cursor()
            else
               clear_motion_cursor()
         "selection_on_key_moves" :
            if test_flag(attr, val) then
               set_selection_on_key_moves()
            else
               clear_selection_on_key_moves()
         default: self.Component.set_one(attr, val)
      }
   end

   #
   # This method is overridden by the subclass to draw the given
   # line at the given position
   # @param xp  The left position it should be drawn at
   # @param yp   The y position it should be drawn at
   # @param i   The line number to draw
   # @param selection_cw If non-null, this cloned window must be used to show a selected row
   # @param cursor_cw If non-null, this cloned window must be used to show the cursor
   # @param highlight_cw If non-null, this cloned window must be used to show the highlight
   #
   abstract method draw_line(xp, yp, i, selection_cw, cursor_cw, highlight_cw)

   method can_drag()
      local l, obj
      if /self.draggable_cursor & \self.is_held then {
         self.is_held := &null
         return self.object_get_gesture_selections()
      }
   end

   method drag_event(d)
      local old_highlight

      old_highlight := self.highlight
      self.highlight := self.get_line_under_pointer() | &null
      if self.highlight ~=== old_highlight then
         self.refresh(1)

      return \self.highlight
   end

   method drag_reset()
      if \self.highlight then {
         self.highlight := &null
         self.refresh(1)
      }
   end

   initially
      self.ScrollArea.initially()
      self.set_accepts_focus()
      self.checked := []
      self.cursor := &null
      self.contents := []
      self.selection_on_key_moves := 1
end
