# # $Id: selectablescrollarea.icn,v 1.10 2004/11/05 19:24:55 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() 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() 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) 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) 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 row <:= 1 row >:= *self.contents self.cursor := self.prev_cursor := row if \self.selection_on_key_moves & (\self.select_one | \self.select_many) then { if not (&shift & \select_many) then self.checked := list(*self.contents) self.checked[self.cursor] := 1 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) start_handle(e) if /self.cursor | (/self.select_one & /self.select_many) then return if not (&shift & \select_many) then self.checked := list(*self.contents) self.checked[self.cursor] := 1 self.refresh(1) selchange := 1 end_handle(e) end method handle_key_page_up(e) 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) 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) start_handle(e) if i := (1 < get_cursor()) then { move_cursor_on_key(i - 1) } end_handle(e) end method handle_key_down(e) 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) 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) 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) 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) & (\self.select_one | \self.select_many) & (get_line_under_pointer() = self.cursor) then { 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) 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