class Umbra::Form

Attributes

active_index[RW]

index of active widget inside focusables array

focusables[R]

array of widgets, and those that can be traversed

name[RW]

name given to form for debugging

widgets[R]

array of widgets, and those that can be traversed

window[RW]

related window pointer used for printing or other FFI calls

Public Class Methods

new(win, &block) click to toggle source
# File lib/umbra/form.rb, line 38
def initialize win, &block
  @window = win
  @widgets = []
  @active_index = nil
  @row = @col = 0                    # 2018-03-07 - umbra
  @focusables = []                   # focusable components
  register_events(:RESIZE)
  instance_eval &block if block_given?
  @name ||= ""                       # for debugging

  # for storing error message NOT_SURE
  #$error_message ||= Variable.new ""

  map_keys unless @keys_mapped
  $log ||= create_logger '/dev/null'    ## FIXME temporarily adding here. If user has not
                                        ##  specified a logger, create a quiet one.
end

Public Instance Methods

add_widget(*widget) click to toggle source

Add given widget to widget list and returns self A widget must be added to a Form for it to be painted and focussed. @param [Widget] widget to display on form @return [Form] pointer to self

# File lib/umbra/form.rb, line 60
def add_widget *widget
  widget.each do |w|
    next if @widgets.include? w
    # NOTE: if form created with nil window (messagebox), then this would have to happen later
    w.graphic = @window if @window # 2018-03-19 - prevent widget from needing to call form back
    $log.warn "Window is nil in form.add_widget" unless @window
    w._form = self    # 2018-04-20 - so that update_focusables can be called.
    @widgets << w
  end
  return self
end
current_widget()
Alias for: get_current_field
get_current_field() click to toggle source

@return [Widget, nil] current field, nil if no focusable field

NOTE 2018-05-17 - this is called by form in the very beginning when no field is actually focussed
  but active_index has been set to 0, so the on_enter has not been executed, but the handle_key
  is invoked.
# File lib/umbra/form.rb, line 192
def get_current_field

  ##  rewrite on 2018-05-18 - so that on_enter is called for first field
  if @_focussed_widget.nil?             ## when form handle_key first called
    select_widget @focusables.first
  end
  return @_focussed_widget

end
Also aliased as: current_widget
handle_key(ch) click to toggle source
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/umbra/form.rb, line 352
def handle_key(ch)
  handled = :UNHANDLED 

  case ch
  when -1
    #repaint # only for continuous updates, and will need to use wtimeout and not nodelay in getch
    return
  when 1000, 18  # what if someone has trapped this.
    # 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, 18 is C-r
    $log.debug " form REFRESH_ALL repaint_all HK #{ch} #{self}, #{@name} "
    FFI::NCurses.endwin
    @window.wrefresh    ## endwin must be followed by refresh
    @window.repaint
    repaint_all_widgets
    @window.wrefresh
    return 0
  when FFI::NCurses::KEY_RESIZE  # SIGWINCH # UNTESTED XXX
    ## NOTE: this works but boxes are not resized since hardcoded height and width were given.
    ## 2018-05-13 - only if a layout is used, can a recalc happen.
    # 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.
    
    # next line may be causing flicker, can we do without.
    FFI::NCurses.endwin
    @window.wrefresh
    @window.wclear
    lines = FFI::NCurses.LINES
    cols = FFI::NCurses.COLS
    #x = FFI::NCurses.stdscr.getmaxy
    x = @window.getmaxy
    #y = FFI::NCurses.stdscr.getmaxx
    y = @window.getmaxx
    @window.wresize(x,y)
    $log.debug " form RESIZE SIGWINCH 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} "

    repaint_all_widgets
    #if @layout_manager
      #@layout_manager.do_layout
      ## we need to redo statusline and others that layout ignores
    #else
    #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 
    @window.wrefresh
  else
    field =  get_current_field
    handled = :UNHANDLED 
    handled = field.handle_key ch unless field.nil? # no field focussable
    ## next line "field" can print entire content of a list or table if to_s is large
    $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 FFI::NCurses::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
        FFI::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
  ret || 0  # 2011-10-17
end
map_keys() click to toggle source

NOTE: These mappings will only trigger if the current field

does not use them in handle_key
# File lib/umbra/form.rb, line 315
def map_keys
  return if @keys_mapped
  #bind_key(FFI::NCurses::KEY_F1, 'help') { hm = help_manager(); hm.display_help }
  #bind_key(FFI::NCurses::KEY_F9, "Print keys", :print_key_bindings) # show bindings, tentative on F9
  @keys_mapped = true
end
pack() click to toggle source

Decide layout of objects. User has to call this after creating components More may come here.

# File lib/umbra/form.rb, line 90
def pack

  update_focusables

  # set up hotkeys for buttons and labels with mnemonics and labels.
  @widgets.each do |w|
    #$log.debug "  FOCUSABLES #{w.name} #{w.to_s} #{w.class}"
    if w.respond_to? :mnemonic
      if w.mnemonic
        ch = w.mnemonic.downcase()[0].ord
        # meta key
        mch = ?\M-a.getbyte(0) + (ch - ?a.getbyte(0))

        if w.respond_to? :fire
          #$log.debug "  setting hotkey #{mch} to button #{w} "
          self.bind_key(mch, "hotkey for button #{w} ") { w.fire }
        else
          # case of labels and labeled field
          #$log.debug "  setting hotkey #{mch} to field #{w} "
          self.bind_key(mch, "hotkey for field #{w.related_widget} ") { 
            
            #$log.debug "  HOTKEY got key #{mch} : for #{w.related_widget} "
            self.select_field w.related_widget }
        end
      end
    end
  end
  @active_index = 0 if @focusables.size > 0
  # 2018-04-14 - why the repaint here ? commenting off. Gave error in messagbox if no window yet.
  #repaint
  self
end
remove_widget(widget) click to toggle source

remove a widget from form. Will not be displayed or focussed. @param [Widget] widget to remove from form

# File lib/umbra/form.rb, line 75
def remove_widget widget
  @widgets.delete widget
  @focusables.delete widget
end
repaint() click to toggle source

form repaint,calls repaint on each widget which will repaint it only if it has been modified since last call. called after each keypress and on select_field.

# File lib/umbra/form.rb, line 163
def repaint
  $log.debug " form repaint:#{self}, #{@name} , r #{@row} c #{@col} " if $log.debug? 
  @widgets.each do |f|
    next if f.visible == false
    #f.repaint
    # changed on 2018-03-21 - so widgets don't need to do this.
    if f.repaint_required
      f.graphic = @window unless f.graphic   # messageboxes may not have a window till very late
      f.repaint 
      f.repaint_required = false
      f.instance_variable_set(:@_object_created, true)   ## after this property_change handlers will fire
    end
  end

  # get curpos of active widget 2018-03-21 - form is taking control of this now.
  f = get_current_field
  if f
    @row, @col = f.rowcol
    _setpos 
  end
  @window.wrefresh
end
repaint_all_widgets() click to toggle source

repaint all # {{{ 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/umbra/form.rb, line 328
def repaint_all_widgets
  $log.debug "  REPAINT ALL in FORM called "
  #raise "it has come to repaint_all"
  @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
select_field(fld)
Alias for: select_widget
select_first_field() click to toggle source

take focus to first focusable field

# File lib/umbra/form.rb, line 205
def select_first_field
  select_field 0
end
select_last_field() click to toggle source

take focus to last field on form 2018-03-08 - WHY IS THIS REQUIRED NOT_SURE

# File lib/umbra/form.rb, line 211
def select_last_field
  raise
  return nil if @active_index.nil?   # for forms that have no focusable field 2009-01-08 12:22
  i = @focusables.length -1
  select_field i
end
select_next_field() click to toggle source

put focus on next field will cycle by default, unless navigation policy not :CYCLICAL in which case returns :NO_NEXT_FIELD. 2018-03-07 - UMBRA: let us force user to run validation when he does next field

# File lib/umbra/form.rb, line 225
def select_next_field
  return :UNHANDLED if @focusables.nil? || @focusables.empty?
  index = nil
  if @_focussed_widget
     index = @focusables.index @_focussed_widget
  end
  index = index ? index+1 : 0
  index = 0 if index >= @focusables.length # CYCLICAL 2018-03-11 -
  select_widget @focusables[index]
end
select_prev_field() click to toggle source

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/umbra/form.rb, line 243
def select_prev_field
  return :UNHANDLED if @focusables.nil? or @focusables.empty?
  index = nil
  if @_focussed_widget
     index = @focusables.index @_focussed_widget
     else
     index = @focusables.length 
  end
  index -= 1
  index = @focusables.length-1 if index < 0 # CYCLICAL 2018-03-11 -
  select_widget @focusables[index]
end
select_widget(fld) click to toggle source

set focussed widget to given fld. This ensures that whenever a widget is given focus, the on_leave of the previous widget

is called, and the on_enter of this field is called.

2018-05-18 - rewrite of select_field which did not call on_leave

# File lib/umbra/form.rb, line 127
def select_widget fld

  return nil unless fld   ## no focusable

  if fld.is_a? Integer
    fld = @focusables[fld]
  end

  return unless fld.focusable

  ## leave existing widget if there was one
  fw =  @_focussed_widget
  return if fw == fld

  if fw
    on_leave fw
  end


  ## enter given widget
  on_enter fld
  @_focussed_widget = fld
  ix = @focusables.index fld
  @active_index = ix

  @row, @col = fld.rowcol
  _setrowcol @row, @col 
  repaint # 2018-03-21 - handle_key calls repaint, is this for cases not involving keypress ?
  @window.refresh
end
Also aliased as: select_field
to_s() click to toggle source

2010-02-07 14:50 to aid in debugging and comparing log files.

# File lib/umbra/form.rb, line 442
def to_s; @name || self; end
update_focusables() click to toggle source

maintain a list of focusable objects so form can traverse between them easily.

# File lib/umbra/form.rb, line 82
def update_focusables
  #$log.debug "1 inside update_focusables #{@focusables.count} "
  @focusables = @widgets.select { |w| w.focusable }
  #$log.debug "2  inside update_focusables #{@focusables.count} "
end

Private Instance Methods

_setpos(r=@row, c=@col) click to toggle source

move cursor to where the fields row and col are

# File lib/umbra/form.rb, line 269
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
_setrowcol(r, c) click to toggle source

New attempt at setting cursor using absolute coordinates Also, trying NOT to go up. let this pad or window print cursor. 2018-03-21 - we should prevent other widgets from calling this. Tehy need to set their own offsets so form picks up the correct one. 2018-03-21 - renamed to _setrowcol so other programs calling it will bork.

# File lib/umbra/form.rb, line 262
def _setrowcol r, c
  @row = r unless r.nil?
  @col = c unless c.nil?
end
on_enter(f) click to toggle source

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 2018-03-07 - NOT_SURE

# File lib/umbra/form.rb, line 294
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_color_pair || f.highlight_attr
    f.repaint_required = true
  end

  f.modified = false
  f.on_enter if f.respond_to? :on_enter
end
on_leave(f) click to toggle source

form's trigger, fired when any widget loses focus NOTE: Do NOT override

This wont get called in editor components in tables, since  they are formless
# File lib/umbra/form.rb, line 280
def on_leave f
  return if f.nil? || !f.focusable # added focusable, else label was firing
  $log.debug "Form setting state of #{f.name} to NORMAL"
  f.state = :NORMAL
  # 2018-03-11 - trying out, there can be other things a widget may want to do on entry and exit
  if f.highlight_color_pair || f.highlight_attr
    f.repaint_required = true
  end
  f.on_leave if f.respond_to? :on_leave
end