ncumbra 0.1.0

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