class Canis::Form
Manages the controls/widgets on a screen. Manages traversal, rendering and events of all widgets that are associated with it via the add_widget
method.
Passes keys pressed by user to the current field. Any keys that are not handled by the current field, are handled by the form if the application has bound the key via bind_key
.
TODO: we don't have an event for when form is entered and exited.
Attributes
index of active widget
color and bgcolor for all widget, widgets that don't have color specified will inherit from form If not mentioned, then global defaults will be taken
hash containing widgets by name for retrieval Useful if one widget refers to second before second created.
lb = @form.by_name["listb"]
cursor row and col
color and bgcolor for all widget, widgets that don't have color specified will inherit from form If not mentioned, then global defaults will be taken
class that lays out objects (calculates row, col, width and height)
has the form been modified
name given to form for debugging
signify that the layout manager must calculate each widgets dimensions again since typically the window has been resized.
cursor row and col
array of widgets
related window used for printing
Public Class Methods
# File lib/canis/core/widgets/rwidget.rb, line 1570 def initialize win, &block @window = win # added 2014-05-01 - 20:43 so that a window can update its form, during overlapping forms. @window.form = self if win @widgets = [] @by_name = {} @active_index = -1 @row = @col = -1 @modified = false @resize_required = true @focusable = true # when widgets are added, add them here if focusable so traversal is easier. However, # if user changes this during the app, we need to update this somehow. FIXME @focusables = [] # added 2014-04-24 - 12:28 to make traversal easier @navigation_policy ||= :CYCLICAL # 2014-04-24 - 17:42 NO MORE ENTER LEAVE at FORM LEVEL #register_events([:ENTER, :LEAVE, :RESIZE]) register_events(:RESIZE) instance_eval &block if block_given? @_firsttime = true; # added on 2010-01-02 19:21 to prevent scrolling crash ! @name ||= "" # related to emacs kill ring concept for copy-paste $kill_ring ||= [] # 2010-03-09 22:42 so textarea and others can copy and paste emacs EMACS $kill_ring_pointer = 0 # needs to be incremented with each append, moved with yank-pop $append_next_kill = false $kill_last_pop_size = 0 # size of last pop which has to be cleared $last_key = 0 # last key pressed @since 1.1.5 (not used yet) $current_key = 0 # curr key pressed @since 1.1.5 (so some containers can behave based on whether # user tabbed in, or backtabbed in (rmultisplit) # for storing error message $error_message ||= Variable.new "" # what kind of key-bindings do you want, :vim or :emacs $key_map_type ||= :vim ## :emacs or :vim, keys to be defined accordingly. TODO #bind_key(KEY_F1, 'help') { hm = help_manager(); hm.display_help } map_keys unless @keys_mapped end
Public Instance Methods
Add given widget to widget list and returns an incremental id. Adding to widgets, results in it being painted, and focussed. removing a widget and adding can give the same ID's, however at this point we are not really using ID. But need to use an incremental int in future. (internal use)
# File lib/canis/core/widgets/rwidget.rb, line 1635 def add_widget widget # this help to access widget by a name if widget.respond_to? :name and !widget.name.nil? @by_name[widget.name] = widget end @widgets << widget @focusable_modified = true return @widgets.length-1 end
move cursor by num columns. Form
# File lib/canis/core/widgets/rwidget.rb, line 1951 def addcol num return if @col.nil? || @col == -1 @col += num @window.wmove @row, @col ## 2010-01-30 23:45 exchange calling parent with calling this forms setrow # since in tabbedpane with table i am not gietting this forms offset. setrowcol nil, col end
move cursor by given rows and columns, can be negative. 2010-01-30 23:47 FIXME, if this is called we should call setrowcol like in addcol
# File lib/canis/core/widgets/rwidget.rb, line 1962 def addrowcol row,col return if @col.nil? or @col == -1 # contradicts comment on top return if @row.nil? or @row == -1 @col += col @row += row @window.wmove @row, @col end
returns form's bgcolor, or global default.
# File lib/canis/core/widgets/rwidget.rb, line 2224 def bgcolor @bgcolor || $def_bg_color end
returns forms color, or if not set then app default This is used by widget's as the color to fallback on when no color is specified for them. This way all widgets in a form can have one color.
# File lib/canis/core/widgets/rwidget.rb, line 2220 def color @color || $def_fg_color end
# File lib/canis/core/widgets/rwidget.rb, line 2030 def digit_argument ch $multiplier = ch - ?\M-0.getbyte(0) $log.debug " inside UNIV MULT 0 #{$multiplier} " # See if user enters numerics. If so discard existing varaible and take only #+ entered values _m = $multiplier while true ch = @window.getchar() case ch when -1 next when ?0.getbyte(0)..?9.getbyte(0) _m *= 10 ; _m += (ch-48) $multiplier = _m $log.debug " inside UNIV MULT 1 #{$multiplier} " when ?\M-0.getbyte(0)..?\M-9.getbyte(0) _m *= 10 ; _m += (ch-?\M-0.getbyte(0)) $multiplier = _m $log.debug " inside UNIV MULT 2 #{$multiplier} " else $log.debug " inside UNIV MULT else got #{ch} " # here is some other key that is the function key to be repeated. we must honor this # and ensure it goes to the right widget return ch #return :UNHANDLED end end return 0 end
@return [Widget, nil] current field, nil if no focusable field
# File lib/canis/core/widgets/rwidget.rb, line 1721 def get_current_field select_next_field if @active_index == -1 return nil if @active_index.nil? # for forms that have no focusable field 2009-01-08 12:22 @widgets[@active_index] end
forms handle keys
mainly traps tab and backtab to navigate between widgets. I know some widgets will want to use tab, e.g edit boxes for entering a tab
or for completion.
@throws FieldValidationException
NOTE : please rescue exceptions when you use this in your main loop and alert() user
# File lib/canis/core/widgets/rwidget.rb, line 2110 def handle_key(ch) # 2014-08-19 - 21:10 moving to init, so that user may override or remove #map_keys unless @keys_mapped handled = :UNHANDLED # 2011-10-4 if ch == ?\C-u.getbyte(0) ret = universal_argument $log.debug "C-u FORM set MULT to #{$multiplier}, ret = #{ret} " return 0 if ret == 0 ch = ret # unhandled char elsif ch >= ?\M-1.getbyte(0) && ch <= ?\M-9.getbyte(0) if $catch_alt_digits # emacs EMACS ret = digit_argument ch $log.debug " FORM set MULT DA to #{$multiplier}, ret = #{ret} " return 0 if ret == 0 # don't see this happening ch = ret # unhandled char end end $current_key = ch case ch when -1 return when 1000, 12 # NOTE this works if widgets cover entire screen like text areas and lists but not in # dialogs where there is blank space. only widgets are painted. # testing out 12 is C-l $log.debug " form REFRESH_ALL repaint_all HK #{ch} #{self}, #{@name} " repaint_all_widgets return when FFI::NCurses::KEY_RESIZE # SIGWINCH # note that in windows that have dialogs or text painted on window such as title or # box, the clear call will clear it out. these are not redrawn. lines = Ncurses.LINES cols = Ncurses.COLS x = Ncurses.stdscr.getmaxy y = Ncurses.stdscr.getmaxx $log.debug " form RESIZE HK #{ch} #{self}, #{@name}, #{ch}, x #{x} y #{y} lines #{lines} , cols: #{cols} " #alert "SIGWINCH WE NEED TO RECALC AND REPAINT resize #{lines}, #{cols}: #{x}, #{y} " # next line may be causing flicker, can we do without. Ncurses.endwin @window.wrefresh @window.wclear if @layout_manager @layout_manager.do_layout # we need to redo statusline and others that layout ignores else @widgets.each { |e| e.repaint_all(true) } # trying out end ## added RESIZE on 2012-01-5 ## stuff that relies on last line such as statusline dock etc will need to be redrawn. fire_handler :RESIZE, self else field = get_current_field if $log.debug? keycode = keycode_tos(ch) $log.debug " form HK #{ch} #{self}, #{@name}, #{keycode}, field: giving to: #{field}, #{field.name} " if field end handled = :UNHANDLED handled = field.handle_key ch unless field.nil? # no field focussable $log.debug "handled inside Form #{ch} from #{field} got #{handled} " # some widgets like textarea and list handle up and down if handled == :UNHANDLED or handled == -1 or field.nil? case ch when KEY_TAB, ?\M-\C-i.getbyte(0) # tab and M-tab in case widget eats tab (such as Table) ret = select_next_field return ret if ret == :NO_NEXT_FIELD # alt-shift-tab or backtab (in case Table eats backtab) when FFI::NCurses::KEY_BTAB, 481 ## backtab added 2008-12-14 18:41 ret = select_prev_field return ret if ret == :NO_PREV_FIELD when FFI::NCurses::KEY_UP ret = select_prev_field return ret if ret == :NO_PREV_FIELD when FFI::NCurses::KEY_DOWN ret = select_next_field return ret if ret == :NO_NEXT_FIELD else #$log.debug " before calling process_key in form #{ch} " if $log.debug? ret = process_key ch, self # seems we need to flushinp in case composite has pushed key $log.debug "FORM process_key #{ch} got ret #{ret} in #{self}, flushing input " # 2014-06-01 - 17:01 added flush, maybe at some point we could do it only if unhandled # in case some method wishes to actually push some keys Ncurses.flushinp return :UNHANDLED if ret == :UNHANDLED end elsif handled == :NO_NEXT_FIELD || handled == :NO_PREV_FIELD # 2011-10-4 return handled end end $log.debug " form before repaint #{self} , #{@name}, ret #{ret}" repaint $last_key = ch ret || 0 # 2011-10-17 end
returns in instance of help_manager
with which one may install help_text and call help. user apps will only supply help_text, form would already have mapped F1 to help.
# File lib/canis/core/widgets/rwidget.rb, line 2213 def help_manager require 'canis/core/util/helpmanager' @help_manager ||= Canis::HelpManager.new self end
These mappings will only trigger if the current field
does not use them.
# File lib/canis/core/widgets/rwidget.rb, line 2063 def map_keys return if @keys_mapped bind_key(KEY_F1, 'help') { hm = help_manager(); hm.display_help } bind_keys([?\M-?,?\?], 'show field help') { #if get_current_field.help_text #textdialog(get_current_field.help_text, 'title' => 'Help Text', :bgcolor => 'green', :color => :white) #else print_key_bindings #end } bind_key(FFI::NCurses::KEY_F9, "Print keys", :print_key_bindings) # show bindings, tentative on F9 bind_key(?\M-:, 'show menu') { fld = get_current_field am = fld.action_manager() #fld.init_menu am.show_actions } @keys_mapped = true end
form calls on_enter
of each object. However, if a multicomponent calls on_enter
of a widget, this code will not be triggered. The highlighted part
# File lib/canis/core/widgets/rwidget.rb, line 1783 def on_enter f return if f.nil? || !f.focusable # added focusable, else label was firing 2010-09 f.state = :HIGHLIGHTED # If the widget has a color defined for focussed, set repaint # otherwise it will not be repainted unless user edits ! if f.highlight_bgcolor || f.highlight_color f.repaint_required true end f.modified false #f.set_modified false f.on_enter if f.respond_to? :on_enter # 2014-04-24 - 17:42 NO MORE ENTER LEAVE at FORM LEVEL #fire_handler :ENTER, f end
do not override
form's trigger, fired when any widget loses focus
This wont get called in editor components in tables, since they are formless
# File lib/canis/core/widgets/rwidget.rb, line 1765 def on_leave f return if f.nil? || !f.focusable # added focusable, else label was firing f.state = :NORMAL # on leaving update text_variable if defined. Should happen on modified only # should this not be f.text_var ... f.buffer ? 2008-11-25 18:58 #f.text_variable.value = f.buffer if !f.text_variable.nil? # 2008-12-20 23:36 f.on_leave if f.respond_to? :on_leave # 2014-04-24 - 17:42 NO MORE ENTER LEAVE at FORM LEVEL #fire_handler :LEAVE, f ## to test XXX in combo boxes the box may not be editable by be modified by selection. if f.respond_to? :editable and f.modified? $log.debug " Form about to fire CHANGED for #{f} " f.fire_handler(:CHANGED, f) end end
e.g. process_key
ch, self returns UNHANDLED if no block for it after form handles basic keys, it gives unhandled key to current field, if current field returns unhandled, then it checks this map. Please update widget with any changes here. TODO: match regexes as in mapper
# File lib/canis/core/widgets/rwidget.rb, line 1986 def process_key keycode, object return _process_key keycode, object, @window end
remove a widget (internal use)
# File lib/canis/core/widgets/rwidget.rb, line 1650 def remove_widget widget if widget.respond_to? :name and !widget.name.nil? @by_name.delete(widget.name) end @focusable_modified = true @widgets.delete widget end
form repaint,calls repaint on each widget which will repaint it only if it has been modified since last call. called after each keypress.
# File lib/canis/core/widgets/rwidget.rb, line 1676 def repaint $log.debug " form repaint:#{self}, #{@name} , r #{@row} c #{@col} " if $log.debug? if @resize_required && @layout_manager @layout_manager.form = self unless @layout_manager.form @layout_manager.do_layout @resize_required = false end @widgets.each do |f| next if f.visible == false # added 2008-12-09 12:17 #$log.debug "XXX: FORM CALLING REPAINT OF WIDGET #{f} IN LOOP" #raise "Row or col nil #{f.row} #{f.col} for #{f}, #{f.name} " if f.row.nil? || f.col.nil? f.repaint f._object_created = true # added 2010-09-16 13:02 now prop handlers can be fired end _update_focusables if @focusable_modified # this can bomb if someone sets row. We need a better way! if @row == -1 and @_firsttime == true select_first_field @_firsttime = false end setpos # XXX this creates a problem if window is a pad # although this does show cursor movement etc. ### @window.wrefresh #if @window.window_type == :WINDOW #$log.debug " formrepaint #{@name} calling window.wrefresh #{@window} " @window.wrefresh Ncurses::Panel.update_panels ## added 2010-11-05 00:30 to see if clears the stdscr problems #else #$log.warn " XXX formrepaint #{@name} no refresh called 2011-09-19 #{@window} " #end end
this forces a repaint of all visible widgets and has been added for the case of overlapping windows, since a black rectangle is often left when a window is destroyed. This is internally triggered whenever a window is destroyed, and currently only for root window. NOTE: often the window itself or spaces between widgets also gets cleared, so basically the window itself may need recreating ? 2014-08-18 - 21:03
# File lib/canis/core/widgets/rwidget.rb, line 2088 def repaint_all_widgets $log.debug " REPAINT ALL in FORM called " @widgets.each do |w| next if w.visible == false next if w.class.to_s == "Canis::MenuBar" $log.debug " ---- REPAINT ALL #{w.name} " #w.repaint_required true w.repaint_all true w.repaint end $log.debug " REPAINT ALL in FORM complete " # place cursor on current_widget setpos end
puts focus on the given field/widget index @param index of field in @widgets (or can be a Widget
too) XXX if called externally will not run a on_leave
of previous field
# File lib/canis/core/widgets/rwidget.rb, line 1804 def select_field ix0 if ix0.is_a? Widget ix0 = @widgets.index(ix0) end return if @widgets.nil? or @widgets.empty? #$log.debug "inside select_field : #{ix0} ai #{@active_index}" f = @widgets[ix0] return if !f.focusable? if f.focusable? @active_index = ix0 @row, @col = f.rowcol #$log.debug " WMOVE insdie sele nxt field : ROW #{@row} COL #{@col} " on_enter f @window.wmove @row, @col # added RK FFI 2011-09-7 = setpos f.set_form_row # added 2011-10-5 so when embedded in another form it can get the cursor f.set_form_col # this can wreak havoc in containers, unless overridden # next line in field changes cursor position after setting form_col # resulting in a bug. 2011-11-25 # maybe it is in containers or tabbed panes and multi-containers # where previous objects col is still shown. we cannot do this after # setformcol #f.curpos = 0 # why was this, okay is it because of prev obj's cursor ? repaint @window.refresh else $log.debug "inside select field ENABLED FALSE : act #{@active_index} ix0 #{ix0}" end end
take focus to first focussable field we shoud not send to select_next. have a separate method to avoid bugs. but check current_field, in case called from anotehr field TODO FIXME
# File lib/canis/core/widgets/rwidget.rb, line 1730 def select_first_field # this results in on_leave of last field being executed when form starts. #@active_index = -1 # FIXME HACK #select_next_field ix = @focusables.first return unless ix # no focussable field # if the user is on a field other than current then fire on_leave if @active_index.nil? || @active_index < 0 elsif @active_index != ix f = @widgets[@active_index] begin #$log.debug " select first field, calling on_leave of #{f} #{@active_index} " on_leave f rescue => err $log.error " Caught EXCEPTION select_first_field on_leave #{err}" Ncurses.beep #$error_message = "#{err}" $error_message.value = "#{err}" return end end select_field ix end
take focus to last field on form
# File lib/canis/core/widgets/rwidget.rb, line 1756 def select_last_field @active_index = nil select_prev_field end
put focus on next field will cycle by default, unless navigation policy not :CYCLICAL in which case returns :NO_NEXT_FIELD. FIXME: in the beginning it comes in as -1 and does an on_leave
of last field
# File lib/canis/core/widgets/rwidget.rb, line 1858 def select_next_field return :UNHANDLED if @widgets.nil? || @widgets.empty? #$log.debug "insdie sele nxt field : #{@active_index} WL:#{@widgets.length}" if @active_index.nil? || @active_index == -1 # needs to be tested out A LOT # what is this silly hack for still here 2014-04-24 - 13:04 DELETE FIXME @active_index = -1 else f = @widgets[@active_index] begin on_leave f rescue FieldValidationException => err # added 2011-10-2 v1.3.1 so we can rollback $log.error "select_next_field: caught EXCEPTION #{err}" $error_message.value = "#{err}" raise err rescue => err $log.error "select_next_field: caught EXCEPTION #{err}" $log.error(err.backtrace.join("\n")) # $error_message = "#{err}" # changed 2010 $error_message.value = "#{err}" Ncurses.beep return 0 end end f = @widgets[@active_index] index = @focusables.index(f) # 2014-08-09 - 13:09 f may be status_line esp if ai is -1, so it is not found in focusables # why are we first checking widgets and then focusables. #index += 1 index = index ? index+1 : 0 f = @focusables[index] if f select_field f return 0 end # #$log.debug "insdie sele nxt field FAILED: #{@active_index} WL:#{@widgets.length}" ## added on 2008-12-14 18:27 so we can skip to another form/tab if @navigation_policy == :CYCLICAL f = @focusables.first if f select_field f return 0 end end $log.debug "inside sele nxt field : NO NEXT #{@active_index} WL:#{@widgets.length}" return :NO_NEXT_FIELD end
put focus on previous field will cycle by default, unless navigation policy not :CYCLICAL in which case returns :NO_PREV_FIELD. @return [nil, :NO_PREV_FIELD] nil if cyclical and it finds a field
if not cyclical, and no more fields then :NO_PREV_FIELD
# File lib/canis/core/widgets/rwidget.rb, line 1911 def select_prev_field return :UNHANDLED if @widgets.nil? or @widgets.empty? #$log.debug "insdie sele prev field : #{@active_index} WL:#{@widgets.length}" if @active_index.nil? @active_index = @widgets.length else f = @widgets[@active_index] begin on_leave f rescue => err $log.error " Caught EXCEPTION #{err}" Ncurses.beep # $error_message = "#{err}" # changed 2010 $error_message.value = "#{err}" return end end f = @widgets[@active_index] index = @focusables.index(f) if index > 0 index -= 1 f = @focusables[index] if f select_field f return end end ## added on 2008-12-14 18:27 so we can skip to another form/tab # 2009-01-08 12:24 no recursion, can be stack overflows if no focusable field if @navigation_policy == :CYCLICAL f = @focusables.last select_field @widgets.index(f) if f end return :NO_PREV_FIELD end
move cursor to where the fields row and col are private
# File lib/canis/core/widgets/rwidget.rb, line 1713 def setpos r=@row, c=@col #$log.debug "setpos : (#{self.name}) #{r} #{c} XXX" ## adding just in case things are going out of bounds of a parent and no cursor to be shown return if r.nil? or c.nil? # added 2009-12-29 23:28 BUFFERED return if r<0 or c<0 # added 2010-01-02 18:49 stack too deep coming if goes above screen @window.wmove r,c end
Form
New attempt at setting cursor using absolute coordinates Also, trying NOT to go up. let this pad or window print cursor.
# File lib/canis/core/widgets/rwidget.rb, line 1974 def setrowcol r, c @row = r unless r.nil? @col = c unless c.nil? end
2010-02-07 14:50 to aid in debugging and comparing log files.
# File lib/canis/core/widgets/rwidget.rb, line 2208 def to_s; @name || self; end
Defines how user can give numeric args to a command even in edit mode User either presses universal_argument
(C-u) which generates a series of 4 16 64. Or he presses C-u and then types some numbers. Followed by the action. @returns [0, :UNHANDLED] :UNHANDLED implies that last keystroke is still to evaluated by system. ) implies only numeric args were obtained. This method updates $multiplier
# File lib/canis/core/widgets/rwidget.rb, line 1996 def universal_argument $multiplier = ( ($multiplier.nil? || $multiplier == 0) ? 4 : $multiplier *= 4) $log.debug " inside UNIV MULT0: #{$multiplier} " # See if user enters numerics. If so discard existing varaible and take only #+ entered values _m = 0 while true ch = @window.getchar() case ch when -1 next when ?0.getbyte(0)..?9.getbyte(0) _m *= 10 ; _m += (ch-48) $multiplier = _m $log.debug " inside UNIV MULT #{$multiplier} " when ?\C-u.getbyte(0) if _m == 0 # user is incrementally hitting C-u $multiplier *= 4 else # user is terminating some numbers so he can enter a numeric command next return 0 end else $log.debug " inside UNIV MULT else got #{ch} " # here is some other key that is the function key to be repeated. we must honor this # and ensure it goes to the right widget return ch #return :UNHANDLED end end return 0 end
sets a flag that focusables should be updated called whenever a widgets changes its focusable property
# File lib/canis/core/widgets/rwidget.rb, line 1660 def update_focusables $log.debug "XXX: inside update focusables" @focusable_modified = true end
run validate_field
on a field, usually whatevers current before transferring control We should try to automate this so developer does not have to remember to call it. # @param field object @return [0, -1] for success or failure NOTE : catches exception and sets $error_message, check if -1
# File lib/canis/core/widgets/rwidget.rb, line 1841 def validate_field f=@widgets[@active_index] begin on_leave f rescue => err $log.error "form: validate_field caught EXCEPTION #{err}" $log.error(err.backtrace.join("\n")) # $error_message = "#{err}" # changed 2010 $error_message.value = "#{err}" Ncurses.beep return -1 end return 0 end