# # $Id: textfield.icn,v 1.9 2004/11/11 19:55:34 rparlett Exp $ # # This file is in the public domain. # # Author: Robert Parlett (parlett@dial.pipex.com) # package gui link graphics import undo import lang $include "guih.icn" # # A class for a single input line of text. The text can # scroll within the area specified. By default, a border # surrounds the text area; this can be turned off by using # {toggle_draw_border()}. # # The horizontal size must be set by the {set_size()} method: # there is no default (the vertical size will default, however). # # An ACTION_EVENT is generated when return is pressed, # a CONTENT_CHANGED_EVENT whenever the contents are changed, # a CURSOR_MOVED_EVENT when the cursor moves, and a # SELECTION_CHANGED_EVENT whenver the selection changes. # # @example # @ t := TextField() # @ t.set_pos(50, 250) # @ # Vertical size will default # @ t.set_size(100) # @ t.set_contents("Initial string") # @ self.add(t) # class TextField : Component( filter, # Cset for filtering characters printable, # The printable characters contents, # is_held, # True if dragging going_left, cursor, # mark, leftmost, # rightmost, # tx, # tw, # displaychar, # char to print on screen undo_manager, old_cursor, old_has_region, changed ) # # Set the displaychar attribute # method set_displaychar(c) displaychar := c end # # Set a filter on the characters allowed to be input to the text field. # @param c The cset of permissible characters. # @example # @ # Permit only hexadecimal characters as input # @ set_filter('0987654321abcdefABCDEF') # method set_filter(c) return self.filter := c ** printable end # # Return the present contents of the text field. # method get_contents() return self.contents end method resize() if \self.draw_border_flag then /self.h_spec := WAttrib(self.cwin, "fheight") + 2 * DEFAULT_TEXT_Y_SURROUND else /self.h_spec := WAttrib(self.cwin, "fheight") self.Component.resize() if \self.draw_border_flag then { self.tx := self.x + DEFAULT_TEXT_X_SURROUND self.tw := self.w - 2 * DEFAULT_TEXT_X_SURROUND } else { # # Still want an offset for the text so that a click slightly to the left of # the text itself is recognised. Therefore, just have a slightly smaller surround. # self.tx := self.x + (DEFAULT_TEXT_X_SURROUND - BORDER_WIDTH) self.tw := self.w - 2 * (DEFAULT_TEXT_X_SURROUND - BORDER_WIDTH) } end # # Set the contents of the field. If not invoked then # the initial content is the empty string. # @param x The contents # method set_contents(x) self.contents := string(x) self.cursor := *self.contents + 1 clear_mark() self.leftmost := 1 self.invalidate() undo_manager.clear() return x end method start_handle(e) old_cursor := cursor old_has_region := has_region() | &null changed := &null end method end_handle(e) local hr, moved if \changed then { self.invalidate() fire(CONTENT_CHANGED_EVENT, e) } if old_cursor ~= cursor then { moved := 1 self.invalidate() fire(CURSOR_MOVED_EVENT, e) } # # Deduce a region change from looking for a change in whether there # was/is a region; or if there is and was a region and if the cursor # has moved, or the content changed. # hr := has_region() if (/old_has_region & \hr) | (\old_has_region & /hr) | (\hr & (\moved | \changed)) then { self.invalidate() fire(SELECTION_CHANGED_EVENT, e) } end # # Mouse click - compute new cursor position, re-display # # @p method handle_press(e) start_handle(e) in_region() | fail self.mark := self.cursor := whereis_x() self.is_held := 1 end_handle(e) end method whereis_x() local i, startx, s # Space at end for cursor at end of string s := self.contents || " " i := self.leftmost startx := self.tx while (startx + TextWidth(self.cwin, s[self.leftmost:i + 1]) <= &x) & (i + 1 < self.rightmost) do i +:= 1 return i end method handle_drag(e) local p start_handle(e) if \self.is_held then { p := self.cursor # Start ticking if to the left/right, otherwise stop ticking if &x < self.tx then { self.going_left := 1 is_ticking() | set_ticker(30) } else if &x > self.tx + self.tw then { self.going_left := &null is_ticking() | set_ticker(30) } else { self.cursor := whereis_x() stop_ticker() } } end_handle(e) end method tick() start_handle() if \going_left then { self.cursor := self.leftmost - 1 self.cursor <:= 1 } else { self.cursor := self.rightmost self.cursor >:= (*self.contents + 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() } end_handle(e) end method clear_mark() self.mark := &null end # # Delete # # @p method handle_delete_left(e) local ed start_handle(e) if has_region() then delete_region(e) else if self.cursor > 1 then { ed := TextFieldDeleteLeftEdit(self) ed.redo() undo_manager.add_edit(ed) clear_mark() changed := 1 } end_handle(e) end method handle_return(e) fire(ACTION_EVENT, e) end method keyboard_mark() if &shift then /mark := cursor else mark := &null end method handle_key_right(e) start_handle(e) keyboard_mark() self.cursor := (*self.contents + 1 >= self.cursor + 1) end_handle(e) end method handle_key_left(e) start_handle(e) keyboard_mark() self.cursor := (0 < self.cursor - 1) end_handle(e) end method handle_delete_line(e) local ed start_handle(e) ed := TextFieldDeleteLineEdit(self) ed.redo() undo_manager.add_edit(ed) clear_mark() changed := 1 end_handle(e) end method handle_delete_right(e) local ed start_handle(e) if has_region() then delete_region(e) else if self.cursor <= *self.contents then { ed := TextFieldDeleteRightEdit(self) ed.redo() undo_manager.add_edit(ed) clear_mark() changed := 1 } end_handle(e) end method handle_select_all(e) start_handle(e) if *self.contents > 0 then { self.cursor := *self.contents + 1 self.mark := 1 } end_handle(e) end method handle_end_of_line(e) start_handle(e) keyboard_mark() self.cursor := *self.contents + 1 end_handle(e) end method handle_start_of_line(e) start_handle(e) keyboard_mark() self.cursor := 1 end_handle(e) end method handle_cut(e) start_handle(e) if has_region() then { get_clipboard().set_content(get_region()) delete_region(e) } end_handle(e) end method handle_copy(e) start_handle(e) if has_region() then { get_clipboard().set_content(get_region()) } end_handle(e) end method keeps(e) return e === (Key_Left | Key_Right) end method get_pasteable_clipboard() local x, t, s, c x := get_clipboard().get_content() t := string(x) | fail # Apply the filter to the string to paste s := "" every c := !t do { if member(filter, c) then s ||:= c } if *s = 0 then fail return s end method handle_paste(e) local s, ce, ed start_handle(e) if s := get_pasteable_clipboard() then { ce := CompoundEdit() if has_region() then { ed := TextFieldDeleteRegionEdit(self) ed.redo() ce.add_edit(ed) } clear_mark() ed := TextFieldPasteEdit(self, s) ed.redo() ce.add_edit(ed) undo_manager.add_edit(ce) changed := 1 } end_handle(e) end method handle_default(e) local ce, ed start_handle(e) # # Add any printable character at cursor position # if type(e) == "string" & not(&control | &meta) & any(filter, e) then { if has_region() then { ce := CompoundEdit() ed := TextFieldDeleteRegionEdit(self) ed.redo() ce.add_edit(ed) clear_mark() ed := TextFieldDefaultEdit(self, e) ed.redo() ce.add_edit(ed) undo_manager.add_edit(ce) } else { ed := TextFieldDefaultEdit(self, e) ed.redo() undo_manager.add_edit(ed) } changed := 1 } end_handle(e) end method handle_undo() start_handle(e) if undo_manager.can_undo() then { undo_manager.undo() changed := 1 } end_handle(e) end method handle_redo() start_handle(e) if undo_manager.can_redo() then { undo_manager.redo() changed := 1 } end_handle(e) end method handle_event(e) local code if e === (&lpress | &rpress | &mpress) then return handle_press(e) else if e === (&ldrag | &rdrag | &mdrag) then return handle_drag(e) else if e === (&lrelease | &rrelease | &mrelease) then return handle_release(e) else if \self.has_focus then { # # Object has focus. Handle various key presses. # return case e of { "\b" : handle_delete_left(e) "\r" | "\l": handle_return(e) "\^k" : handle_delete_line(e) Key_Right : handle_key_right(e) Key_Left : handle_key_left(e) "\^a" : handle_select_all(e) "\^e" : handle_end_of_line(e) "\d" | "\^d" : handle_delete_right(e) "\^x" : handle_cut() "\^c" : handle_copy() "\^v" : handle_paste() "\^z" : handle_undo() "\^y" : handle_redo() default : handle_default(e) } } end method get_region() if self.mark < self.cursor then { return self.contents[self.mark:self.cursor] } else { return self.contents[self.cursor:self.mark] } end method delete_region(e) local ed ed := TextFieldDeleteRegionEdit(self) ed.redo() undo_manager.add_edit(ed) clear_mark() changed := 1 end method has_region() return \self.mark ~= self.cursor end method got_focus(e) if not(e === (&lpress | &rpress | &mpress)) then { self.cursor := *self.contents + 1 self.mark := 1 } self.Component.got_focus(e) end method lost_focus(e) clear_mark() self.Component.lost_focus(e) end method display(buffer_flag) local r fh := WAttrib(self.cwin, "fheight") spc := self.tw # # Space at end for cursor at end of string # if \displaychar then s := repl(displaychar, *(self.contents)) || " " else s := self.contents || " " # # Initialize left and right markers; only move leftmost if needed # self.leftmost >:= self.cursor self.rightmost := self.cursor + 1 # # Now pad out left and right markers to fill space # if TextWidth(self.cwin, s[self.leftmost:self.rightmost]) <= spc then { while TextWidth(self.cwin, s[self.leftmost:self.rightmost + 1]) <= spc do self.rightmost +:= 1 while (self.leftmost > 1) & TextWidth(self.cwin, s[self.leftmost - 1:self.rightmost]) <= spc do self.leftmost -:= 1 } else { while TextWidth(self.cwin, s[self.leftmost:self.rightmost]) > spc do self.leftmost +:= 1 } # # Clear rectangle, set s to string to display # EraseRectangle(self.cbwin, self.x, self.y, self.w, self.h) s := s[self.leftmost:self.rightmost] # # Cursor position within s # cp := self.cursor - self.leftmost + 1 if \self.draw_border_flag then DrawSunkenRectangle(self.cbwin, self.x, self.y, self.w, self.h) # # Display s centred vertically in box # left_string(self.cbwin, self.tx, self.y + self.h / 2 , s) # # If has focus display box cursor, else display outline cursor # if \self.has_focus then { cw := Clone(self.cbwin, "bg=red", "fg=white") EraseRectangle(cw, self.tx + TextWidth(self.cwin, s[1:cp]), 1 + self.y + (self.h - fh) / 2, TextWidth(self.cwin, s[cp]), fh) left_string(cw, self.tx + TextWidth(cw, s[1:cp]), self.y + self.h / 2 , s[cp]) Uncouple(cw) } else { cw := Clone(self.cbwin, "fg=red") Rectangle(cw, self.tx + TextWidth(self.cwin, s[1:cp]), 1 + self.y + (self.h - fh) / 2, TextWidth(self.cwin, s[cp]), fh) Uncouple(cw) } if has_region() then { mp := self.mark - self.leftmost + 1 mp <:= 1 mp >:= (*s + 1) if mp > cp then { np := mp mp := cp + 1 } else { np := cp } off1 := TextWidth(self.cwin, s[1:mp]) off2 := TextWidth(self.cwin, s[1:np]) cw := Clone(self.cbwin, "drawop=reverse") FillRectangle(cw, self.tx + off1, 1 + self.y + (self.h - fh) / 2, off2 - off1, fh) Uncouple(cw) } 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 method set_one(attr, val) case attr of { "filter" : set_filter(cset_val(attr, val)) "displaychar" : set_displaychar(string_val(attr, val)) "contents" : set_contents(string_val(attr, val)) default: self.Component.set_one(attr, val) } end initially(a[]) self.Component.initially() undo_manager := UndoManager() filter := printable := cset(&cset[33:0]) self.accepts_focus_flag := 1 self.set_contents("") self.draw_border_flag := 1 set_fields(a) end class TextFieldEdit:UndoableEdit(parent, cursor, mark) method redo() restore() self.redo_impl() end method undo() self.undo_impl() restore() end abstract method redo_impl() abstract method undo_impl() method save() self.cursor := parent.cursor self.mark := parent.mark end method restore() parent.cursor := self.cursor parent.mark := self.mark end initially(parent) self.parent := parent save() end class TextFieldDeleteRightEdit:TextFieldEdit(ch) method redo_impl() ch := parent.contents[parent.cursor] parent.contents[parent.cursor] := "" end method undo_impl() if self.cursor > *parent.contents then parent.contents ||:= ch else parent.contents[self.cursor] := ch || parent.contents[self.cursor] end initially(parent) self.TextFieldEdit.initially(parent) end class TextFieldDeleteLeftEdit:TextFieldEdit(ch) method redo_impl() ch := parent.contents[parent.cursor - 1] parent.contents[parent.cursor - 1] := "" parent.cursor -:= 1 end method undo_impl() if self.cursor - 1 > *parent.contents then parent.contents ||:= ch else parent.contents[self.cursor - 1] := ch || parent.contents[self.cursor - 1] end initially(parent) self.TextFieldEdit.initially(parent) end class TextFieldDeleteRegionEdit:TextFieldEdit(pre) method redo_impl() if parent.mark < parent.cursor then { pre := parent.contents[parent.mark:parent.cursor] parent.contents[parent.mark:parent.cursor] := "" parent.cursor := parent.mark } else { pre := parent.contents[parent.cursor:parent.mark] parent.contents[parent.cursor:parent.mark] := "" } end method undo_impl() if self.mark < self.cursor then { parent.contents := parent.contents[1:self.mark] || pre || parent.contents[self.mark:0] } else { parent.contents := parent.contents[1:self.cursor] || pre || parent.contents[self.cursor:0] } end initially(parent) self.TextFieldEdit.initially(parent) end class TextFieldPasteEdit:TextFieldEdit(s) method redo_impl() parent.contents := parent.contents[1:parent.cursor] || s || parent.contents[parent.cursor:0] parent.cursor +:= *s end method undo_impl() parent.contents[self.cursor +: *s] := "" end initially(parent, s) self.TextFieldEdit.initially(parent) self.s := s end class TextFieldDefaultEdit:TextFieldEdit(s) method add_edit(other) if is_instance(other, "gui__TextFieldDefaultEdit") & (other.cursor = self.cursor + *s) then { s ||:= other.s return } end method redo_impl() if parent.cursor = 1 then parent.contents := s || parent.contents else parent.contents[parent.cursor - 1] ||:= s parent.cursor +:= *s parent.clear_mark() end method undo_impl() parent.contents[self.cursor +: *s] := "" end initially(parent, e) self.TextFieldEdit.initially(parent) s := e end class TextFieldDeleteLineEdit:TextFieldEdit(pre) method redo_impl() pre := parent.contents parent.contents := "" parent.cursor := 1 end method undo_impl() parent.contents := pre end initially(parent) self.TextFieldEdit.initially(parent) end