Class: Umbra::Form

Inherits:
Object
  • Object
show all
Includes:
EventHandler, KeyMappingHandler
Defined in:
lib/umbra/form.rb

Instance Attribute Summary collapse

Instance Method Summary collapse

Methods included from KeyMappingHandler

#_process_key, #bind_key, #bind_keys, #process_key, #unbind_key

Methods included from EventHandler

#bind_event, #event?, #fire_handler, #fire_property_change, #register_events

Constructor Details

#initialize(win, &block) ⇒ Form

Returns a new instance of Form.



38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
# 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

Instance Attribute Details

#active_indexObject

index of active widget inside focusables array



31
32
33
# File 'lib/umbra/form.rb', line 31

def active_index
  @active_index
end

#focusablesObject (readonly)

array of widgets, and those that can be traversed



21
22
23
# File 'lib/umbra/form.rb', line 21

def focusables
  @focusables
end

#nameObject

name given to form for debugging



34
35
36
# File 'lib/umbra/form.rb', line 34

def name
  @name
end

#widgetsObject (readonly)

array of widgets, and those that can be traversed



21
22
23
# File 'lib/umbra/form.rb', line 21

def widgets
  @widgets
end

#windowObject

related window pointer used for printing or other FFI calls



24
25
26
# File 'lib/umbra/form.rb', line 24

def window
  @window
end

Instance Method Details

#add_widget(*widget) ⇒ Form

Add given widget to widget list and returns self A widget must be added to a Form for it to be painted and focussed.

Parameters:

  • widget (Widget)

    to display on form

Returns:

  • (Form)

    pointer to self



60
61
62
63
64
65
66
67
68
69
70
# 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

#get_current_fieldWidget? Also known as: current_widget

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.

Returns:

  • (Widget, nil)

    current field, nil if no focusable field



192
193
194
195
196
197
198
199
200
# 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

#handle_key(ch) ⇒ Object

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.

NOTE : please rescue exceptions when you use this in your main loop and alert() user



352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
# 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_keysObject

NOTE: These mappings will only trigger if the current field

does not use them in handle_key


315
316
317
318
319
320
# 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

#packObject

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



90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
# 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) ⇒ Object

remove a widget from form. Will not be displayed or focussed.

Parameters:

  • widget (Widget)

    to remove from form



75
76
77
78
# File 'lib/umbra/form.rb', line 75

def remove_widget widget
  @widgets.delete widget
  @focusables.delete widget
end

#repaintObject

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.



163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
# 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_widgetsObject

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



328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
# 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_first_fieldObject

take focus to first focusable field



205
206
207
# File 'lib/umbra/form.rb', line 205

def select_first_field
  select_field 0
end

#select_last_fieldObject

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



211
212
213
214
215
216
# 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_fieldObject

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



225
226
227
228
229
230
231
232
233
234
# 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_fieldnil, :NO_PREV_FIELD

put focus on previous field will cycle by default, unless navigation policy not :CYCLICAL in which case returns :NO_PREV_FIELD.

Returns:

  • (nil, :NO_PREV_FIELD)

    nil if cyclical and it finds a field if not cyclical, and no more fields then :NO_PREV_FIELD



243
244
245
246
247
248
249
250
251
252
253
254
# 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) ⇒ Object Also known as: select_field

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



127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
# 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

#to_sObject

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



442
# File 'lib/umbra/form.rb', line 442

def to_s; @name || self; end

#update_focusablesObject

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



82
83
84
85
86
# 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