ncumbra 0.1.0 → 0.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,476 @@
1
+ require 'umbra/widget'
2
+ # ----------------------------------------------------------------------------- #
3
+ # File: multiline.rb
4
+ # Description: A base class for lists and textboxes and tables, i.e. components
5
+ # having multiple lines that are scrollable.
6
+ # Author: j kepler http://github.com/mare-imbrium/canis/
7
+ # Date: 2018-05-08 - 11:54
8
+ # License: MIT
9
+ # Last update: 2018-05-22 12:27
10
+ # ----------------------------------------------------------------------------- #
11
+ # multiline.rb Copyright (C) 2012-2018 j kepler
12
+ #
13
+ ## TODO search through text and put cursor on next result.
14
+ #/
15
+
16
+
17
+ module Umbra
18
+ class Multiline < Widget
19
+
20
+ attr_reader :list # array containing data (usually Strings)
21
+
22
+ # index of focussed row, starting 0, index into the list supplied
23
+ attr_reader :current_index
24
+
25
+ def initialize config={}, &block # {{{
26
+ @focusable = false
27
+ @editable = false
28
+ @pstart = 0 # which row does printing start from
29
+ @current_index = 0 # index of row on which cursor is
30
+ #register_events([:LEAVE_ROW, :ENTER_ROW, :LIST_SELECTION_EVENT])
31
+
32
+ ## PRESS event relates to pressing RETURN/ENTER (10)
33
+ register_events([:LEAVE_ROW, :ENTER_ROW, :PRESS])
34
+
35
+ super
36
+
37
+ map_keys
38
+ @row_offset = 0
39
+ @pcol = 0
40
+ @curpos = 0
41
+ @repaint_required = true
42
+ end
43
+
44
+ ## Set list of data to be displayed.
45
+ ## NOTE this can be called again and again, so we need to take care of change in size of data
46
+ ## as well as things like current_index and selected_index or indices.
47
+ def list=(alist)
48
+ if !alist or alist.size == 0
49
+ self.focusable=(false)
50
+ else
51
+ self.focusable=(true)
52
+ end
53
+ @list = alist
54
+ @repaint_required = true
55
+ @pstart = @current_index = 0
56
+ @pcol = 0
57
+ $log.debug " before multiline list= CHANGED "
58
+ fire_handler(:CHANGED, self) ## added 2018-05-08 -
59
+ end
60
+
61
+ def row_count
62
+ @list.size
63
+ end
64
+
65
+
66
+ # Calculate dimensions as late as possible, since we can have some other container such as a box,
67
+ # determine the dimensions after creation.
68
+ ## This is called by repaint.
69
+ private def _calc_dimensions
70
+ raise "Dimensions not supplied to multiline" if @row.nil? or @col.nil? or @width.nil? or @height.nil?
71
+ @_calc_dimensions = true
72
+ #@int_width = self.width # internal width NOT USED ELSEWHERE
73
+ height = self.height
74
+ @scroll_lines ||= height/2
75
+ @page_lines = height
76
+ end
77
+
78
+ def getvalue
79
+ @list
80
+ end
81
+
82
+ # }}}
83
+
84
+ ## repaints the entire multiline, called by +form+ {{{
85
+ def repaint
86
+ _calc_dimensions unless @_calc_dimensions
87
+
88
+ return unless @repaint_required
89
+ return unless @list
90
+ win = @graphic
91
+ r,c = self.row, self.col
92
+ _attr = @attr || NORMAL
93
+ _color = @color_pair || CP_WHITE
94
+ curpos = 1
95
+ coffset = 0
96
+
97
+ rows = getvalue
98
+
99
+ ht = self.height
100
+ cur = @current_index
101
+ st = pstart = @pstart # previous start
102
+ pend = pstart + ht -1 # previous end
103
+ if cur > pend
104
+ st = (cur -ht) + 1
105
+ elsif cur < pstart
106
+ st = cur
107
+ end
108
+ $log.debug "REPAINT #{self.class} : cur = #{cur} st = #{st} pstart = #{pstart} pend = #{pend} listsize = #{@list.size} "
109
+ $log.debug "REPAINT ML : row = #{r} col = #{c} width = #{width}/#{@width}, height = #{ht}/#{@height} ( #{FFI::NCurses.COLS} #{FFI::NCurses.LINES} "
110
+ y = 0
111
+ ctr = 0
112
+ filler = " "*(self.width)
113
+ rows.each_with_index {|_f, y|
114
+ next if y < st
115
+
116
+ curpos = ctr if y == cur ## used for setting row_offset
117
+
118
+ #_state = state_of_row(y) ## XXX should be move this into paint_row
119
+
120
+ win.printstring(ctr + r, coffset+c, filler, _color ) ## print filler
121
+
122
+ #paint_row( win, ctr+r, coffset+c, _f, y, _state)
123
+ paint_row( win, ctr+r, coffset+c, _f, y)
124
+
125
+
126
+ ctr += 1
127
+ @pstart = st
128
+ break if ctr >= ht
129
+ }
130
+ ## if counter < ht then we need to clear the rest in case there was data earlier {{{
131
+ if ctr < ht
132
+ while ctr < ht
133
+ win.printstring(ctr + r, coffset+c, filler, _color )
134
+ ctr += 1
135
+ end
136
+ end # }}}
137
+ @row_offset = curpos ## used by +widget+ in +rowcol+ called by +Form+
138
+ #@col_offset = coffset ## NOTE listbox had this line, but it interferes with textbox
139
+ @repaint_required = false
140
+ end # }}}
141
+
142
+ ## Paint given row. {{{
143
+ ## This is not be be called by user, but may be overridden if caller wishes
144
+ ## to completely change the presentation of each row. In most cases, it should suffice
145
+ ## to override just +print_row+ or +value_of_row+ or +_format_color+.
146
+ ##
147
+ ## @param [Window] window pointer for printing
148
+ ## @param [Integer] row number to print on
149
+ ## @param [Integer] col: column to print on
150
+ ## @param [String] line to be printed, usually String. Whatever was passed in to +list+ method.
151
+ ## @param [Integer] ctr: offset of row starting zero
152
+ ## @param [String] state: state of row (SELECTED CURRENT HIGHLIGHTED NORMAL)
153
+ def paint_row(win, row, col, line, ctr)
154
+
155
+ state = state_of_row(ctr)
156
+
157
+ ff = value_of_row(line, ctr, state)
158
+
159
+ ff = _truncate_to_width( ff ) ## truncate and handle panning
160
+
161
+ print_row(win, row, col, ff, ctr, state)
162
+ end
163
+
164
+
165
+ # do the actual printing of the row, depending on index and state
166
+ # This method starts with underscore since it is only required to be overriden
167
+ # if an object has special printing needs.
168
+ def print_row(win, row, col, str, index, state)
169
+ arr = color_of_row index, state
170
+ win.printstring(row, col, str, arr[0], arr[1])
171
+ end
172
+
173
+ # Each row can be in one of the following states:
174
+ # 1. HIGHLIGHTED: cursor is on the row, and the list is focussed (user is in it)
175
+ # 2. CURRENT : cursor was on this row, now user has exited the list
176
+ # 3. SELECTED : user has selected this row (this can also have above two states actually)
177
+ # 4. NORMAL : All other rows: not selected, not under cursor
178
+ # returns color, attrib for given row
179
+ # @param index of row in the list
180
+ # @param state of row in the list (see above states)
181
+ def color_of_row index, state
182
+ arr = case state
183
+ #when :SELECTED
184
+ #[@selected_color_pair, @selected_attr]
185
+ when :HIGHLIGHTED
186
+ [@highlight_color_pair || CP_WHITE, @highlight_attr || REVERSE]
187
+ when :CURRENT
188
+ [@color_pair, @attr]
189
+ when :NORMAL
190
+ _color = CP_CYAN
191
+ _color = CP_WHITE if index % 2 == 0
192
+ #_color = @alt_color_pair if index % 2 == 0
193
+ [@color_pair || _color, @attr || NORMAL]
194
+ end
195
+ return arr
196
+ end
197
+ alias :_format_color :color_of_row
198
+
199
+
200
+
201
+
202
+ # how to convert the line of the array to a simple String.
203
+ # This is only required to be overridden if the list passed in is not an array of Strings.
204
+ # @param the current row which could be a string or array or whatever was passed in in +list=()+.
205
+ # @return [String] string to print. A String must be returned.
206
+ def value_of_row line, ctr, state
207
+ line
208
+ end
209
+ alias :_format_value :value_of_row
210
+
211
+ def state_of_row ix
212
+ _st = :NORMAL
213
+ cur = @current_index
214
+ if ix == cur # current row, row on which cursor is or was
215
+ ## highlight only if object is focussed, otherwise just show mark
216
+ if @state == :HIGHLIGHTED
217
+ _st = :HIGHLIGHTED
218
+ else
219
+ ## cursor was on this row, but now user has tabbed out
220
+ _st = :CURRENT
221
+ end
222
+ end
223
+ return _st
224
+ end
225
+ # }}}
226
+
227
+
228
+ ## truncate string to width, and handle panning {{{
229
+ def _truncate_to_width ff
230
+ _width = self.width
231
+ if ff
232
+ if ff.size > _width
233
+ # pcol can be greater than width then we get null
234
+ if @pcol < ff.size
235
+ ff = ff[@pcol..@pcol+_width-1]
236
+ else
237
+ ff = ""
238
+ end
239
+ else
240
+ if @pcol < ff.size
241
+ ff = ff[@pcol..-1]
242
+ else
243
+ ff = ""
244
+ end
245
+ end
246
+ end
247
+ ff = "" unless ff
248
+ return ff
249
+ end # }}}
250
+
251
+
252
+ ## mapping of keys for multiline {{{
253
+ def map_keys
254
+ return if @keys_mapped
255
+ bind_keys([?k,FFI::NCurses::KEY_UP], "Up") { cursor_up }
256
+ bind_keys([?j,FFI::NCurses::KEY_DOWN], "Down") { cursor_down }
257
+ bind_keys([?l,FFI::NCurses::KEY_RIGHT], "Right") { cursor_forward }
258
+ bind_keys([?h,FFI::NCurses::KEY_LEFT], "Left") { cursor_backward }
259
+ bind_key(?g, 'goto_start') { goto_start }
260
+ bind_key(?G, 'goto_end') { goto_end }
261
+ bind_key(FFI::NCurses::KEY_CTRL_A, 'cursor_home') { cursor_home }
262
+ bind_key(FFI::NCurses::KEY_CTRL_E, 'cursor_end') { cursor_end }
263
+ bind_key(FFI::NCurses::KEY_CTRL_F, 'page_forward') { page_forward }
264
+ bind_key(32, 'page_forward') { page_forward }
265
+ bind_key(FFI::NCurses::KEY_CTRL_B, 'page_backward'){ page_backward }
266
+ bind_key(FFI::NCurses::KEY_CTRL_U, 'scroll_up') { scroll_up }
267
+ bind_key(FFI::NCurses::KEY_CTRL_D, 'scroll_down') { scroll_down }
268
+ ## C-h was not working, so trying C-j
269
+ bind_key(FFI::NCurses::KEY_CTRL_J, 'scroll_left') { scroll_left }
270
+ bind_key(FFI::NCurses::KEY_CTRL_L, 'scroll_right') { scroll_right }
271
+ @keys_mapped = true
272
+ end
273
+
274
+ ## on enter of this multiline
275
+ def on_enter
276
+ super
277
+ on_enter_row @current_index
278
+ # basically I need to only highlight the current index, not repaint all OPTIMIZE
279
+ touch ; repaint
280
+ end
281
+
282
+ # on leave of this multiline
283
+ def on_leave
284
+ super
285
+ on_leave_row @current_index
286
+ # basically I need to only unhighlight the current index, not repaint all OPTIMIZE
287
+ touch ; repaint
288
+ end
289
+
290
+ ## called when user leaves a row and when object is exited.
291
+ def on_leave_row index
292
+ fire_handler(:LEAVE_ROW, [index]) # 2018-03-26 - improve this
293
+ end
294
+ # called whenever a row entered.
295
+ # Call when object entered, also.
296
+ def on_enter_row index
297
+ fire_handler(:ENTER_ROW, [@current_index]) # 2018-03-26 - improve this
298
+ # if cursor ahead of blen then fix it
299
+ blen = current_row().size-1
300
+ if @curpos > blen
301
+ @col_offset = blen - @pcol
302
+ @curpos = blen
303
+ if @pcol > blen
304
+ @pcol = blen - self.width ## @int_width 2018-05-22 -
305
+ @pcol = 0 if @pcol < 0
306
+ @col_offset = blen - @pcol
307
+ end
308
+ end
309
+ @col_offset = 0 if @col_offset < 0
310
+ end
311
+ def cursor_up
312
+ @current_index -= 1
313
+ end
314
+ # go to next row
315
+ def cursor_down
316
+ @current_index += 1
317
+ end
318
+ # position cursor at start of field
319
+ def cursor_home
320
+ @curpos = 0
321
+ @pcol = 0
322
+ set_col_offset 0
323
+ end
324
+ # goto end of line.
325
+ # This should be consistent with moving the cursor to the end of the row with right arrow
326
+ def cursor_end
327
+ blen = current_row().length
328
+ if blen < self.width
329
+ set_col_offset blen # just after the last character
330
+ @pcol = 0
331
+ else
332
+ @pcol = blen-self.width #+2 # 2 is due to mark and space XXX could be a problem with textbox
333
+ set_col_offset blen # just after the last character
334
+ end
335
+ @curpos = blen # this is position in array where editing or motion is to happen regardless of what you see
336
+ # regardless of pcol (panning)
337
+ end
338
+ # returns current row as String
339
+ # 2018-04-11 - NOTE this may not be a String so we convert it to string before returning
340
+ # @return [String] row the cursor/user is on
341
+ def current_row
342
+ s = @list[@current_index]
343
+ value_of_row s, @current_index, :CURRENT
344
+ end
345
+ # move cursor forward one character, called with KEY_RIGHT action.
346
+ def cursor_forward
347
+ blen = current_row().size # -1
348
+ if @curpos < blen
349
+ if add_col_offset(1)==-1 # go forward if you can, else scroll
350
+ #@pcol += 1 if @pcol < self.width
351
+ @pcol += 1 if @pcol < blen
352
+ end
353
+ @curpos += 1
354
+ end
355
+ end
356
+ def cursor_backward
357
+
358
+ if @col_offset > 0
359
+ @curpos -= 1
360
+ add_col_offset -1
361
+ else
362
+ # cur is on the first col, then scroll left
363
+ if @pcol > 0
364
+ @pcol -= 1
365
+ @curpos -= 1
366
+ else
367
+ # do nothing
368
+ end
369
+ end
370
+ end
371
+ # advance col_offset (where cursor will be displayed on screen)
372
+ # @param [Integer] advance by n (can be negative or positive)
373
+ # @return -1 if cannot advance
374
+ private def add_col_offset num
375
+ x = @col_offset + num
376
+ return -1 if x < 0
377
+ return -1 if x > self.width ## @int_width 2018-05-22 -
378
+ # is it a problem that i am directly changing col_offset ??? XXX
379
+ @col_offset += num
380
+ end
381
+ # sets the visual cursor on the window at correct place
382
+ # NOTE be careful of curpos - pcol being less than 0
383
+ # @param [Integer] position in data on the line
384
+ private def set_col_offset x=@curpos
385
+ @curpos = x || 0 # NOTE we set the index of cursor here - WHY TWO THINGS ??? XXX
386
+ #return -1 if x < 0
387
+ #return -1 if x > @width
388
+ _w = self.width
389
+ if x >= _w
390
+ x = _w
391
+ @col_offset = _w
392
+ return
393
+ end
394
+ @col_offset = x
395
+ @col_offset = _w if @col_offset > _w
396
+ return
397
+ end
398
+ def scroll_right ## cursor_forward
399
+ blen = current_row().size-1
400
+ @pcol += 1 if @pcol < blen
401
+ end
402
+ def scroll_left ##cursor_backward
403
+ @pcol -= 1 if @pcol > 0
404
+ end
405
+ # go to start of file (first line)
406
+ def goto_start
407
+ @current_index = 0
408
+ @pcol = @curpos = 0
409
+ set_col_offset 0
410
+ end
411
+ # go to end of file (last line)
412
+ def goto_end
413
+ @current_index = @list.size-1
414
+ @pcol = @curpos = 0
415
+ end
416
+ def scroll_down
417
+ @current_index += @scroll_lines
418
+ end
419
+ def scroll_up
420
+ @current_index -= @scroll_lines
421
+ end
422
+ def page_backward
423
+ @current_index -= @page_lines
424
+ end
425
+ def page_forward
426
+ @current_index += @page_lines
427
+ end
428
+ # }}}
429
+
430
+
431
+ ## Multiline key handling. {{{
432
+ ## Called by +form+ from form's +handle_key+ when this object is in focus.
433
+ ## @param [Integer] ch: key caught by getch of window
434
+ def handle_key ch
435
+ old_current_index = @current_index
436
+ old_pcol = @pcol
437
+ old_col_offset = @col_offset
438
+
439
+ ret = super
440
+ return ret
441
+ ensure
442
+ ## NOTE: it is possible that a block called above may have cleared the list.
443
+ ## In that case, the on_enter_row will crash. I had put a check here, but it
444
+ ## has vanished ???
445
+ @current_index = 0 if @current_index < 0
446
+ @current_index = @list.size-1 if @current_index >= @list.size
447
+ if @current_index != old_current_index
448
+ on_leave_row old_current_index
449
+ on_enter_row @current_index
450
+ @repaint_required = true
451
+ end
452
+ @repaint_required = true if old_pcol != @pcol or old_col_offset != @col_offset
453
+ end
454
+
455
+ ## convenience method for calling most used event of a widget
456
+ ## Called by user programs.
457
+ def command *args, &block
458
+ bind_event :ENTER_ROW, *args, &block
459
+ end # }}}
460
+
461
+ #
462
+ # event when user hits ENTER on a row, user would bind :PRESS
463
+ # callers may use w.current_index or w.current_row or w.curpos.
464
+ #
465
+ # obj.bind :PRESS { |w| w.current_row }
466
+ #
467
+ def fire_action_event
468
+ return if @list.nil? || @list.size == 0
469
+ #require 'canis/core/include/ractionevent'
470
+ #aev = text_action_event
471
+ #fire_handler :PRESS, aev
472
+ fire_handler :PRESS, self
473
+ end
474
+ end # class
475
+ end # module
476
+
data/lib/umbra/pad.rb CHANGED
@@ -2,11 +2,12 @@
2
2
  * Name: PadReader.rb
3
3
  * Description : This is an independent file viewer that uses a Pad and traps keys
4
4
  I am using only ffi-ncurses and not window.rb or any other support classes
5
- so this can be used anywhere else.
5
+ so this can be used anywhere else. This however, limits the pad to very simple
6
+ printing.
6
7
  * Author: jkepler
7
8
  * Date: 2018-03-28 14:30
8
9
  * License: MIT
9
- * Last update: 2018-04-17 11:25
10
+ * Last update: 2018-04-26 08:29
10
11
 
11
12
  == CHANGES
12
13
  == TODO
@@ -25,11 +26,12 @@ class Pad
25
26
  # You may pass height, width, row and col for creating a window otherwise a fullscreen window
26
27
  # will be created. If you pass a window from caller then that window will be used.
27
28
  # Some keys are trapped, jkhl space, pgup, pgdown, end, home, t b
28
- # This is currently very minimal and was created to get me started to integrating
29
- # pads into other classes such as textview.
29
+ # NOTE: this is very minimal, and uses no widgets, so I am unable to yield an object
30
+ # for further configuration. If we used a textbox, I could have yielded that.
31
+ # TODO handle passed block
30
32
  def initialize config={}, &block
31
33
 
32
- $log.debug " inside pad contructor"
34
+ $log.debug " inside pad contructor" if $log
33
35
  @config = config
34
36
  @rows = FFI::NCurses.LINES-1
35
37
  @cols = FFI::NCurses.COLS-1
@@ -62,7 +64,7 @@ class Pad
62
64
  @rows -=3 # 3 is since print_border_only reduces one from width, to check whether this is correct
63
65
  @cols -=3
64
66
  end
65
- $log.debug "top and left are: #{top} #{left} "
67
+ $log.debug "top and left are: #{top} #{left} " if $log
66
68
  #@window.box # 2018-03-28 -
67
69
  FFI::NCurses.box @pointer, 0, 0
68
70
  title(config[:title])
@@ -120,21 +122,25 @@ class Pad
120
122
  destroy_pad
121
123
  @content_rows, @content_cols = content_dimensions(content)
122
124
  pad = FFI::NCurses.newpad(@content_rows, @content_cols)
123
- FFI::NCurses.wbkgd(pad, FFI::NCurses.COLOR_PAIR(@color_pair) | @attr);
124
125
  FFI::NCurses.keypad(pad, true); # function and arrow keys
125
126
 
126
127
  FFI::NCurses.update_panels
127
- cp = @color_pair
128
- #@attr = FFI::NCurses::A_BOLD
129
- FFI::NCurses.wattron(pad, FFI::NCurses.COLOR_PAIR(cp) | @attr)
128
+ render(content, pad, @color_pair, @attr)
129
+ return pad
130
+ end
131
+ # renders the content in a loop.
132
+ # NOTE: separated in the hope that caller can override.
133
+ def render content, pad, color_pair, attr
134
+ cp = color_pair
135
+ FFI::NCurses.wbkgd(pad, FFI::NCurses.COLOR_PAIR(color_pair) | attr);
136
+ FFI::NCurses.wattron(pad, FFI::NCurses.COLOR_PAIR(cp) | attr)
130
137
  # WRITE
131
- filler = " "*@content_cols
138
+ #filler = " "*@content_cols
132
139
  content.each_index { |ix|
133
140
  #FFI::NCurses.mvwaddstr(pad,ix, 0, filler)
134
141
  FFI::NCurses.mvwaddstr(pad,ix, 0, content[ix])
135
142
  }
136
- FFI::NCurses.wattroff(pad, FFI::NCurses.COLOR_PAIR(cp) | @attr)
137
- return pad
143
+ FFI::NCurses.wattroff(pad, FFI::NCurses.COLOR_PAIR(cp) | attr)
138
144
  end
139
145
 
140
146
  # receive array as content source
@@ -174,7 +180,7 @@ class Pad
174
180
  x.getbyte(0)
175
181
  end
176
182
  def content_cols content
177
- # FIXME bombs if content contains integer or nil.
183
+ # next line bombs if content contains integer or nil.
178
184
  #longest = content.max_by(&:length)
179
185
  #longest.length
180
186
  max = 1
@@ -4,7 +4,7 @@
4
4
  # Author: j kepler http://github.com/mare-imbrium/canis/
5
5
  # Date: 2018-04-02 - 10:37
6
6
  # License: MIT
7
- # Last update: 2018-04-03 14:22
7
+ # Last update: 2018-05-14 14:31
8
8
  # ----------------------------------------------------------------------------- #
9
9
  # radiobutton.rb Copyright (C) 2012-2018 j kepler
10
10
 
@@ -21,7 +21,7 @@ module Umbra
21
21
  # radiobuttons of this group is fired.
22
22
 
23
23
  class RadioButton < ToggleButton
24
- attr_accessor :align_right # the button will be on the right
24
+ attr_property :align_right # the button will be on the right
25
25
  attr_accessor :button_group # group that this button belongs to.
26
26
 
27
27
  def initialize config={}, &block