ncumbra 0.1.0 → 0.1.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -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