ncumbra 0.1.0

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,503 @@
1
+ # ----------------------------------------------------------------------------- #
2
+ # File: field.rb
3
+ # Description: text input field or widget
4
+ # Author: j kepler http://github.com/mare-imbrium/canis/
5
+ # Date: 2018-03
6
+ # License: MIT
7
+ # Last update: 2018-04-14 08:55
8
+ # ----------------------------------------------------------------------------- #
9
+ # field.rb Copyright (C) 2012-2018 j kepler
10
+ #
11
+
12
+ class InputDataEvent # {{{
13
+ attr_accessor :index0, :index1, :source, :type, :row, :text
14
+ def initialize index0, index1, source, type, row, text
15
+ @index0 = index0
16
+ @index1 = index1
17
+ @source = source
18
+ @type = type
19
+ @row = row
20
+ @text = text
21
+ end
22
+ # until now to_s was returning inspect, but to make it easy for users let us return the value
23
+ # they most expect which is the text that was changed
24
+ def to_s
25
+ inspect
26
+ end
27
+ def inspect
28
+ ## now that textarea.to_s prints content we shouldn pass it here.
29
+ #"#{@type.to_s}, #{@source}, ind0:#{@index0}, ind1:#{@index1}, row:#{@row}, text:#{@text}"
30
+ "#{@type.to_s}, ind0:#{@index0}, ind1:#{@index1}, row:#{@row}, text:#{@text}"
31
+ end
32
+ # this is so that earlier applications were getting source in the block, not an event. they
33
+ # were doing a fld.getvalue, so we must keep those apps running
34
+ # @since 1.2.0 added 2010-09-11 12:25
35
+ def getvalue
36
+ @source.getvalue
37
+ end
38
+ end # }}}
39
+ # Text edit field
40
+ # TODO :
41
+ # ? remove datatype, just use strings.
42
+ # NOTE: +width+ is the length of the display whereas +maxlen+ is the maximum size that the value
43
+ # can take. Thus, +maxlen+ can exceed +width+. Currently, +maxlen+ defaults to +width+ which
44
+ # defaults to 20.
45
+ # NOTE: Use +text=(val)+ to set value of field, and +text()+ to retrieve value.
46
+ # == Example
47
+ # f = Field.new text: "Some value", row: 10, col: 2
48
+ #
49
+ # Field introduces an event :CHANGE which is fired for each character deleted or inserted
50
+ #
51
+ module Umbra
52
+ class Field < Widget
53
+ attr_accessor :maxlen # maximum length allowed into field
54
+ attr_reader :buffer # actual buffer being used for storage
55
+
56
+
57
+ attr_accessor :values # validate against provided list, (+include?+)
58
+ attr_accessor :valid_regex # validate against regular expression (+match()+)
59
+ attr_accessor :valid_range # validate against numeric range, should respond to +include?+
60
+ # for numeric fields, specify lower or upper limit of entered value
61
+ attr_accessor :below, :above
62
+
63
+ # aliased to type
64
+ #attr_accessor :chars_allowed # regex, what characters to allow entry, will ignore all else
65
+ # character to show, earlier called +show+ which clashed with Widget method +show+
66
+ attr_accessor :mask # what charactr to show for each char entered (password field)
67
+ attr_accessor :null_allowed # allow nulls, don't validate if null # added , boolean
68
+
69
+ # any new widget that has +editable+ should have +modified+ also
70
+ attr_accessor :editable # allow editing
71
+
72
+ # +type+ is just a convenience over +chars_allowed+ and sets some basic filters
73
+ # @example: :integer, :float, :alpha, :alnum
74
+ # NOTE: we do not store type, only chars_allowed, so this won't return any value
75
+ #attr_reader :type # datatype of field, currently only sets chars_allowed
76
+
77
+ # this accesses the field created or passed with set_label
78
+ #attr_reader :label
79
+ # this is the class of the field set in +text()+, so value is returned in same class
80
+ # @example : Integer, Integer, Float
81
+ attr_accessor :datatype # currently set during set_buffer
82
+ attr_reader :original_value # value on entering field
83
+ attr_accessor :overwrite_mode # true or false INSERT OVERWRITE MODE
84
+
85
+ # column on which field printed, usually the same as +col+ unless +label+ used.
86
+ # Required by +History+ to popup field history.
87
+ attr_reader :field_col # column on which field is printed
88
+ # required due to labels. Is updated after printing
89
+ # # so can be nil if accessed early 2011-12-8
90
+
91
+ def initialize config={}, &block
92
+ @buffer = String.new
93
+ @row = 0
94
+ @col = 0
95
+ @editable = true
96
+ @focusable = true
97
+
98
+ map_keys
99
+ init_vars
100
+ register_events(:CHANGE)
101
+ super
102
+ @width ||= 20
103
+ @maxlen ||= @width
104
+ end
105
+ def init_vars
106
+ @pcol = 0 # needed for horiz scrolling
107
+ @curpos = 0 # current cursor position in buffer (NOT screen/window/field)
108
+ # this is the index where characters are put or deleted
109
+ # # when user edits
110
+ @modified = false
111
+ end
112
+
113
+ # NOTE: earlier there was some confusion over type, chars_allowed and datatype
114
+ # Now type and chars_allowed are merged into one.
115
+ # If you pass a symbol such as :integer, :float or Float Integer then some
116
+ # standard chars_allowed will be used. Otherwise you may pass a regexp.
117
+ #
118
+ # @param [symbol] :integer, :float, :alpha, :alnum, Float, Integer, Numeric, Regexp
119
+ def type=(val)
120
+
121
+ dtype = val
122
+ #return self if @chars_allowed # disallow changing
123
+ # send in a regexp, we just save it.
124
+ if dtype.is_a? Regexp
125
+ @chars_allowed = dtype
126
+ return self
127
+ end
128
+ dtype = dtype.to_s.downcase.to_sym if dtype.is_a? String
129
+ case dtype # missing to_sym would have always failed due to to_s 2011-09-30 1.3.1
130
+ when :integer, Integer
131
+ @chars_allowed = /\d/
132
+ when :numeric, :float, Numeric, Float
133
+ @chars_allowed = /[\d\.]/
134
+ when :alpha
135
+ @chars_allowed = /[a-zA-Z]/
136
+ when :alnum
137
+ @chars_allowed = /[a-zA-Z0-9]/
138
+ else
139
+ raise ArgumentError, "Field type: invalid datatype specified. Use :integer, :numeric, :float, :alpha, :alnum "
140
+ end
141
+ self
142
+ end
143
+ alias :chars_allowed= :type=
144
+
145
+ #
146
+ # add a char to field, and validate
147
+ # if disabled or exceeding size
148
+ # @param [char] a character to add
149
+ # @return [Integer] 0 if okay, -1 if not editable or exceeding length
150
+ def putch char
151
+ return -1 if !@editable
152
+ return -1 if !@overwrite_mode && (@buffer.length >= @maxlen)
153
+ blen = @buffer.length
154
+ if @chars_allowed != nil
155
+ return if char.match(@chars_allowed).nil?
156
+ end
157
+ # added insert or overwrite mode 2010-03-17 20:11
158
+ oldchar = nil
159
+ if @overwrite_mode
160
+ oldchar = @buffer[@curpos]
161
+ @buffer[@curpos] = char
162
+ else
163
+ @buffer.insert(@curpos, char)
164
+ end
165
+ oldcurpos = @curpos
166
+ #$log.warn "XXX: FIELD CURPOS #{@curpos} blen #{@buffer.length} " #if @curpos > blen
167
+ @curpos += 1 if @curpos < @maxlen
168
+ @modified = true
169
+ #$log.debug " FIELD FIRING CHANGE: #{char} at new #{@curpos}: bl:#{@buffer.length} buff:[#{@buffer}]"
170
+ if @overwrite_mode
171
+ fire_handler :CHANGE, InputDataEvent.new(oldcurpos,@curpos, self, :DELETE, 0, oldchar) # 2010-09-11 12:43
172
+ end
173
+ fire_handler :CHANGE, InputDataEvent.new(oldcurpos,@curpos, self, :INSERT, 0, char) # 2010-09-11 12:43
174
+ 0
175
+ end
176
+
177
+ ##
178
+ # add character to field, adjust scrolling.
179
+ # NOTE: handlekey only calls this if between 32 and 126.
180
+ def putc c
181
+ if c >= 0 and c <= 127
182
+ ret = putch c.chr
183
+ if ret == 0
184
+ # character is valid
185
+ if addcol(1) == -1 # if can't go forward, try scrolling
186
+ # scroll if exceeding display len but less than max len
187
+ if @curpos > @width && @curpos <= @maxlen
188
+ @pcol += 1 if @pcol < @width
189
+ end
190
+ end
191
+ @modified = true
192
+ return 0
193
+ end
194
+ end
195
+ return -1
196
+ end
197
+ # delete a character from the buffer at given position
198
+ # Called by +delete_curr_char+ and +delete_prev_char+.
199
+ def delete_at index=@curpos
200
+ return -1 if !@editable
201
+ char = @buffer.slice!(index,1)
202
+ #$log.debug " delete at #{index}: #{@buffer.length}: #{@buffer}"
203
+ @modified = true
204
+ fire_handler :CHANGE, InputDataEvent.new(@curpos,@curpos, self, :DELETE, 0, char) # 2010-09-11 13:01
205
+ end
206
+ #
207
+ # silently restores earlier value without firing handlers, use if exception and you want old value
208
+ # Called when pressing <ESC> or <c-g>.
209
+ def restore_original_value
210
+ @buffer = @original_value.dup
211
+ # earlier commented but trying again, since i am getting IndexError in insert 2188
212
+ # Added next 3 lines to fix issue, now it comes back to beginning.
213
+ cursor_home
214
+
215
+ @repaint_required = true
216
+ end
217
+ ##
218
+ # set value of Field
219
+ # fires CHANGE handler
220
+ # Please don't use this directly, use +text+
221
+ # This name is from ncurses field, added underscore to emphasize not to use
222
+ private def _set_buffer value #:nodoc:
223
+ @repaint_required = true
224
+ @datatype = value.class
225
+ @delete_buffer = @buffer.dup
226
+ @buffer = value.to_s.dup
227
+ # don't allow setting of value greater than maxlen
228
+ @buffer = @buffer[0,@maxlen] if @maxlen && @buffer.length > @maxlen
229
+ @curpos = 0
230
+ # hope @delete_buffer is not overwritten
231
+ fire_handler :CHANGE, InputDataEvent.new(@curpos,@curpos, self, :DELETE, 0, @delete_buffer) # 2010-09-11 13:01
232
+ fire_handler :CHANGE, InputDataEvent.new(@curpos,@curpos, self, :INSERT, 0, @buffer) # 2010-09-11 13:01
233
+ self # 2011-10-2
234
+ end
235
+ ## add a column to cursor position. Field
236
+ private def addcol num
237
+ x = @col_offset + num
238
+ return -1 if x < 0
239
+ return -1 if x > @width
240
+ @col_offset += num
241
+ end
242
+
243
+ # converts back into original type
244
+ # changed to convert on 2009-01-06 23:39
245
+ # 2018-04-13 - Changed from private to public since I got an error on on_leave
246
+ # when this was called from LabeledField.
247
+ def getvalue
248
+ dt = @datatype || String
249
+ case dt.to_s
250
+ when "String"
251
+ return @buffer
252
+ when "Integer"
253
+ return @buffer.to_i
254
+ when "Float"
255
+ return @buffer.to_f
256
+ else
257
+ return @buffer.to_s
258
+ end
259
+ end
260
+
261
+
262
+ public
263
+ ## Note that some older widgets like Field repaint every time the form.repaint
264
+ ##+ is called, whether updated or not. I can't remember why this is, but
265
+ ##+ currently I've not implemented events with these widgets. 2010-01-03 15:00
266
+
267
+ def repaint
268
+ return unless @repaint_required # 2010-11-20 13:13 its writing over a window i think TESTING
269
+ $log.debug("repaint FIELD: #{name}, r:#{row} c:#{col},wid:#{width},pcol:#{@pcol}, #{focusable} st: #{@state} ")
270
+ @width = 1 if width == 0
271
+ printval = getvalue_for_paint().to_s # added 2009-01-06 23:27
272
+ printval = mask()*printval.length unless @mask.nil?
273
+ if !printval.nil?
274
+ if printval.length > width # only show maxlen
275
+ printval = printval[@pcol..@pcol+width-1]
276
+ else
277
+ printval = printval[@pcol..-1]
278
+ end
279
+ end
280
+
281
+ acolor = @color_pair || CP_WHITE
282
+ if @state == :HIGHLIGHTED
283
+ acolor = @highlight_color_pair || CP_RED
284
+ end
285
+ #$log.debug " Field g:#{@graphic}. r,c,displen:#{@row}, #{@col}, #{@width} c:#{@color} bg:#{@bgcolor} a:#{@attr} :#{@name} "
286
+ r = row
287
+ c = col
288
+ @graphic.printstring r, c, sprintf("%-*s", width, printval), acolor, attr()
289
+ @field_col = c
290
+ @repaint_required = false
291
+ end
292
+
293
+ def map_keys
294
+ return if @keys_mapped
295
+ bind_key(FFI::NCurses::KEY_LEFT, :cursor_backward )
296
+ bind_key(FFI::NCurses::KEY_RIGHT, :cursor_forward )
297
+ bind_key(FFI::NCurses::KEY_BACKSPACE, :delete_prev_char )
298
+ bind_key(127, :delete_prev_char )
299
+ bind_key(330, :delete_curr_char )
300
+ bind_key(?\C-a, :cursor_home )
301
+ bind_key(?\C-e, :cursor_end )
302
+ bind_key(?\C-k, :delete_eol )
303
+ bind_key(?\C-_, :undo_delete_eol )
304
+ #bind_key(27){ text @original_value }
305
+ bind_key(?\C-g, 'revert'){ restore_original_value }
306
+ @keys_mapped = true
307
+ end
308
+
309
+ # field - handle a key sent for form
310
+ #
311
+ def handle_key ch
312
+ $log.debug "inside handle key of field with #{ch}"
313
+ @repaint_required = true
314
+ case ch
315
+ when 32..126
316
+ putc ch
317
+ when 27 # cannot bind it, so hardcoding it here
318
+ restore_original_value
319
+ else
320
+ ret = super
321
+ return ret
322
+ end
323
+ 0 # 2008-12-16 23:05 without this -1 was going back so no repaint
324
+ end
325
+ # does an undo on delete_eol, not a real undo
326
+ def undo_delete_eol
327
+ return if @delete_buffer.nil?
328
+ #oldvalue = @buffer
329
+ @buffer.insert @curpos, @delete_buffer
330
+ fire_handler :CHANGE, InputDataEvent.new(@curpos,@curpos+@delete_buffer.length, self, :INSERT, 0, @delete_buffer) # 2010-09-11 13:01
331
+ end
332
+ ##
333
+ # position cursor at start of field
334
+ def cursor_home
335
+ @curpos = 0
336
+ @pcol = 0
337
+ set_col_offset 0
338
+ end
339
+ ##
340
+ # goto end of field, "end" is a keyword so could not use it.
341
+ def cursor_end
342
+ blen = @buffer.rstrip.length
343
+ if blen < @width
344
+ set_col_offset blen
345
+ else
346
+ @pcol = blen-@width
347
+ #set_form_col @width-1
348
+ set_col_offset blen
349
+ end
350
+ @curpos = blen # this is position in array where editing or motion is to happen regardless of what you see
351
+ # regardless of pcol (panning)
352
+ end
353
+ # sets the visual cursor on the window at correct place
354
+ # NOTE be careful of curpos - pcol being less than 0
355
+ private def set_col_offset x=@curpos
356
+ @curpos = x || 0 # NOTE we set the index of cursor here
357
+ #return -1 if x < 0
358
+ #return -1 if x > @width
359
+ if x > @width
360
+ x = @width
361
+ end
362
+ @col_offset = x
363
+ return
364
+ =begin
365
+ c = @col + @col_offset + @curpos - @pcol
366
+ min = @col + @col_offset
367
+ max = min + @width
368
+ c = min if c < min
369
+ c = max if c > max
370
+ #$log.debug " #{@name} FIELD set_form_col #{c}, curpos #{@curpos} , #{@col} + #{@col_offset} pcol:#{@pcol} "
371
+ #setrowcol nil, c # this does not exist since it called form
372
+ =end
373
+ end
374
+ def delete_eol
375
+ return -1 unless @editable
376
+ pos = @curpos-1
377
+ @delete_buffer = @buffer[@curpos..-1]
378
+ # if pos is 0, pos-1 becomes -1, end of line!
379
+ @buffer = pos == -1 ? "" : @buffer[0..pos]
380
+ fire_handler :CHANGE, InputDataEvent.new(@curpos,@curpos+@delete_buffer.length, self, :DELETE, 0, @delete_buffer)
381
+ return @delete_buffer
382
+ end
383
+ # move cursor forward one character, called with KEY_RIGHT action.
384
+ def cursor_forward
385
+ if @curpos < @buffer.length
386
+ if addcol(1)==-1 # go forward if you can, else scroll
387
+ @pcol += 1 if @pcol < @width
388
+ end
389
+ @curpos += 1
390
+ end
391
+ # $log.debug " crusor FORWARD cp:#{@curpos} pcol:#{@pcol} b.l:#{@buffer.length} d_l:#{@display_length} fc:#{@form.col}"
392
+ end
393
+ # move cursor back one character, called with KEY_LEFT action.
394
+ def cursor_backward
395
+ if @curpos > 0
396
+ @curpos -= 1
397
+ #if @pcol > 0 #and @form.col == @col + @col_offset
398
+ #@pcol -= 1
399
+ #end
400
+ addcol -1
401
+ elsif @pcol > 0
402
+ # pan left only when cursor pos is 0
403
+ @pcol -= 1
404
+ end
405
+ # $log.debug " crusor back cp:#{@curpos} pcol:#{@pcol} b.l:#{@buffer.length} d_l:#{@display_length} fc:#{@form.col}"
406
+ =begin
407
+ # this is perfect if not scrolling, but now needs changes
408
+ if @curpos > 0
409
+ @curpos -= 1
410
+ addcol -1
411
+ end
412
+ =end
413
+ end
414
+ def delete_curr_char
415
+ return -1 unless @editable
416
+ delete_at
417
+ @modified = true
418
+ end
419
+ # called when user presses backspace.
420
+ # Delete character on left of cursor
421
+ def delete_prev_char
422
+ return -1 if !@editable
423
+ return if @curpos <= 0
424
+ # if we've panned, then unpan, and don't move cursor back
425
+ # Otherwise, adjust cursor (move cursor back as we delete)
426
+ adjust = true
427
+ if @pcol > 0
428
+ @pcol -= 1
429
+ adjust = false
430
+ end
431
+ @curpos -= 1 if @curpos > 0
432
+ delete_at
433
+ addcol -1 if adjust # move visual cursor back
434
+ @modified = true
435
+ end
436
+ # upon leaving a field
437
+ # returns false if value not valid as per values or valid_regex
438
+ # 2008-12-22 12:40 if null_allowed, don't validate, but do fire_handlers
439
+ def on_leave
440
+ val = getvalue
441
+ #$log.debug " FIELD ON LEAVE:#{val}. #{@values.inspect}"
442
+ valid = true
443
+ if val.to_s.empty? && @null_allowed
444
+ #$log.debug " empty and null allowed"
445
+ else
446
+ if !@values.nil?
447
+ valid = @values.include? val
448
+ raise FieldValidationException, "Field value (#{val}) not in values: #{@values.join(',')}" unless valid
449
+ end
450
+ if !@valid_regex.nil?
451
+ valid = @valid_regex.match(val.to_s)
452
+ raise FieldValidationException, "Field not matching regex #{@valid_regex}" unless valid
453
+ end
454
+ # added valid_range for numerics 2011-09-29
455
+ if !in_range?(val)
456
+ raise FieldValidationException, "Field not matching range #{@valid_range}, above #{@above} or below #{@below} "
457
+ end
458
+ end
459
+ # here is where we should set the forms modified to true - 2009-01
460
+ if modified?
461
+ @modified = true
462
+ end
463
+ # if super fails we would have still set modified to true
464
+ super
465
+ #return valid
466
+ end
467
+
468
+ # checks field against +valid_range+, +above+ and +below+ , returning +true+ if it passes
469
+ # set attributes, +false+ if it fails any one.
470
+ def in_range?( val )
471
+ val = val.to_i
472
+ (@above.nil? or val > @above) and
473
+ (@below.nil? or val < @below) and
474
+ (@valid_range.nil? or @valid_range.include?(val))
475
+ end
476
+ ## save original value on enter, so we can check for modified.
477
+ # 2009-01-18 12:25
478
+ # 2011-10-9 I have changed to take @buffer since getvalue returns a datatype
479
+ # and this causes a crash in set_original on cursor forward.
480
+ def on_enter
481
+ #@original_value = getvalue.dup rescue getvalue
482
+ @original_value = @buffer.dup # getvalue.dup rescue getvalue
483
+ super
484
+ end
485
+ ##
486
+ # overriding widget, check for value change
487
+ def modified?
488
+ getvalue() != @original_value
489
+ end
490
+ # 2018-03-22 - replaced the old style text(val) with attr style
491
+ def text
492
+ getvalue
493
+ end
494
+ def text=(val)
495
+ return unless val # added 2010-11-17 20:11, dup will fail on nil
496
+ # will bomb on integer or float etc !!
497
+ #_set_buffer(val.dup)
498
+ _set_buffer(val)
499
+ end
500
+ alias :default= :text=
501
+ # ADD HERE FIELD
502
+ end # }}}
503
+ end # module