Class: Umbra::Multiline

Inherits:
Widget
  • Object
show all
Defined in:
lib/umbra/multiline.rb

Overview

Base class for widgets that have multiple lines and are scrollable. Preferably, use a concrete class such as Listbox, Table or Textbox. This is not editable by default. To delete or insert rows, set editable to true.

Direct Known Subclasses

Listbox, Table, Textbox

Instance Attribute Summary collapse

Attributes inherited from Widget

#col, #col_offset, #curpos, #focusable, #graphic, #handler, #key_label, #modified, #name, #repaint_required, #row, #row_offset, #state

Instance Method Summary collapse

Methods inherited from Widget

#_form=, #color_pair, #getvalue_for_paint, #height, #highlight_attr, #init_vars, #modified?, #repaint_all, #rowcol, #set_form_col, #set_form_row, #text, #touch, #variable_set, #visible, #width

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(config = {}, &block) ⇒ Multiline

{{{



35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
# File 'lib/umbra/multiline.rb', line 35

def initialize config={}, &block    # {{{
  @focusable          = false
  @editable           = false
  @pstart             = 0                  # which row does printing start from
  @current_index      = 0                  # index of row on which cursor is
  @search_offset      = 0                  # search has no offset

  ## PRESS event relates to pressing RETURN/ENTER (10)
  register_events([:LEAVE_ROW, :ENTER_ROW, :PRESS])

  super

  map_keys
  @row_offset         = 0
  @panned_cols               = 0     ## how many columns the view has panned.
                              ## The name means panned_cols
  @curpos             = 0     ## Widget defines an accessor on this.
  @repaint_required   = true
end

Instance Attribute Details

#current_indexObject (readonly)

index of focussed row, starting 0, index into the list supplied



33
34
35
# File 'lib/umbra/multiline.rb', line 33

def current_index
  @current_index
end

#listObject

array containing data (usually Strings)



26
27
28
# File 'lib/umbra/multiline.rb', line 26

def list
  @list
end

#panned_colsObject (readonly)

How may columns has widget panned to right



27
28
29
# File 'lib/umbra/multiline.rb', line 27

def panned_cols
  @panned_cols
end

#search_offsetObject

adjustment for searching for a pattern in a line, if adding text to start of string. Example, listbox adds one char to start. One may override and add two.



30
31
32
# File 'lib/umbra/multiline.rb', line 30

def search_offset
  @search_offset
end

Instance Method Details

#_truncate_to_width(ff) ⇒ Object

truncate string to width, and handle panning {{{



241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
# File 'lib/umbra/multiline.rb', line 241

def _truncate_to_width ff
  _width = self.width
  if ff
    if ff.size > _width
      # panned_cols can be greater than width then we get null
      if @panned_cols < ff.size
        ff = ff[@panned_cols..@panned_cols+_width-1] 
      else
        ff = ""
      end
    else
      if @panned_cols < ff.size
        ff = ff[@panned_cols..-1]
      else
        ff = ""
      end
    end
  end 
  ff = "" unless ff
  return ff
end

#ask_searchObject

Ask user for a pattern to look for in lines. Put cursor on first match.



531
532
533
534
535
536
537
538
539
540
541
542
543
# File 'lib/umbra/multiline.rb', line 531

def ask_search
  str = get_string("Search:")
  return unless str
  ix = next_match str
  if ix
    @current_index, @curpos = ix
    set_col_offset @curpos   ## why do we need to do this also
    $log.debug "  ask_search ci: #{@current_index}  , #{@curpos} "
    @last_regex = str
  else
    alert "Pattern not found: #{str}"
  end
end

#color_of_row(index, state) ⇒ Object Also known as: _format_color

Each row can be in one of the following states:

1. HIGHLIGHTED: cursor is on the row, and the list is focussed (user is in it)
2. CURRENT    : cursor was on this row, now user has exited the list
3. SELECTED   : user has selected this row (this can also have above two states actually)
4. NORMAL     : All other rows: not selected, not under cursor

returns color, attrib for given row

Parameters:

  • index

    of row in the list

  • state

    of row in the list (see above states)



193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
# File 'lib/umbra/multiline.rb', line 193

def color_of_row index, state
  arr = case state
          #when :SELECTED
          #[@selected_color_pair, @selected_attr]
        when :HIGHLIGHTED
          [@highlight_color_pair || CP_WHITE, @highlight_attr || REVERSE]
        when :CURRENT
          [@color_pair, @attr]
        when :NORMAL
          _color = CP_CYAN
          _color = CP_WHITE if index % 2 == 0
          #_color = @alt_color_pair if index % 2 == 0
          [@color_pair || _color, @attr || NORMAL]
        end
  return arr
end

#command(*args, &block) ⇒ Object

convenience method for calling most used event of a widget Called by user programs.



475
476
477
# File 'lib/umbra/multiline.rb', line 475

def command *args, &block
  bind_event :ENTER_ROW, *args, &block
end

#current_rowString

returns current row as String 2018-04-11 - NOTE this may not be a String so we convert it to string before returning

Returns:

  • (String)

    row the cursor/user is on



359
360
361
362
# File 'lib/umbra/multiline.rb', line 359

def current_row
  s = @list[@current_index]
  value_of_row s, @current_index, :CURRENT
end

#cursor_backwardObject



374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
# File 'lib/umbra/multiline.rb', line 374

def cursor_backward

  if @col_offset > 0
    @curpos -= 1
    add_col_offset -1
  else
    # cur is on the first col, then scroll left
    if @panned_cols > 0
      @panned_cols -= 1
      @curpos -= 1
    else
      # do nothing
    end
  end
end

#cursor_downObject

go to next row



333
334
335
# File 'lib/umbra/multiline.rb', line 333

def cursor_down
  @current_index += 1
end

#cursor_endObject

goto end of line. This should be consistent with moving the cursor to the end of the row with right arrow



344
345
346
347
348
349
350
351
352
353
354
355
# File 'lib/umbra/multiline.rb', line 344

def cursor_end
  blen = current_row().length
  if blen < self.width
    set_col_offset blen # just after the last character
    @panned_cols = 0
  else
    @panned_cols = blen-self.width #+2  # 2 is due to mark and space XXX could be a problem with textbox
    set_col_offset blen # just after the last character
  end
  @curpos = blen # this is position in array where editing or motion is to happen regardless of what you see
  # regardless of panned_cols (panning)
end

#cursor_forwardObject

move cursor forward one character, called with KEY_RIGHT action.



364
365
366
367
368
369
370
371
372
373
# File 'lib/umbra/multiline.rb', line 364

def cursor_forward
  blen = current_row().size # -1
  if @curpos < blen
    if add_col_offset(1)==-1  # go forward if you can, else scroll
      #@panned_cols += 1 if @panned_cols < self.width 
      @panned_cols += 1 if @panned_cols < blen
    end
    @curpos += 1
  end
end

#cursor_homeObject

position cursor at start of field



337
338
339
340
341
# File 'lib/umbra/multiline.rb', line 337

def cursor_home
  @curpos  = 0 
  @panned_cols    = 0
  set_col_offset 0
end

#cursor_upObject



329
330
331
# File 'lib/umbra/multiline.rb', line 329

def cursor_up
  @current_index -= 1
end

#delete_at(index) ⇒ Object

UNTESTED



614
615
616
617
618
619
620
# File 'lib/umbra/multiline.rb', line 614

def delete_at index
  return unless @list
  return unless @editable
  #@repaint_all = true
  @repaint_required = true
  @list.delete_at index
end

#ensure_visible(_row = @current_index) ⇒ Object

Ensure current row is visible, if not make it first row NOTE - need to check if its at end and then reduce scroll at rows, check_prow does that

UNTESTED

Parameters:

  • current_index (default if not given)


509
510
511
512
513
# File 'lib/umbra/multiline.rb', line 509

def ensure_visible _row = @current_index
  unless is_visible? _row
    @pstart = _row
  end
end

#find_moreObject

find more occurrences of match. This is bound to ‘n’ key.



546
547
548
549
550
551
552
553
554
555
556
557
558
# File 'lib/umbra/multiline.rb', line 546

def find_more
  return unless @last_regex
  str = @last_regex
  $log.debug "  FIND MORE last_regex is : #{@last_regex} "
  ix = next_match @last_regex
  #return unless ix
  if ix
    @current_index, @curpos = ix
    set_col_offset @curpos   ## why do we need to do this also
  else
    alert "No more matches for: #{str}"
  end
end

#fire_action_eventObject

event when user hits ENTER on a row, user would bind :PRESS callers may use w.current_index or w.current_row or w.curpos.

obj.bind :PRESS { |w| w.current_row }


485
486
487
488
489
490
491
# File 'lib/umbra/multiline.rb', line 485

def fire_action_event
  return if @list.nil? || @list.size == 0
  #require 'canis/core/include/ractionevent'
  #aev = text_action_event
  #fire_handler :PRESS, aev
  fire_handler :PRESS, self
end

#getvalueObject



89
90
91
# File 'lib/umbra/multiline.rb', line 89

def getvalue
  @list
end

#goto_endObject

go to end of file (last line)



430
431
432
433
# File 'lib/umbra/multiline.rb', line 430

def goto_end
  @current_index = @list.size-1
  @panned_cols = @curpos = 0
end

#goto_line(line) ⇒ Object

NOTE what about firing handlers when moving rows, i suppose that these will be called from a binding. UNTESTED



517
518
519
520
521
# File 'lib/umbra/multiline.rb', line 517

def goto_line line
  return if line < 0 or line >= self.row_count
  @current_index = line
  ensure_visible line
end

#goto_startObject

go to start of file (first line)



424
425
426
427
428
# File 'lib/umbra/multiline.rb', line 424

def goto_start
  @current_index = 0
  @panned_cols = @curpos = 0
  set_col_offset 0
end

#handle_key(ch) ⇒ Object

Multiline key handling. {{{ Called by form from form’s handle_key when this object is in focus.

Parameters:

  • ch: (Integer)

    key caught by getch of window



452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
# File 'lib/umbra/multiline.rb', line 452

def handle_key ch
  old_current_index = @current_index
  old_panned_cols = @panned_cols
  old_col_offset = @col_offset

  ret = super
  return ret
ensure
  ## NOTE: it is possible that a block called above may have cleared the list.
  ##  In that case, the on_enter_row will crash. I had put a check here, but it 
  ##    has vanished ???
  @current_index = 0 if @current_index < 0
  @current_index = @list.size-1 if @current_index >= @list.size
  if @current_index != old_current_index
    on_leave_row old_current_index
    on_enter_row @current_index
    @repaint_required = true  
  end
  @repaint_required = true if old_panned_cols != @panned_cols or old_col_offset != @col_offset
end

#insert(index, line) ⇒ Object

UNTESTED



623
624
625
626
627
628
629
# File 'lib/umbra/multiline.rb', line 623

def insert index, line
  return unless @list
  return unless @editable
  #@repaint_all = true
  @repaint_required = true
  @list.insert index, line
end

#is_visible?(_row) ⇒ Boolean

Is the given row visible UNTESTED

Returns:

  • (Boolean)


498
499
500
501
# File 'lib/umbra/multiline.rb', line 498

def is_visible? _row
  j = _row - @pstart
  j >= 0 && j <= (self.height - 1)
end

#map_keysObject

mapping of keys for multiline {{{



265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
# File 'lib/umbra/multiline.rb', line 265

def map_keys
  return if @keys_mapped
  bind_keys([?k,FFI::NCurses::KEY_UP], "Up")         { cursor_up }
  bind_keys([?j,FFI::NCurses::KEY_DOWN], "Down")     { cursor_down }
  bind_keys([?l,FFI::NCurses::KEY_RIGHT], "Right")   { cursor_forward }
  bind_keys([?h,FFI::NCurses::KEY_LEFT], "Left")     { cursor_backward }
  bind_key(?g, 'goto_start')                         { goto_start }
  bind_key(?G, 'goto_end')                           { goto_end }
  bind_key(FFI::NCurses::KEY_CTRL_A, 'cursor_home')  { cursor_home }
  bind_key(FFI::NCurses::KEY_CTRL_E, 'cursor_end')   { cursor_end }
  bind_key(FFI::NCurses::KEY_CTRL_F, 'page_forward') { page_forward }
  bind_key(32, 'page_forward')                       { page_forward }
  bind_key(FFI::NCurses::KEY_CTRL_B, 'page_backward'){ page_backward }
  bind_key(FFI::NCurses::KEY_CTRL_U, 'scroll_up')    { scroll_up }
  bind_key(FFI::NCurses::KEY_CTRL_D, 'scroll_down')  { scroll_down }
  ## C-h was not working, so trying C-j
  bind_key(FFI::NCurses::KEY_CTRL_J, 'scroll_left')  { scroll_left }
  bind_key(FFI::NCurses::KEY_CTRL_L, 'scroll_right')  { scroll_right }
  bind_key(?/, 'ask search')                         { ask_search }
  bind_key(?n, 'next match ')                        { find_more }
  @keys_mapped = true
end

#next_match(str, startline = nil, _curpos = nil, endline = nil) ⇒ Object

find the first or next occurrence of a pattern

Parameters:

  • str (String)

    pattern to match

  • startline (Integer) (defaults to: nil)

    line to start search on

  • curpos (Integer)

    cursor position to start search on

  • endline (Integer) (defaults to: nil)

    line to end search on



565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
# File 'lib/umbra/multiline.rb', line 565

def next_match str, startline = nil,  _curpos = nil, endline = nil
  return unless str

  ## check current line for more occurrences.
  if !startline
    startline = @current_index
    _curpos ||= (@curpos + 1) # FIXME +1 should only happen if a search has already happened
    #_pos = @list[startline].index(str, _curpos)
    _pos = to_searchable(startline).index(str, _curpos)
    return [startline, _pos + search_offset] if _pos
    startline += 1
  end
  ## Check rest of file
  ## loop through array, check after startline to eof
  @list.each_with_index do | line, ix|
    next if ix < startline
    break if endline && ix > endline
    #_found = line.index(str)
    _found = to_searchable(ix).index(str)
    #$log.debug "  next_match: #{line}: #{_found}  " if _found
    return [ix, _found + search_offset] if _found
  end
  if startline > 0
    # this can get called again since startline was made 1 in above block. FIXME
    #return next_match str, 0, @current_index
  end
  return nil
end

#on_enterObject

on enter of this multiline



289
290
291
292
293
294
295
# File 'lib/umbra/multiline.rb', line 289

def on_enter
  super
  on_enter_row @current_index
  # basically I need to only highlight the current index, not repaint all OPTIMIZE 
  #touch ; repaint ## 2018 why was i calling repaint here ??? causing error in messagebox since window nil
  touch 
end

#on_enter_row(index) ⇒ Object

called whenever a row entered. Call when object entered, also.



312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
# File 'lib/umbra/multiline.rb', line 312

def on_enter_row index
  fire_handler(:ENTER_ROW, [@current_index])     # 2018-03-26 - improve this
  # if cursor ahead of blen then fix it
  blen = current_row().size-1
  ## why -1 on above line. Empty lines will give -1
  blen = 0 if blen < 0
  if @curpos > blen
    @col_offset = blen - @panned_cols 
    @curpos = blen
    if @panned_cols > blen
      @panned_cols = blen - self.width  ## @int_width 2018-05-22 - 
      @panned_cols = 0 if @panned_cols < 0
      @col_offset = blen - @panned_cols 
    end
  end
  @col_offset = 0 if @col_offset < 0
end

#on_leaveObject

on leave of this multiline



298
299
300
301
302
303
304
# File 'lib/umbra/multiline.rb', line 298

def on_leave
  super
  on_leave_row @current_index
  # basically I need to only unhighlight the current index, not repaint all OPTIMIZE 
  #touch ; repaint
  touch ##; repaint  ## why repaint here ?? when was this necessary ?
end

#on_leave_row(index) ⇒ Object

called when user leaves a row and when object is exited.



307
308
309
# File 'lib/umbra/multiline.rb', line 307

def on_leave_row index
  fire_handler(:LEAVE_ROW, [index])     # 2018-03-26 - improve this
end

#page_backwardObject



440
441
442
# File 'lib/umbra/multiline.rb', line 440

def page_backward
  @current_index -= @page_lines
end

#page_forwardObject



443
444
445
# File 'lib/umbra/multiline.rb', line 443

def page_forward
  @current_index += @page_lines
end

#paint_row(win, row, col, line, ctr) ⇒ Object

Paint given row. {{{ This is not be be called by user, but may be overridden if caller wishes

to completely change the presentation of each row. In most cases, it should suffice
to override just +print_row+ or +value_of_row+ or +_format_color+.

Parameters:

  • window (Window)

    pointer for printing

  • row (Integer)

    number to print on

  • col: (Integer)

    column to print on

  • line (String)

    to be printed, usually String. Whatever was passed in to list method.

  • ctr: (Integer)

    offset of row starting zero

  • state: (String)

    state of row (SELECTED CURRENT HIGHLIGHTED NORMAL)



165
166
167
168
169
170
171
172
173
174
# File 'lib/umbra/multiline.rb', line 165

def paint_row(win, row, col, line, ctr)

  state = state_of_row(ctr)     

  ff = value_of_row(line, ctr, state)

  ff = _truncate_to_width( ff )   ## truncate and handle panning

  print_row(win, row, col, ff, ctr, state)
end

do the actual printing of the row, depending on index and state This method starts with underscore since it is only required to be overriden if an object has special printing needs.



180
181
182
183
# File 'lib/umbra/multiline.rb', line 180

def print_row(win, row, col, str, index, state)
  arr = color_of_row index, state
  win.printstring(row, col, str, arr[0], arr[1])
end

#repaintObject

repaints the entire multiline, called by form {{{



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
122
123
124
125
126
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
# File 'lib/umbra/multiline.rb', line 96

def repaint 
  _calc_dimensions unless @_calc_dimensions

  return unless @repaint_required
  return unless @list
  win                 = @graphic
  raise "window nil in multiline" unless win
  r,c                 = self.row, self.col 
  _attr               = @attr || NORMAL
  _color              = @color_pair || CP_WHITE
  curpos              = 1
  coffset             = 0

  rows               = getvalue 

  ht                  = self.height
  cur                 = @current_index
  st                  = pstart = @pstart           # previous start
  pend = pstart + ht -1                            # previous end
  if cur > pend
    st = (cur -ht) + 1 
  elsif cur < pstart
    st = cur
  end
  $log.debug "REPAINT #{self.class} : cur = #{cur} st = #{st} pstart = #{pstart} pend = #{pend} listsize = #{@list.size} "
  $log.debug "REPAINT ML : row = #{r} col = #{c} width = #{width}/#{@width},  height = #{ht}/#{@height} ( #{FFI::NCurses.COLS}   #{FFI::NCurses.LINES} "
  y = 0
  ctr = 0
  filler = " "*(self.width)
  rows.each_with_index {|_f, y| 
    next if y < st

    curpos = ctr if y == cur                                         ## used for setting row_offset

    #_state = state_of_row(y)     ## XXX should be move this into paint_row

    win.printstring(ctr + r, coffset+c, filler, _color )            ## print filler

    #paint_row( win, ctr+r, coffset+c, _f, y, _state)
    paint_row( win, ctr+r, coffset+c, _f, y)


    ctr += 1 
    @pstart = st
    break if ctr >= ht 
  }
  ## if counter < ht then we need to clear the rest in case there was data earlier {{{
  if ctr < ht
    while ctr < ht
      win.printstring(ctr + r, coffset+c, filler, _color )
      ctr += 1
    end
  end # }}}
  @row_offset = curpos                             ## used by +widget+ in +rowcol+ called by +Form+
  #@col_offset = coffset    ## NOTE listbox had this line, but it interferes with textbox
  @repaint_required = false
end

#row_countObject



72
73
74
# File 'lib/umbra/multiline.rb', line 72

def row_count
  @list.size
end

#scroll_downObject



434
435
436
# File 'lib/umbra/multiline.rb', line 434

def scroll_down
  @current_index += @scroll_lines
end

#scroll_leftObject

cursor_backward



420
421
422
# File 'lib/umbra/multiline.rb', line 420

def scroll_left  ##cursor_backward
  @panned_cols -= 1 if @panned_cols > 0
end

#scroll_rightObject

cursor_forward



416
417
418
419
# File 'lib/umbra/multiline.rb', line 416

def scroll_right ## cursor_forward
  blen = current_row().size-1
  @panned_cols += 1 if @panned_cols < blen
end

#scroll_upObject



437
438
439
# File 'lib/umbra/multiline.rb', line 437

def scroll_up
  @current_index -= @scroll_lines
end

#state_of_row(ix) ⇒ Object



223
224
225
226
227
228
229
230
231
232
233
234
235
236
# File 'lib/umbra/multiline.rb', line 223

def state_of_row ix
  _st = :NORMAL
  cur = @current_index
  if ix == cur # current row, row on which cursor is or was
    ## highlight only if object is focussed, otherwise just show mark
    if @state == :HIGHLIGHTED
      _st = :HIGHLIGHTED
    else
      ## cursor was on this row, but now user has tabbed out
      _st = :CURRENT
    end
  end
  return _st
end

#to_searchable(index) ⇒ String

This needs to be overridden in case of lists that contain something other than ‘String` as their elements. In such a case, `search_offset` may need to be adjusted also.

Parameters:

  • index (Integer)

    offset of row in list

Returns:

  • (String)

    searchable representation of list element at ‘index`



599
600
601
602
603
604
605
606
607
608
609
610
# File 'lib/umbra/multiline.rb', line 599

def to_searchable index
  s = @list[index]
  case s
  when String
    return s                                       ## return the string as is.
  when Array
                                                   ## may need to be overridden by caller
    return s.join(" ")
  else
    raise "Don't know how to handle this datatype, please override to_searchable"
  end
end

#value_of_row(line, ctr, state) ⇒ String Also known as: _format_value

how to convert the line of the array to a simple String. This is only required to be overridden if the list passed in is not an array of Strings.

Parameters:

  • the

    current row which could be a string or array or whatever was passed in in list=().

Returns:

  • (String)

    string to print. A String must be returned.



218
219
220
# File 'lib/umbra/multiline.rb', line 218

def value_of_row line, ctr, state
  line
end