rbcurse 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (55) hide show
  1. data/CHANGELOG +1570 -0
  2. data/History.txt +6 -0
  3. data/Manifest.txt +54 -0
  4. data/README.txt +304 -0
  5. data/Rakefile +28 -0
  6. data/examples/qdfilechooser.rb +68 -0
  7. data/examples/rfe.rb +853 -0
  8. data/examples/rfe_renderer.rb +69 -0
  9. data/examples/test1.rb +242 -0
  10. data/examples/test2.rb +498 -0
  11. data/examples/testcombo.rb +95 -0
  12. data/examples/testkeypress.rb +61 -0
  13. data/examples/testmenu.rb +105 -0
  14. data/examples/testtable.rb +266 -0
  15. data/examples/testtabp.rb +106 -0
  16. data/examples/testtodo.rb +532 -0
  17. data/examples/viewtodo.rb +512 -0
  18. data/lib/rbcurse/action.rb +31 -0
  19. data/lib/rbcurse/applicationheader.rb +57 -0
  20. data/lib/rbcurse/celleditor.rb +120 -0
  21. data/lib/rbcurse/checkboxcellrenderer.rb +69 -0
  22. data/lib/rbcurse/colormap.rb +133 -0
  23. data/lib/rbcurse/comboboxcellrenderer.rb +45 -0
  24. data/lib/rbcurse/defaultlistselectionmodel.rb +49 -0
  25. data/lib/rbcurse/keylabelprinter.rb +143 -0
  26. data/lib/rbcurse/listcellrenderer.rb +99 -0
  27. data/lib/rbcurse/listkeys.rb +33 -0
  28. data/lib/rbcurse/listscrollable.rb +216 -0
  29. data/lib/rbcurse/listselectable.rb +67 -0
  30. data/lib/rbcurse/mapper.rb +108 -0
  31. data/lib/rbcurse/orderedhash.rb +77 -0
  32. data/lib/rbcurse/rcombo.rb +243 -0
  33. data/lib/rbcurse/rdialogs.rb +183 -0
  34. data/lib/rbcurse/rform.rb +845 -0
  35. data/lib/rbcurse/rinputdataevent.rb +36 -0
  36. data/lib/rbcurse/rlistbox.rb +804 -0
  37. data/lib/rbcurse/rmenu.rb +666 -0
  38. data/lib/rbcurse/rmessagebox.rb +325 -0
  39. data/lib/rbcurse/rpopupmenu.rb +754 -0
  40. data/lib/rbcurse/rtabbedpane.rb +259 -0
  41. data/lib/rbcurse/rtable.rb +1296 -0
  42. data/lib/rbcurse/rtextarea.rb +673 -0
  43. data/lib/rbcurse/rtextview.rb +335 -0
  44. data/lib/rbcurse/rwidget.rb +1731 -0
  45. data/lib/rbcurse/scrollable.rb +301 -0
  46. data/lib/rbcurse/selectable.rb +94 -0
  47. data/lib/rbcurse/table/tablecellrenderer.rb +85 -0
  48. data/lib/rbcurse/table/tabledatecellrenderer.rb +102 -0
  49. data/lib/rbcurse.rb +7 -0
  50. data/lib/ver/keyboard.rb +150 -0
  51. data/lib/ver/keyboard2.rb +170 -0
  52. data/lib/ver/ncurses.rb +102 -0
  53. data/lib/ver/window.rb +369 -0
  54. data/test/test_rbcurse.rb +0 -0
  55. metadata +118 -0
@@ -0,0 +1,1731 @@
1
+ =begin
2
+ * Name: rwidget: base class and then popup and other derived widgets
3
+ * $Id$
4
+ * Description
5
+ Some simple light widgets for creating ncurses applications. No reliance on ncurses
6
+ forms and fields.
7
+ I expect to pass through this world but once. Any good therefore that I can do,
8
+ or any kindness or ablities that I can show to any fellow creature, let me do it now.
9
+ Let me not defer it or neglect it, for I shall not pass this way again.
10
+ * Author: rkumar (arunachalesha)
11
+ * Date: 2008-11-19 12:49
12
+ * License: Same as Ruby's License (http://www.ruby-lang.org/LICENSE.txt)
13
+ TODO
14
+ - repaint only what is modified
15
+ - save data in a hash when called for.
16
+ - make some methods private/protected
17
+ - Add bottom bar also, perhaps allow it to be displayed on a key so it does not take
18
+ - Can key bindings be abstracted so they can be inherited /reused.
19
+ - some kind of CSS style sheet.
20
+
21
+
22
+ =end
23
+ require 'rubygems'
24
+ require 'ncurses'
25
+ require 'logger'
26
+ require 'rbcurse/mapper'
27
+ require 'rbcurse/colormap'
28
+ #require 'rbcurse/rdialogs'
29
+ #require 'rbcurse/listcellrenderer'
30
+
31
+ module DSL
32
+ ## others may not want this, if = sent, it creates DSL and sets
33
+ # using this resulted in time lost in bedebugging why some method was not working.
34
+ def OLD_method_missing(sym, *args)
35
+ $log.debug "METHOD MISSING : #{sym} #{self} "
36
+ #if "#{sym}"[-1].chr=="="
37
+ # sym = "#{sym}"[0..-2]
38
+ #else
39
+ self.class.dsl_accessor sym
40
+ #end
41
+ send(sym, *args)
42
+ end
43
+ end
44
+ class Module
45
+ ## others may not want this, sets config, so there's a duplicate hash
46
+ # also creates a attr_writer so you can use =.
47
+ def dsl_accessor(*symbols)
48
+ symbols.each { |sym|
49
+ class_eval %{
50
+ def #{sym}(*val)
51
+ if val.empty?
52
+ @#{sym}
53
+ else
54
+ @#{sym} = val.size == 1 ? val[0] : val
55
+ @config["#{sym}"]=@#{sym}
56
+ end
57
+ end
58
+ attr_writer sym
59
+ }
60
+ }
61
+ end
62
+ def dsl_property(*symbols)
63
+ symbols.each { |sym|
64
+ class_eval %{
65
+ def #{sym}(*val)
66
+ if val.empty?
67
+ @#{sym}
68
+ else
69
+ oldvalue = @#{sym}
70
+ @#{sym} = val.size == 1 ? val[0] : val
71
+ newvalue = @#{sym}
72
+ @config["#{sym}"]=@#{sym}
73
+ if oldvalue != newvalue
74
+ fire_property_change("#{sym}", oldvalue, newvalue)
75
+ end
76
+ end
77
+ end
78
+ #attr_writer sym
79
+ def #{sym}=val
80
+ #{sym}(val)
81
+ end
82
+ }
83
+ }
84
+ end
85
+
86
+ end
87
+
88
+ include Ncurses
89
+ module RubyCurses
90
+ extend self
91
+ include ColorMap
92
+ class FieldValidationException < RuntimeError
93
+ end
94
+ module Utils
95
+ ##
96
+ # wraps text given max length, puts newlines in it.
97
+ # it does not take into account existing newlines
98
+ # Some classes have @maxlen or display_length which may be passed as the second parameter
99
+ def wrap_text(txt, max )
100
+ txt.gsub(/(.{1,#{max}})( +|$\n?)|(.{1,#{max}})/,
101
+ "\\1\\3\n")
102
+ end
103
+ def clean_string! content
104
+ content.chomp! # don't display newline
105
+ content.gsub!(/[\t\n]/, ' ') # don't display tab
106
+ content.gsub!(/[^[:print:]]/, '') # don't display non print characters
107
+ content
108
+ end
109
+ # needs to move to a keystroke class
110
+ def keycode_tos keycode
111
+ case keycode
112
+ when 33..126
113
+ return keycode.chr
114
+ when ?\C-a .. ?\C-z
115
+ return "C-" + (keycode + ?a -1).chr
116
+ when ?\M-A..?\M-z
117
+ return "M-"+ (keycode - 128).chr
118
+ when ?\M-\C-A..?\M-\C-Z
119
+ return "M-C-"+ (keycode - 32).chr
120
+ when ?\M-0..?\M-9
121
+ return "M-"+ (keycode-?\M-0).to_s
122
+ when 32:
123
+ return "Space"
124
+ when 27:
125
+ return "Esc"
126
+ when ?\C-]
127
+ return "C-]"
128
+ when 258
129
+ return "down"
130
+ when 259
131
+ return "up"
132
+ when 260
133
+ return "left"
134
+ when 261
135
+ return "right"
136
+ when KEY_F1..KEY_F12
137
+ return "F"+ (keycode-264).to_s
138
+ when 330
139
+ return "delete"
140
+ when 127
141
+ return "bs"
142
+ when 353
143
+ return "btab"
144
+ when 481
145
+ return "M-S-tab"
146
+ else
147
+ others=[?\M--,?\M-+,?\M-=,?\M-',?\M-",?\M-;,?\M-:,?\M-\,, ?\M-.,?\M-<,?\M->]
148
+ s_others=%w[M-- M-+ M-= M-' M-" M-; M-: M-\, M-. M-<]
149
+ if others.include? keycode
150
+ index = others.index keycode
151
+ return s_others[index]
152
+ end
153
+ # all else failed
154
+ return keycode.to_s
155
+ end
156
+ end
157
+
158
+ def get_color default=$datacolor, color=@color, bgcolor=@bgcolor
159
+ if bgcolor.is_a? String and color.is_a? String
160
+ acolor = ColorMap.get_color(color, bgcolor)
161
+ else
162
+ acolor = default
163
+ end
164
+ return acolor
165
+ end
166
+ end
167
+
168
+ module EventHandler
169
+ ##
170
+ # bind an event to a block, optional args will also be passed when calling
171
+ def bind event, *xargs, &blk
172
+ #$log.debug "#{self} called EventHandler BIND #{event}, args:#{xargs} "
173
+ @handler ||= {}
174
+ @event_args ||= {}
175
+ #@handler[event] = blk
176
+ #@event_args[event] = xargs
177
+ @handler[event] ||= []
178
+ @handler[event] << blk
179
+ @event_args[event] ||= []
180
+ @event_args[event] << xargs
181
+ end
182
+ alias :add_binding :bind # temporary, needs a proper name to point out that we are adding
183
+
184
+ # NOTE: Do we have a way of removing bindings
185
+ # # TODO check if event is valid. Classes need to define what valid event names are
186
+
187
+ ##
188
+ # Fire all bindings for given event
189
+ # e.g. fire_handler :ENTER, self
190
+ # currently object usually contains self which is perhaps a bit of a waste,
191
+ # could contain an event object with source, and some relevant methods or values
192
+ def fire_handler event, object
193
+ #$log.debug " def fire_handler evt:#{event}, o: #{object}, #{self}, hdnler:#{@handler}"
194
+ if !@handler.nil?
195
+ #blk = @handler[event]
196
+ ablk = @handler[event]
197
+ if !ablk.nil?
198
+ aeve = @event_args[event]
199
+ ablk.each_with_index do |blk, ix|
200
+ #$log.debug "#{self} called EventHandler firehander #{@name}, #{event}, obj: #{object},args: #{aeve[ix]}"
201
+ blk.call object, *aeve[ix]
202
+ end
203
+ end # if
204
+ end # if
205
+ end
206
+ ## added on 2009-01-08 00:33
207
+ # goes with dsl_property
208
+ # Need to inform listeners
209
+ def fire_property_change text, oldvalue, newvalue
210
+ #$log.debug " FPC #{self}: #{text} #{oldvalue}, #{newvalue}"
211
+ @repaint_required = true
212
+ end
213
+
214
+ end # module eventh
215
+
216
+ module ConfigSetup
217
+ # private
218
+ def variable_set var, val
219
+ nvar = "@#{var}"
220
+ send("#{var}", val) # 2009-01-08 01:30 BIG CHANGE calling methods too here.
221
+ #instance_variable_set(nvar, val) # we should not call this !!! bypassing
222
+ end
223
+ def configure(*val , &block)
224
+ case val.size
225
+ when 1
226
+ return @config[val[0]]
227
+ when 2
228
+ @config[val[0]] = val[1]
229
+ variable_set(val[0], val[1])
230
+ end
231
+ instance_eval &block if block_given?
232
+ end
233
+ ##
234
+ # returns param from hash. Unused and untested.
235
+ def cget param
236
+ @config[param]
237
+ end
238
+ # this bypasses our methods and sets directly !
239
+ def config_setup aconfig
240
+ @config = aconfig
241
+ @config.each_pair { |k,v| variable_set(k,v) }
242
+ end
243
+ end # module config
244
+ ##
245
+ # Basic widget class.
246
+ # NOTE: I may soon remove the config hash. I don't use it and its just making things heavy.
247
+ # Unless someone convinces me otherwise.
248
+ class Widget
249
+ include DSL
250
+ include EventHandler
251
+ include ConfigSetup
252
+ include RubyCurses::Utils
253
+ dsl_property :text
254
+ #dsl_accessor :text_variable
255
+ #dsl_accessor :underline # offset of text to underline DEPRECATED
256
+ dsl_property :width # desired width of text
257
+ #dsl_accessor :wrap_length # wrap length of text, if applic UNUSED
258
+
259
+ # next 3 to be checked if used or not. Copied from TK.
260
+ dsl_property :select_foreground, :select_background # color init_pair
261
+ dsl_property :highlight_foreground, :highlight_background # color init_pair
262
+ dsl_property :disabled_foreground, :disabled_background # color init_pair
263
+
264
+ # FIXME is enabled used?
265
+ dsl_accessor :focusable, :enabled # boolean
266
+ dsl_property :row, :col # location of object
267
+ dsl_property :color, :bgcolor # normal foreground and background
268
+ dsl_property :attr # attribute bold, normal, reverse
269
+ dsl_accessor :name # name to refr to or recall object by_name
270
+ attr_accessor :id #, :zorder
271
+ attr_accessor :curpos # cursor position inside object
272
+ attr_reader :config # COULD GET AXED SOON NOTE
273
+ attr_accessor :form # made accessor 2008-11-27 22:32 so menu can set
274
+ attr_accessor :state # normal, selected, highlighted
275
+ attr_reader :row_offset, :col_offset # where should the cursor be placed to start with
276
+ dsl_property :visible # boolean # 2008-12-09 11:29
277
+ #attr_accessor :modified # boolean, value modified or not (moved from field 2009-01-18 00:14 )
278
+ dsl_accessor :help_text # added 2009-01-22 17:41 can be used for status/tooltips
279
+
280
+ def initialize form, aconfig={}, &block
281
+ @form = form
282
+ @bgcolor ||= "black" # 0
283
+ @row_offset = @col_offset = 0
284
+ @state = :NORMAL
285
+ @color ||= "white" # $datacolor
286
+ @attr = nil
287
+ @handler = {}
288
+ @event_args = {}
289
+ config_setup aconfig # @config.each_pair { |k,v| variable_set(k,v) }
290
+ instance_eval &block if block_given?
291
+ # @id = form.add_widget(self) if !form.nil? and form.respond_to? :add_widget
292
+ set_form(form) unless form.nil?
293
+ end
294
+ def init_vars
295
+ # just in case anyone does a super. Not putting anything here
296
+ # since i don't want anyone accidentally overriding
297
+ end
298
+
299
+ # modified
300
+ ##
301
+ # typically read will be overridden to check if value changed from what it was on enter.
302
+ # getter and setter for modified (added 2009-01-18 12:31 )
303
+ def modified?
304
+ @modified
305
+ end
306
+ def set_modified tf=true
307
+ @modified = tf
308
+ @form.modified = true if tf
309
+ end
310
+ alias :modified :set_modified
311
+ ##
312
+ # getter and setter for text_variable
313
+ def text_variable(*val)
314
+ if val.empty?
315
+ @text_variable
316
+ else
317
+ @text_variable = val[0]
318
+ $log.debug " GOING TO CALL ADD DELPENDENT #{self}"
319
+ @text_variable.add_dependent(self)
320
+ end
321
+ end
322
+
323
+ ## got left out by mistake 2008-11-26 20:20
324
+ def on_enter
325
+ fire_handler :ENTER, self
326
+ end
327
+ ## got left out by mistake 2008-11-26 20:20
328
+ def on_leave
329
+ fire_handler :LEAVE, self
330
+ end
331
+ def rowcol
332
+ # $log.debug "widgte rowcol : #{@row+@row_offset}, #{@col+@col_offset}"
333
+ return @row+@row_offset, @col+@col_offset
334
+ end
335
+ ## return the value of the widget.
336
+ # In cases where selection is possible, should return selected value/s
337
+ def getvalue
338
+ @text_variable && @text_variable.value || @text
339
+ end
340
+ ##
341
+ # Am making a separate method since often value for print differs from actual value
342
+ def getvalue_for_paint
343
+ getvalue
344
+ end
345
+ ##
346
+ # default repaint method. Called by form for all widgets.
347
+ def repaint
348
+ r,c = rowcol
349
+ $log.debug("widget repaint : r:#{r} c:#{c} col:#{@color}" )
350
+ value = getvalue_for_paint
351
+ len = @display_length || value.length
352
+ if @bgcolor.is_a? String and @color.is_a? String
353
+ acolor = ColorMap.get_color(@color, @bgcolor)
354
+ else
355
+ acolor = $datacolor
356
+ end
357
+ @form.window.printstring r, c, "%-*s" % [len, value], acolor, @attr
358
+ # next line should be in same color but only have @att so we can change att is nec
359
+ #@form.window.mvchgat(y=r, x=c, max=len, Ncurses::A_NORMAL, @bgcolor, nil)
360
+ end
361
+
362
+ def destroy
363
+ $log.debug "DESTROY : widget"
364
+ panel = @window.panel
365
+ Ncurses::Panel.del_panel(panel) if !panel.nil?
366
+ @window.delwin if !@window.nil?
367
+ end
368
+ # @ deprecated pls call windows method
369
+ def printstring(win, r,c,string, color, att = Ncurses::A_NORMAL)
370
+
371
+ att = Ncurses::A_NORMAL if att.nil?
372
+ case att.to_s.downcase
373
+ when 'underline'
374
+ att = Ncurses::A_UNDERLINE
375
+ $log.debug "UL att #{att}"
376
+ when 'bold'
377
+ att = Ncurses::A_BOLD
378
+ when 'blink'
379
+ att = Ncurses::A_BLINK
380
+ when 'reverse'
381
+ att = Ncurses::A_REVERSE
382
+ else
383
+ att = Ncurses::A_NORMAL
384
+ end
385
+ #$log.debug "att #{att}"
386
+
387
+ #att = bold ? Ncurses::A_BLINK|Ncurses::A_BOLD : Ncurses::A_NORMAL
388
+ # att = bold ? Ncurses::A_BOLD : Ncurses::A_NORMAL
389
+ win.attron(Ncurses.COLOR_PAIR(color) | att)
390
+ win.mvprintw(r, c, "%s", string);
391
+ win.attroff(Ncurses.COLOR_PAIR(color) | att)
392
+ end
393
+ # in those rare cases where we create widget without a form, and later give it to
394
+ # some other program which sets the form. Dirty, we should perhaps create widgets
395
+ # without forms, and add explicitly.
396
+ def set_form form
397
+ raise "Form is nil in set_form" if form.nil?
398
+ @form = form
399
+ @id = form.add_widget(self) if !form.nil? and form.respond_to? :add_widget
400
+ end
401
+ # puts cursor on correct row.
402
+ def set_form_row
403
+ raise "empty todo widget"
404
+ # @form.row = @row + 1 + @winrow
405
+ @form.row = @row + 1
406
+ end
407
+ # set cursor on correct column, widget
408
+ def set_form_col col=@curpos
409
+ @curpos = col
410
+ @form.col = @col + @col_offset + @curpos
411
+ end
412
+ def hide
413
+ @visible = false
414
+ end
415
+ def show
416
+ @visible = true
417
+ end
418
+ def remove
419
+ @form.remove_widget(self)
420
+ end
421
+ def move row, col
422
+ @row = row
423
+ @col = col
424
+ end
425
+ ##
426
+ # moves focus to this field
427
+ # XXX we must look into running on_leave of previous field
428
+ def focus
429
+ return if !@focusable
430
+ if @form.validate_field != -1
431
+ @form.select_field @id
432
+ end
433
+ end
434
+ def get_color default=$datacolor, _color=@color, _bgcolor=@bgcolor
435
+ if _bgcolor.is_a? String and _color.is_a? String
436
+ acolor = ColorMap.get_color(_color, _bgcolor)
437
+ else
438
+ acolor = default
439
+ end
440
+ return acolor
441
+ end
442
+ ##
443
+ # bind an action to a key, required if you create a button which has a hotkey
444
+ # or a field to be focussed on a key, or any other user defined action based on key
445
+ # e.g. bind_key ?\C-x, object, block
446
+ # added 2009-01-06 19:13 since widgets need to handle keys properly
447
+ def bind_key keycode, *args, &blk
448
+ $log.debug "called bind_key BIND #{keycode} #{keycode_tos(keycode)} #{args} "
449
+ @key_handler ||= {}
450
+ @key_args ||= {}
451
+ @key_handler[keycode] = blk
452
+ @key_args[keycode] = args
453
+ end
454
+ ##
455
+ # remove a binding that you don't want
456
+ def unbind_key keycode
457
+ @key_args.delete keycode unless @key_args.nil?
458
+ @key_handler.delete keycode unless @key_handler.nil?
459
+ end
460
+
461
+ # e.g. process_key ch, self
462
+ # returns UNHANDLED if no block for it
463
+ # after form handles basic keys, it gives unhandled key to current field, if current field returns
464
+ # unhandled, then it checks this map.
465
+ # added 2009-01-06 19:13 since widgets need to handle keys properly
466
+ # added 2009-01-18 12:58 returns ret val of blk.call
467
+ # so that if block does not handle, the key can still be handled
468
+ # e.g. table last row, last col does not handle, so it will auto go to next field
469
+ def process_key keycode, object
470
+ return :UNHANDLED if @key_handler.nil?
471
+ blk = @key_handler[keycode]
472
+ return :UNHANDLED if blk.nil?
473
+ #$log.debug "called process_key #{object}, #{@key_args[keycode]}"
474
+ return blk.call object, *@key_args[keycode]
475
+ #0
476
+ end
477
+ ##
478
+ # to be added at end of handle_key of widgets so instlalled actions can be checked
479
+ def handle_key(ch)
480
+ ret = process_key ch, self
481
+ return :UNHANDLED if ret == :UNHANDLED
482
+ end
483
+ ## ADD HERE WIDGET
484
+ end
485
+
486
+ ##
487
+ #
488
+ # TODO: we don't have an event for when form is entered and exited.
489
+ # Current ENTER and LEAVE are for when any widgt is entered, so a common event can be put for all widgets
490
+ # in one place.
491
+ class Form
492
+ include EventHandler
493
+ include RubyCurses::Utils
494
+ attr_reader :value
495
+ attr_reader :widgets
496
+ attr_accessor :window
497
+ attr_accessor :row, :col
498
+ # attr_accessor :color
499
+ # attr_accessor :bgcolor
500
+ attr_accessor :padx
501
+ attr_accessor :pady
502
+ attr_accessor :modified
503
+ attr_accessor :active_index
504
+ attr_reader :by_name # hash containing widgets by name for retrieval
505
+ attr_reader :menu_bar
506
+ attr_accessor :navigation_policy # :CYCLICAL will cycle around. Needed to move to other tabs
507
+ def initialize win, &block
508
+ @window = win
509
+ @widgets = []
510
+ @by_name = {}
511
+ @active_index = -1
512
+ @padx = @pady = 0
513
+ @row = @col = -1
514
+ @handler = {}
515
+ @modified = false
516
+ @focusable = true
517
+ @navigation_policy ||= :CYCLICAL
518
+ instance_eval &block if block_given?
519
+ @firsttime = true # internal, don't touch
520
+ end
521
+ ##
522
+ # set this menubar as the form's menu bar.
523
+ # also bind the toggle_key for popping up.
524
+ # Should this not be at application level ?
525
+ def set_menu_bar mb
526
+ @menu_bar = mb
527
+ add_widget mb
528
+ mb.toggle_key ||= 27 # ESC
529
+ if !mb.toggle_key.nil?
530
+ ch = mb.toggle_key
531
+ bind_key(ch) do |_form|
532
+ if !@menu_bar.nil?
533
+ @menu_bar.toggle
534
+ @menu_bar.handle_keys
535
+ end
536
+ end
537
+ end
538
+ end
539
+ ##
540
+ # Add given widget to widget list and returns an incremental id.
541
+ # Adding to widgets, results in it being painted, and focussed.
542
+ # removing a widget and adding can give the same ID's, however at this point we are not
543
+ # really using ID. But need to use an incremental int in future.
544
+ def add_widget widget
545
+ # this help to access widget by a name
546
+ if widget.respond_to? :name and !widget.name.nil?
547
+ @by_name[widget.name] = widget
548
+ end
549
+
550
+ @widgets << widget
551
+ return @widgets.length-1
552
+ end
553
+ # remove a widget
554
+ # added 2008-12-09 12:18
555
+ def remove_widget widget
556
+ if widget.respond_to? :name and !widget.name.nil?
557
+ $log.debug "removing from byname: #{widget.name} "
558
+ @by_name.delete(widget.name)
559
+ end
560
+ @widgets.delete widget
561
+ end
562
+ # form repaint
563
+ # to be called at some interval, such as after each keypress.
564
+ def repaint
565
+ @widgets.each do |f|
566
+ next if f.visible == false # added 2008-12-09 12:17
567
+ f.repaint
568
+ end
569
+ @window.clear_error
570
+ @window.print_status_message $status_message unless $status_message.nil?
571
+ @window.print_error_message $error_message unless $error_message.nil?
572
+ $error_message = $status_message = nil
573
+ # this can bomb if someone sets row. We need a better way!
574
+ if @row == -1 #or @firsttime == true
575
+ #set_field_cursor 0
576
+ $log.debug "form repaint calling select field 0"
577
+ #select_field 0
578
+ req_first_field
579
+ #@firsttime = false
580
+ end
581
+ setpos
582
+ @window.wrefresh
583
+ end
584
+ ##
585
+ # move cursor to where the fields row and col are
586
+ # private
587
+ def setpos r=@row, c=@col
588
+ # $log.debug "setpos : #{r} #{c}"
589
+ @window.wmove r,c
590
+ end
591
+ def get_current_field
592
+ select_next_field if @active_index == -1
593
+ return nil if @active_index.nil? # for forms that have no focusable field 2009-01-08 12:22
594
+ @widgets[@active_index]
595
+ end
596
+ def req_first_field
597
+ @active_index = -1 # FIXME HACK
598
+ select_next_field
599
+ end
600
+ def req_last_field
601
+ @active_index = nil
602
+ select_prev_field
603
+ end
604
+ ## do not override
605
+ # form's trigger, fired when any widget loses focus
606
+ # This wont get called in editor components in tables, since they are formless XXX
607
+ def on_leave f
608
+ return if f.nil?
609
+ f.state = :NORMAL
610
+ # on leaving update text_variable if defined. Should happen on modified only
611
+ # should this not be f.text_var ... f.buffer ? XXX 2008-11-25 18:58
612
+ #f.text_variable.value = f.buffer if !f.text_variable.nil? # 2008-12-20 23:36
613
+ f.on_leave if f.respond_to? :on_leave
614
+ fire_handler :LEAVE, f
615
+ ## to test XXX in combo boxes the box may not be editable by be modified by selection.
616
+ if f.respond_to? :editable and f.modified?
617
+ $log.debug " Form about to fire CHANGED for #{f} "
618
+ f.fire_handler(:CHANGED, f)
619
+ end
620
+ end
621
+ def on_enter f
622
+ return if f.nil?
623
+ f.state = :HIGHLIGHTED
624
+ f.modified false
625
+ #f.set_modified false
626
+ f.on_enter if f.respond_to? :on_enter
627
+ fire_handler :ENTER, f
628
+ end
629
+ ##
630
+ # puts focus on the given field/widget index
631
+ # XXX if called externally will not run a on_leave of previous field
632
+ def select_field ix0
633
+ return if @widgets.nil? or @widgets.empty? or !@widgets[ix0].focusable
634
+ # $log.debug "insdie select field : #{ix0} ai #{@active_index}"
635
+ f = @widgets[ix0]
636
+ if f.focusable
637
+ @active_index = ix0
638
+ @row, @col = f.rowcol
639
+ # $log.debug "insdie sele nxt field : ROW #{@row} COL #{@col} "
640
+ @window.wmove @row, @col
641
+ on_enter f
642
+ f.curpos = 0
643
+ repaint
644
+ @window.refresh
645
+ else
646
+ $log.debug "insdie sele nxt field ENABLED FALSE : act #{@active_index} ix0 #{ix0}"
647
+ end
648
+ end
649
+ ##
650
+ # run validate_field on a field, usually whatevers current
651
+ # before transferring control
652
+ # We should try to automate this so developer does not have to remember to call it.
653
+ def validate_field f=@widgets[@active_index]
654
+ begin
655
+ on_leave f
656
+ rescue => err
657
+ $log.debug "form: validate_field caught EXCEPTION #{err}"
658
+ $log.debug(err.backtrace.join("\n"))
659
+ $error_message = "#{err}"
660
+ Ncurses.beep
661
+ return -1
662
+ end
663
+ return 0
664
+ end
665
+ # put focus on next field
666
+ # will cycle by default, unless navigation policy not :CYCLICAL
667
+ # in which case returns :NO_NEXT_FIELD.
668
+ def select_next_field
669
+ return if @widgets.nil? or @widgets.empty?
670
+ #$log.debug "insdie sele nxt field : #{@active_index} WL:#{@widgets.length}"
671
+ if @active_index.nil?
672
+ @active_index = -1
673
+ else
674
+ f = @widgets[@active_index]
675
+ begin
676
+ on_leave f
677
+ rescue => err
678
+ $log.debug "select_next_field: caught EXCEPTION #{err}"
679
+ $log.debug(err.backtrace.join("\n"))
680
+ $error_message = "#{err}"
681
+ Ncurses.beep
682
+ return
683
+ end
684
+ end
685
+ index = @active_index + 1
686
+ index.upto(@widgets.length-1) do |i|
687
+ f = @widgets[i]
688
+ if f.focusable
689
+ select_field i
690
+ return
691
+ end
692
+ end
693
+ #req_first_field
694
+ #$log.debug "insdie sele nxt field FAILED: #{@active_index} WL:#{@widgets.length}"
695
+ ## added on 2008-12-14 18:27 so we can skip to another form/tab
696
+ if @navigation_policy == :CYCLICAL
697
+ @active_index = nil
698
+ # recursive call worked, but bombed if no focusable field!
699
+ #select_next_field
700
+ 0.upto(index-1) do |i|
701
+ f = @widgets[i]
702
+ if f.focusable
703
+ select_field i
704
+ return
705
+ end
706
+ end
707
+ end
708
+ return :NO_NEXT_FIELD
709
+ end
710
+ ##
711
+ # put focus on previous field
712
+ # will cycle by default, unless navigation policy not :CYCLICAL
713
+ # in which case returns :NO_PREV_FIELD.
714
+ def select_prev_field
715
+ return if @widgets.nil? or @widgets.empty?
716
+ #$log.debug "insdie sele prev field : #{@active_index} WL:#{@widgets.length}"
717
+ if @active_index.nil?
718
+ @active_index = @widgets.length
719
+ else
720
+ f = @widgets[@active_index]
721
+ begin
722
+ on_leave f
723
+ rescue => err
724
+ $log.debug " cauGHT EXCEPTION #{err}"
725
+ Ncurses.beep
726
+ return
727
+ end
728
+ end
729
+
730
+ index = @active_index - 1
731
+ (index).downto(0) do |i|
732
+ f = @widgets[i]
733
+ if f.focusable
734
+ select_field i
735
+ return
736
+ end
737
+ end
738
+ # $log.debug "insdie sele prev field FAILED: #{@active_index} WL:#{@widgets.length}"
739
+ ## added on 2008-12-14 18:27 so we can skip to another form/tab
740
+ # 2009-01-08 12:24 no recursion, can be stack overflows if no focusable field
741
+ if @navigation_policy == :CYCLICAL
742
+ @active_index = nil # HACK !!!
743
+ #select_prev_field
744
+ total = @widgets.length-1
745
+ total.downto(index-1) do |i|
746
+ f = @widgets[i]
747
+ if f.focusable
748
+ select_field i
749
+ return
750
+ end
751
+ end
752
+ end
753
+ return :NO_PREV_FIELD
754
+ end
755
+ alias :req_next_field :select_next_field
756
+ alias :req_prev_field :select_prev_field
757
+ ##
758
+ # move cursor by num columns
759
+ def addcol num
760
+ return if @col.nil? or @col == -1
761
+ @col += num
762
+ @window.wmove @row, @col
763
+ end
764
+ ##
765
+ # move cursor by given rows and columns, can be negative.
766
+ def addrowcol row,col
767
+ return if @col.nil? or @col == -1
768
+ return if @row.nil? or @row == -1
769
+ @col += col
770
+ @row += row
771
+ @window.wmove @row, @col
772
+ end
773
+ ##
774
+ # bind an action to a key, required if you create a button which has a hotkey
775
+ # or a field to be focussed on a key, or any other user defined action based on key
776
+ # e.g. bind_key ?\C-x, object, block
777
+ def bind_key keycode, *args, &blk
778
+ $log.debug "called bind_key BIND #{keycode} #{keycode_tos(keycode)} #{args} "
779
+ @key_handler ||= {}
780
+ @key_args ||= {}
781
+ @key_handler[keycode] = blk
782
+ @key_args[keycode] = args
783
+ end
784
+
785
+ # e.g. process_key ch, self
786
+ # returns UNHANDLED if no block for it
787
+ # after form handles basic keys, it gives unhandled key to current field, if current field returns
788
+ # unhandled, then it checks this map.
789
+ def process_key keycode, object
790
+ return :UNHANDLED if @key_handler.nil?
791
+ blk = @key_handler[keycode]
792
+ return :UNHANDLED if blk.nil?
793
+ $log.debug "called process_key #{object}, #{@key_args[keycode]}"
794
+ blk.call object, *@key_args[keycode]
795
+ 0
796
+ end
797
+ ## forms handle keys
798
+ # mainly traps tab and backtab to navigate between widgets.
799
+ # I know some widgets will want to use tab, e.g edit boxes for entering a tab
800
+ # or for completion.
801
+ def handle_key(ch)
802
+ case ch
803
+ when -1
804
+ return
805
+ else
806
+ field = get_current_field
807
+ handled = :UNHANDLED
808
+ handled = field.handle_key ch unless field.nil? # no field focussable
809
+ # some widgets like textarea and list handle up and down
810
+ if handled == :UNHANDLED or handled == -1 or field.nil?
811
+ case ch
812
+ when 9, ?\M-\C-i # tab and M-tab in case widget eats tab (such as Table)
813
+ ret = select_next_field
814
+ return ret if ret == :NO_NEXT_FIELD
815
+ # alt-shift-tab or backtab (in case Table eats backtab)
816
+ when 353, 481 ## backtab added 2008-12-14 18:41
817
+ ret = select_prev_field
818
+ return ret if ret == :NO_PREV_FIELD
819
+ when KEY_UP
820
+ select_prev_field
821
+ when KEY_DOWN
822
+ select_next_field
823
+ else
824
+ ret = process_key ch, self
825
+ return :UNHANDLED if ret == :UNHANDLED
826
+ end
827
+ end
828
+ end
829
+ $log.debug " form before repaint"
830
+ repaint
831
+ end
832
+ ##
833
+ # test program to dump data onto log
834
+ # The problem I face is that since widget array contains everything that should be displayed
835
+ # I do not know what all the user wants - what are his data entry fields.
836
+ # A user could have disabled entry on some field after modification, so i can't use focusable
837
+ # or editable as filters. I just dump everything?
838
+ # What's more, currently getvalue has been used by paint to return what needs to be displayed -
839
+ # at least by label and button.
840
+ def dump_data
841
+ $log.debug " DUMPING DATA "
842
+ @widgets.each do |w|
843
+ # we need checkbox and radio button values
844
+ #next if w.is_a? RubyCurses::Button or w.is_a? RubyCurses::Label
845
+ next if w.is_a? RubyCurses::Label
846
+ next if !w.is_a? RubyCurses::Widget
847
+ if w.respond_to? :getvalue
848
+ $log.debug " #{w.name} #{w.getvalue}"
849
+ else
850
+ $log.debug " #{w.name} DOES NOT RESPOND TO getvalue"
851
+ end
852
+ end
853
+ $log.debug " END DUMPING DATA "
854
+ end
855
+
856
+ ## ADD HERE FORM
857
+ end
858
+
859
+ ##
860
+ # Text edit field
861
+ # To get value use getvalue()
862
+ # TODO - test text_variable
863
+ #
864
+ class Field < Widget
865
+ dsl_accessor :maxlen # maximum length allowed into field
866
+ attr_reader :buffer # actual buffer being used for storage
867
+ dsl_accessor :label # label of field
868
+ dsl_accessor :default # TODO use set_buffer for now
869
+ dsl_accessor :values # validate against provided list
870
+ dsl_accessor :valid_regex # validate against regular expression
871
+
872
+ dsl_accessor :chars_allowed # regex, what characters to allow, will ignore all else
873
+ dsl_accessor :display_length # how much to display
874
+ dsl_accessor :bgcolor # background color 'red' 'black' 'cyan' etc
875
+ dsl_accessor :color # foreground colors from Ncurses COLOR_xxxx
876
+ dsl_accessor :show # what charactr to show for each char entered (password field)
877
+ dsl_accessor :null_allowed # allow nulls, don't validate if null # added 2008-12-22 12:38
878
+
879
+ # any new widget that has editable should have modified also
880
+ dsl_accessor :editable # allow editing
881
+
882
+ attr_reader :form
883
+ attr_reader :handler # event handler
884
+ attr_reader :type # datatype of field, currently only sets chars_allowed
885
+ attr_reader :curpos # cursor position in buffer current
886
+ attr_accessor :datatype # crrently set during set_buffer
887
+ attr_reader :original_value # value on entering field
888
+
889
+ def initialize form, config={}, &block
890
+ @form = form
891
+ @buffer = String.new
892
+ #@type=config.fetch("type", :varchar)
893
+ @display_length = config.fetch("display_length", 20)
894
+ @maxlen=config.fetch("maxlen", @display_length)
895
+ @row = config.fetch("row", 0)
896
+ @col = config.fetch("col", 0)
897
+ @bgcolor = config.fetch("bgcolor", $def_bg_color)
898
+ @color = config.fetch("color", $def_fg_color)
899
+ @name = config.fetch("name", nil)
900
+ @editable = config.fetch("editable", true)
901
+ @focusable = config.fetch("focusable", true)
902
+ @handler = {}
903
+ @event_args = {} # arguments passed at time of binding, to use when firing event
904
+ init_vars
905
+ super
906
+ end
907
+ def init_vars
908
+ @pcol = 0 # needed for horiz scrolling
909
+ @curpos = 0 # current cursor position in buffer
910
+ @modified = false
911
+ end
912
+ def text_variable tv
913
+ @text_variable = tv
914
+ set_buffer tv.value
915
+ end
916
+ ##
917
+ # define a datatype, currently only influences chars allowed
918
+ # integer and float. what about allowing a minus sign? XXX
919
+ def type dtype
920
+ case dtype.to_s.downcase
921
+ when 'integer'
922
+ @chars_allowed = /\d/ if @chars_allowed.nil?
923
+ when 'numeric'
924
+ @chars_allowed = /[\d\.]/ if @chars_allowed.nil?
925
+ when 'alpha'
926
+ @chars_allowed = /[a-zA-Z]/ if @chars_allowed.nil?
927
+ when 'alnum'
928
+ @chars_allowed = /[a-zA-Z0-9]/ if @chars_allowed.nil?
929
+ end
930
+ end
931
+ def putch char
932
+ return -1 if !@editable or @buffer.length >= @maxlen
933
+ if @chars_allowed != nil
934
+ return if char.match(@chars_allowed).nil?
935
+ end
936
+ @buffer.insert(@curpos, char)
937
+ @curpos += 1 if @curpos < @maxlen
938
+ @modified = true
939
+ $log.debug " FIELD FIRING CHANGE: #{char} at new #{@curpos}: bl:#{@buffer.length} buff:[#{@buffer}]"
940
+ fire_handler :CHANGE, self # 2008-12-09 14:51
941
+ 0
942
+ end
943
+
944
+ ##
945
+ # TODO : sending c>=0 allows control chars to go. Should be >= ?A i think.
946
+ def putc c
947
+ if c >= 0 and c <= 127
948
+ ret = putch c.chr
949
+ if ret == 0
950
+ if addcol(1) == -1 # if can't go forward, try scrolling
951
+ # scroll if exceeding display len but less than max len
952
+ if @curpos > @display_length and @curpos <= @maxlen
953
+ @pcol += 1 if @pcol < @display_length
954
+ end
955
+ end
956
+ set_modified
957
+ end
958
+ end
959
+ return -1
960
+ end
961
+ def delete_at index=@curpos
962
+ return -1 if !@editable
963
+ @buffer.slice!(index,1)
964
+ $log.debug " delete at #{index}: #{@buffer.length}: #{@buffer}"
965
+ @modified = true
966
+ fire_handler :CHANGE, self # 2008-12-09 14:51
967
+ end
968
+ ##
969
+ # should this do a dup ??
970
+ def set_buffer value
971
+ @datatype = value.class
972
+ #$log.debug " FIELD DATA #{@datatype}"
973
+ @buffer = value.to_s
974
+ @curpos = 0
975
+ end
976
+ # converts back into original type
977
+ # changed to convert on 2009-01-06 23:39
978
+ def getvalue
979
+ dt = @datatype || String
980
+ case dt.to_s
981
+ when "String"
982
+ return @buffer
983
+ when "Fixnum"
984
+ return @buffer.to_i
985
+ when "Float"
986
+ return @buffer.to_f
987
+ else
988
+ return @buffer.to_s
989
+ end
990
+ end
991
+
992
+ def set_label label
993
+ @label = label
994
+ label.row @row if label.row == -1
995
+ label.col @col-(label.name.length+1) if label.col == -1
996
+ label.label_for(self)
997
+ end
998
+ def repaint
999
+ # $log.debug("FIELD: #{id}, #{zorder}, #{focusable}")
1000
+ printval = getvalue_for_paint().to_s # added 2009-01-06 23:27
1001
+ printval = show()*printval.length unless @show.nil?
1002
+ if !printval.nil?
1003
+ if printval.length > display_length # only show maxlen
1004
+ printval = printval[@pcol..@pcol+display_length-1]
1005
+ else
1006
+ printval = printval[@pcol..-1]
1007
+ end
1008
+ end
1009
+ #printval = printval[0..display_length-1] if printval.length > display_length
1010
+ if @bgcolor.is_a? String and @color.is_a? String
1011
+ acolor = ColorMap.get_color(@color, @bgcolor)
1012
+ else
1013
+ acolor = $datacolor
1014
+ end
1015
+ @form.window.printstring row, col, sprintf("%-*s", display_length, printval), acolor, @attr
1016
+ end
1017
+ def set_focusable(tf)
1018
+ @focusable = tf
1019
+ # @form.regenerate_focusables
1020
+ end
1021
+
1022
+ # field
1023
+ def handle_key ch
1024
+ case ch
1025
+ when KEY_LEFT
1026
+ cursor_backward
1027
+ when KEY_RIGHT
1028
+ cursor_forward
1029
+ when KEY_BACKSPACE, 127
1030
+ delete_prev_char if @editable
1031
+ #when KEY_UP
1032
+ # $log.debug " FIELD GOT KEY_UP, NOW IGNORING 2009-01-16 17:52 "
1033
+ #@form.select_prev_field # in a table this should not happen 2009-01-16 17:47
1034
+ # return :UNHANDLED
1035
+ #when KEY_DOWN
1036
+ # $log.debug " FIELD GOT KEY_DOWN, NOW IGNORING 2009-01-16 17:52 "
1037
+ #@form.select_next_field # in a table this should not happen 2009-01-16 17:47
1038
+ # return :UNHANDLED
1039
+ when KEY_ENTER, 10, 13
1040
+ if respond_to? :fire
1041
+ fire
1042
+ end
1043
+ when 330
1044
+ delete_curr_char if @editable
1045
+ when ?\C-a
1046
+ cursor_home
1047
+ when ?\C-e
1048
+ cursor_end
1049
+ when ?\C-k
1050
+ delete_eol if @editable
1051
+ when ?\C-u
1052
+ @buffer.insert @curpos, @delete_buffer unless @delete_buffer.nil?
1053
+ when 32..126
1054
+ #$log.debug("FIELD: ch #{ch} ,at #{@curpos}, buffer:[#{@buffer}] bl: #{@buffer.to_s.length}")
1055
+ putc ch
1056
+ when 27 # escape
1057
+ $log.debug " ADDED FIELD ESCAPE on 2009-01-18 12:27 XXX #{@original_value}"
1058
+ set_buffer @original_value
1059
+ else
1060
+ ret = super
1061
+ return ret
1062
+ end
1063
+ 0 # 2008-12-16 23:05 without this -1 was going back so no repaint
1064
+ end
1065
+ ##
1066
+ # position cursor at start of field
1067
+ def cursor_home
1068
+ set_form_col 0
1069
+ @pcol = 0
1070
+ end
1071
+ ##
1072
+ # goto end of field, "end" is a keyword so could not use it.
1073
+ def cursor_end
1074
+ blen = @buffer.rstrip.length
1075
+ if blen < @display_length
1076
+ set_form_col blen
1077
+ else
1078
+ @pcol = blen-@display_length
1079
+ set_form_col @display_length-1
1080
+ end
1081
+ @curpos = blen # HACK XXX
1082
+ # $log.debug " crusor END cp:#{@curpos} pcol:#{@pcol} b.l:#{@buffer.length} d_l:#{@display_length} fc:#{@form.col}"
1083
+ #set_form_col @buffer.length
1084
+ end
1085
+ def delete_eol
1086
+ return -1 unless @editable
1087
+ pos = @curpos-1
1088
+ @delete_buffer = @buffer[@curpos..-1]
1089
+ # if pos is 0, pos-1 becomes -1, end of line!
1090
+ @buffer = pos == -1 ? "" : @buffer[0..pos]
1091
+ fire_handler :CHANGE, self # 2008-12-09 14:51
1092
+ return @delete_buffer
1093
+ end
1094
+ def cursor_forward
1095
+ if @curpos < @buffer.length
1096
+ if addcol(1)==-1 # go forward if you can, else scroll
1097
+ @pcol += 1 if @pcol < @display_length
1098
+ end
1099
+ @curpos += 1
1100
+ end
1101
+ # $log.debug " crusor FORWARD cp:#{@curpos} pcol:#{@pcol} b.l:#{@buffer.length} d_l:#{@display_length} fc:#{@form.col}"
1102
+ end
1103
+ def cursor_backward
1104
+ if @curpos > 0
1105
+ @curpos -= 1
1106
+ if @pcol > 0 and @form.col == @col + @col_offset
1107
+ @pcol -= 1
1108
+ end
1109
+ addcol -1
1110
+ elsif @pcol > 0 # added 2008-11-26 23:05
1111
+ @pcol -= 1
1112
+ end
1113
+ # $log.debug " crusor back cp:#{@curpos} pcol:#{@pcol} b.l:#{@buffer.length} d_l:#{@display_length} fc:#{@form.col}"
1114
+ =begin
1115
+ # this is perfect if not scrolling, but now needs changes
1116
+ if @curpos > 0
1117
+ @curpos -= 1
1118
+ addcol -1
1119
+ end
1120
+ =end
1121
+ end
1122
+ def delete_curr_char
1123
+ return -1 unless @editable
1124
+ delete_at
1125
+ set_modified
1126
+ end
1127
+ def delete_prev_char
1128
+ return -1 if !@editable
1129
+ return if @curpos <= 0
1130
+ @curpos -= 1 if @curpos > 0
1131
+ delete_at
1132
+ set_modified
1133
+ addcol -1
1134
+ end
1135
+ def addcol num
1136
+ if num < 0
1137
+ if @form.col <= @col + @col_offset
1138
+ # $log.debug " error trying to cursor back #{@form.col}"
1139
+ return -1
1140
+ end
1141
+ elsif num > 0
1142
+ if @form.col >= @col + @col_offset + @display_length
1143
+ # $log.debug " error trying to cursor forward #{@form.col}"
1144
+ return -1
1145
+ end
1146
+ end
1147
+ @form.addcol num
1148
+ end
1149
+ # upon leaving a field
1150
+ # returns false if value not valid as per values or valid_regex
1151
+ # 2008-12-22 12:40 if null_allowed, don't validate, but do fire_handlers
1152
+ def on_leave
1153
+ val = getvalue
1154
+ #$log.debug " FIELD ON LEAVE:#{val}. #{@values.inspect}"
1155
+ valid = true
1156
+ if val.to_s.empty? and @null_allowed
1157
+ $log.debug " empty and null allowed"
1158
+ else
1159
+ if !@values.nil?
1160
+ valid = @values.include? val
1161
+ raise FieldValidationException, "Field value (#{val}) not in values: #{@values.join(',')}" unless valid
1162
+ end
1163
+ if !@valid_regex.nil?
1164
+ valid = @valid_regex.match(val.to_s)
1165
+ raise FieldValidationException, "Field not matching regex #{@valid_regex}" unless valid
1166
+ end
1167
+ end
1168
+ # here is where we should set the forms modified to true - 2009-01-18 12:36 XXX
1169
+ if modified?
1170
+ set_modified true
1171
+ end
1172
+ super
1173
+ #return valid
1174
+ end
1175
+ ## save original value on enter, so we can check for modified.
1176
+ # 2009-01-18 12:25
1177
+ def on_enter
1178
+ @original_value = getvalue.dup rescue getvalue
1179
+ super
1180
+ end
1181
+ ##
1182
+ # overriding widget, check for value change
1183
+ # 2009-01-18 12:25
1184
+ def modified?
1185
+ getvalue() != @original_value
1186
+ end
1187
+ # ADD HERE FIELD
1188
+ end
1189
+
1190
+ ##
1191
+ # Like Tk's TkVariable, a simple proxy that can be passed to a widget. The widget
1192
+ # will update the Variable. A variable can be used to link a field with a label or
1193
+ # some other widget.
1194
+ # This is the new version of Variable. Deleting old version on 2009-01-17 12:04
1195
+ class Variable
1196
+
1197
+ def initialize value=""
1198
+ @update_command = []
1199
+ @args = []
1200
+ @value = value
1201
+ @klass = value.class.to_s
1202
+ end
1203
+ def add_dependent obj
1204
+ $log.debug " ADDING DEPENDE #{obj}"
1205
+ @dependents ||= []
1206
+ @dependents << obj
1207
+ end
1208
+ ##
1209
+ # install trigger to call whenever a value is updated
1210
+ def update_command *args, &block
1211
+ $log.debug "Variable: update command set #{args}"
1212
+ @update_command << block
1213
+ @args << args
1214
+ end
1215
+ ##
1216
+ # value of the variable
1217
+ def get_value val=nil
1218
+ if @klass == 'String'
1219
+ return @value
1220
+ elsif @klass == 'Hash'
1221
+ return @value[val]
1222
+ elsif @klass == 'Array'
1223
+ return @value[val]
1224
+ else
1225
+ return @value
1226
+ end
1227
+ end
1228
+ ##
1229
+ # update the value of this variable.
1230
+ # 2008-12-31 18:35 Added source so one can identify multiple sources that are updating.
1231
+ # Idea is that mutiple fields (e.g. checkboxes) can share one var and update a hash through it.
1232
+ # Source would contain some code or key relatin to each field.
1233
+ def set_value val, key=""
1234
+ oldval = @value
1235
+ if @klass == 'String'
1236
+ @value = val
1237
+ elsif @klass == 'Hash'
1238
+ $log.debug " Variable setting hash #{key} to #{val}"
1239
+ oldval = @value[key]
1240
+ @value[key]=val
1241
+ elsif @klass == 'Array'
1242
+ $log.debug " Variable setting array #{key} to #{val}"
1243
+ oldval = @value[key]
1244
+ @value[key]=val
1245
+ else
1246
+ oldval = @value
1247
+ @value = val
1248
+ end
1249
+ return if @update_command.nil?
1250
+ @update_command.each_with_index do |comm, ix|
1251
+ comm.call(self, *@args[ix]) unless comm.nil?
1252
+ end
1253
+ @dependents.each {|d| d.fire_property_change(d, oldval, val) } unless @dependents.nil?
1254
+ end
1255
+ ##
1256
+ def value= (val)
1257
+ raise "Please use set_value for hash/array" if @klass=='Hash' or @klass=='Array'
1258
+ oldval = @value
1259
+ @value=val
1260
+ return if @update_command.nil?
1261
+ @update_command.each_with_index do |comm, ix|
1262
+ comm.call(self, *@args[ix]) unless comm.nil?
1263
+ end
1264
+ @dependents.each {|d| d.fire_property_change(d, oldval, val) } unless @dependents.nil?
1265
+ end
1266
+ def value
1267
+ raise "Please use set_value for hash/array: #{@klass}" if @klass=='Hash' #or @klass=='Array'
1268
+ @value
1269
+ end
1270
+ def inspect
1271
+ @value.inspect
1272
+ end
1273
+ def [](key)
1274
+ @value[key]
1275
+ end
1276
+ ##
1277
+ # in order to run some method we don't yet support
1278
+ def source
1279
+ @value
1280
+ end
1281
+ def to_s
1282
+ inspect
1283
+ end
1284
+ end
1285
+ ##
1286
+ # the preferred way of printing text on screen, esp if you want to modify it at run time.
1287
+ # Use display_length to ensure no spillage.
1288
+ class Label < Widget
1289
+ #dsl_accessor :label_for # related field or buddy
1290
+ dsl_accessor :mnemonic # keyboard focus is passed to buddy based on this key (ALT mask)
1291
+ # justify required a display length, esp if center.
1292
+ #dsl_accessor :justify # :right, :left, :center # added 2008-12-22 19:02
1293
+ dsl_property :justify # :right, :left, :center # added 2008-12-22 19:02
1294
+ dsl_property :display_length # please give this to ensure the we only print this much
1295
+ dsl_property :height # if you want a multiline label.
1296
+
1297
+ def initialize form, config={}, &block
1298
+
1299
+ @row = config.fetch("row",-1)
1300
+ @col = config.fetch("col",-1)
1301
+ @bgcolor = config.fetch("bgcolor", $def_bg_color)
1302
+ @color = config.fetch("color", $def_fg_color)
1303
+ @text = config.fetch("text", "NOTFOUND")
1304
+ @editable = false
1305
+ @focusable = false
1306
+ super
1307
+ @justify ||= :left
1308
+ @name ||= @text
1309
+ @repaint_required = true
1310
+ end
1311
+ def getvalue
1312
+ @text_variable && @text_variable.value || @text
1313
+ end
1314
+ def label_for field
1315
+ @label_for = field
1316
+ #$log.debug " label for: #{@label_for}"
1317
+ bind_hotkey unless @form.nil? # GRRR!
1318
+ end
1319
+
1320
+ ##
1321
+ # for a button, fire it when label invoked without changing focus
1322
+ # for other widgets, attempt to change focus to that field
1323
+ def bind_hotkey
1324
+ if !@mnemonic.nil?
1325
+ ch = @mnemonic.downcase()[0] ## FIXME 1.9
1326
+ # meta key
1327
+ mch = ?\M-a + (ch - ?a)
1328
+ if @label_for.is_a? RubyCurses::Button and @label_for.respond_to? :fire
1329
+ @form.bind_key(mch, @label_for) { |_form, _butt| _butt.fire }
1330
+ else
1331
+ $log.debug " bind_hotkey label for: #{@label_for}"
1332
+ @form.bind_key(mch, @label_for) { |_form, _field| _field.focus }
1333
+ end
1334
+ end
1335
+ end
1336
+
1337
+ ##
1338
+ # XXX need to move wrapping etc up and done once.
1339
+ def repaint
1340
+ return unless @repaint_required
1341
+ r,c = rowcol
1342
+ value = getvalue_for_paint
1343
+ lablist = []
1344
+ if @height && @height > 1
1345
+ lablist = wrap_text(value, @display_length).split("\n")
1346
+ else
1347
+ # ensure we do not exceed
1348
+ if !@display_length.nil?
1349
+ if value.length > @display_length
1350
+ value = value[0..@display_length-1]
1351
+ end
1352
+ end
1353
+ lablist << value
1354
+ end
1355
+ len = @display_length || value.length
1356
+ acolor = get_color $datacolor
1357
+ #$log.debug "label :#{@text}, #{value}, #{r}, #{c} col= #{@color}, #{@bgcolor} acolor #{acolor} j:#{@justify} dlL: #{@display_length} "
1358
+ firstrow = r
1359
+ _height = @height || 1
1360
+ str = @justify.to_sym == :right ? "%*s" : "%-*s" # added 2008-12-22 19:05
1361
+ # loop added for labels that are wrapped.
1362
+ # TODO clear separately since value can change in status like labels
1363
+ 0.upto(_height-1) { |i|
1364
+ @form.window.printstring r+i, c, " " * len , acolor,@attr
1365
+ }
1366
+ lablist.each_with_index do |_value, ix|
1367
+ break if ix >= _height
1368
+ if @justify.to_sym == :center
1369
+ padding = (@display_length - _value.length)/2
1370
+ _value = " "*padding + _value + " "*padding # so its cleared if we change it midway
1371
+ end
1372
+ @form.window.printstring r, c, str % [len, _value], acolor,@attr
1373
+ r += 1
1374
+ end
1375
+ if !@mnemonic.nil?
1376
+ ulindex = value.index(@mnemonic) || value.index(@mnemonic.swapcase)
1377
+ @form.window.mvchgat(y=firstrow, x=c+ulindex, max=1, Ncurses::A_BOLD|Ncurses::A_UNDERLINE, acolor, nil)
1378
+ end
1379
+ #@form.window.mvchgat(y=r, x=c, max=len, Ncurses::A_NORMAL, color, nil)
1380
+ @repaint_required = false
1381
+ end
1382
+ # ADD HERE LABEL
1383
+ end
1384
+ ##
1385
+ # action buttons
1386
+ # TODO: phasing out underline, and giving mnemonic and ampersand preference
1387
+ # - Action: may have to listen to Action property changes so enabled, name etc change can be reflected
1388
+ class Button < Widget
1389
+ dsl_accessor :surround_chars # characters to use to surround the button, def is square brackets
1390
+ dsl_accessor :mnemonic
1391
+ def initialize form, config={}, &block
1392
+ @focusable = true
1393
+ @editable = false
1394
+ #@command_block = nil
1395
+ @handler={} # event handler
1396
+ @event_args ||= {}
1397
+ super
1398
+ @bgcolor ||= $datacolor
1399
+ @color ||= $datacolor
1400
+ @surround_chars ||= ['[ ', ' ]']
1401
+ @col_offset = @surround_chars[0].length
1402
+ #@text = @name if @text.nil?
1403
+ #bind_hotkey # 2008-12-23 22:41 remarked
1404
+ end
1405
+ ##
1406
+ # set button based on Action
1407
+ # 2009-01-21 19:59
1408
+ def action a
1409
+ text a.name
1410
+ mnemonic a.mnemonic unless a.mnemonic.nil?
1411
+ command { a.call }
1412
+ end
1413
+ ##
1414
+ # sets text, checking for ampersand, uses that for hotkey and underlines
1415
+ def text(*val)
1416
+ if val.empty?
1417
+ return @text
1418
+ else
1419
+ s = val[0].dup
1420
+ s = s.to_s if !s.is_a? String # 2009-01-15 17:32
1421
+ if (( ix = s.index('&')) != nil)
1422
+ s.slice!(ix,1)
1423
+ @underline = ix unless @form.nil? # this setting a fake underline in messageboxes
1424
+ mnemonic s[ix,1]
1425
+ end
1426
+ @text = s
1427
+ end
1428
+ end
1429
+ ##
1430
+ # FIXME this will not work in messageboxes since no form available
1431
+ def mnemonic char
1432
+ $log.error " #{self} COULD NOT SET MNEMONIC since form NIL" if @form.nil?
1433
+ return if @form.nil?
1434
+ @mnemonic = char
1435
+ ch = char.downcase()[0] ## XXX 1.9
1436
+ # meta key
1437
+ mch = ?\M-a + (ch - ?a)
1438
+ $log.debug " #{self} setting MNEMO to #{char} #{mch}"
1439
+ @form.bind_key(mch, self) { |_form, _butt| _butt.fire }
1440
+ end
1441
+ ##
1442
+ # which index to use as underline.
1443
+ # Instead of using this to make a hotkey, I am thinking of giving this a new usage.
1444
+ # If you wish to override the underline?
1445
+ # @deprecated . use mnemonic or an ampersand in text.
1446
+ def OLDunderline ix
1447
+ _value = @text || getvalue # hack for Togglebutton FIXME
1448
+ raise "#{self}: underline requires text to be set " if _value.nil?
1449
+ mnemonic _value[ix]
1450
+ end
1451
+ # bind hotkey to form keys. added 2008-12-15 20:19
1452
+ # use ampersand in name or underline
1453
+ def bind_hotkey
1454
+ return if @underline.nil? or @form.nil?
1455
+ _value = @text || getvalue # hack for Togglebutton FIXME
1456
+ #_value = getvalue
1457
+ $log.debug " bind hot #{_value} #{@underline}"
1458
+ ch = _value[@underline,1].downcase()[0] ## XXX 1.9
1459
+ @mnemonic = _value[@underline,1]
1460
+ # meta key
1461
+ mch = ?\M-a + (ch - ?a)
1462
+ @form.bind_key(mch, self) { |_form, _butt| _butt.fire }
1463
+ end
1464
+ # 2009-01-17 01:48 removed so widgets can be called
1465
+ # def on_enter
1466
+ # $log.debug "ONENTER : #{@bgcolor} "
1467
+ # end
1468
+ # def on_leave
1469
+ # $log.debug "ONLEAVE : #{@bgcolor} "
1470
+ # end
1471
+ def getvalue
1472
+ @text_variable.nil? ? @text : @text_variable.get_value(@name)
1473
+ end
1474
+
1475
+ def getvalue_for_paint
1476
+ ret = getvalue
1477
+ @text_offset = @surround_chars[0].length
1478
+ @surround_chars[0] + ret + @surround_chars[1]
1479
+ end
1480
+ def repaint # button
1481
+ #$log.debug("BUTTon repaint : #{self} r:#{@row} c:#{@col} #{getvalue_for_paint}" )
1482
+ r,c = @row, @col #rowcol include offset for putting cursor
1483
+ @highlight_foreground ||= $reversecolor
1484
+ @highlight_background ||= 0
1485
+ bgcolor = @state==:HIGHLIGHTED ? @highlight_background : @bgcolor
1486
+ color = @state==:HIGHLIGHTED ? @highlight_foreground : @color
1487
+ if bgcolor.is_a? String and color.is_a? String
1488
+ color = ColorMap.get_color(color, bgcolor)
1489
+ end
1490
+ value = getvalue_for_paint
1491
+ #$log.debug("button repaint :#{self} r:#{r} c:#{c} col:#{color} bg #{bgcolor} v: #{value} ul #{@underline} mnem #{@mnemonic}")
1492
+ len = @display_length || value.length
1493
+ @form.window.printstring r, c, "%-*s" % [len, value], color, @attr
1494
+ # @form.window.mvchgat(y=r, x=c, max=len, Ncurses::A_NORMAL, bgcolor, nil)
1495
+ # in toggle buttons the underline can change as the text toggles
1496
+ if !@underline.nil? or !@mnemonic.nil?
1497
+ uline = @underline && (@underline + @text_offset) || value.index(@mnemonic) || value.index(@mnemonic.swapcase)
1498
+ @form.window.mvchgat(y=r, x=c+uline, max=1, Ncurses::A_BOLD|Ncurses::A_UNDERLINE, color, nil)
1499
+ end
1500
+ end
1501
+ ## command of button (invoked on press, hotkey, space)
1502
+ # added args 2008-12-20 19:22
1503
+ def command *args, &block
1504
+ bind :PRESS, *args, &block
1505
+ $log.debug "#{text} bound PRESS"
1506
+ end
1507
+ ## fires PRESS event of button
1508
+ def fire
1509
+ $log.debug "firing PRESS #{text}"
1510
+ fire_handler :PRESS, @form
1511
+ end
1512
+ # Button
1513
+ def handle_key ch
1514
+ case ch
1515
+ when KEY_LEFT, KEY_UP
1516
+ $log.debug " from 2009-01-16 18:18 buttons return UNHANDLED on UP DOWN LEFT RIGHT"
1517
+ return :UNHANDLED
1518
+ # @form.select_prev_field
1519
+ when KEY_RIGHT, KEY_DOWN
1520
+ $log.debug " from 2009-01-16 18:18 buttons return UNHANDLED on UP DOWN LEFT RIGHT"
1521
+ return :UNHANDLED
1522
+ # @form.select_next_field
1523
+ when KEY_ENTER, 10, 13, 32 # added space bar also
1524
+ if respond_to? :fire
1525
+ fire
1526
+ end
1527
+ else
1528
+ return :UNHANDLED
1529
+ end
1530
+ end
1531
+ # temporary method, shoud be a proper class
1532
+ def self.button_layout buttons, row, startcol=0, cols=Ncurses.COLS-1, gap=5
1533
+ col = startcol
1534
+ buttons.each_with_index do |b, ix|
1535
+ $log.debug " BUTTON #{b}: #{b.col} "
1536
+ b.row = row
1537
+ b.col col
1538
+ $log.debug " after BUTTON #{b}: #{b.col} "
1539
+ len = b.text.length + gap
1540
+ col += len
1541
+ end
1542
+ end
1543
+ end #BUTTON
1544
+
1545
+ ##
1546
+ # an event fired when an item that can be selected is toggled/selected
1547
+ class ItemEvent
1548
+ # http://java.sun.com/javase/6/docs/api/java/awt/event/ItemEvent.html
1549
+ attr_reader :state # :SELECTED :DESELECTED
1550
+ attr_reader :item # the item pressed such as toggle button
1551
+ attr_reader :item_selectable # item originating event such as list or collection
1552
+ attr_reader :item_first # if from a list
1553
+ attr_reader :item_last #
1554
+ attr_reader :param_string # for debugging etc
1555
+ =begin
1556
+ def initialize item, item_selectable, state, item_first=-1, item_last=-1, paramstring=nil
1557
+ @item, @item_selectable, @state, @item_first, @item_last =
1558
+ item, item_selectable, state, item_first, item_last
1559
+ @param_string = "Item event fired: #{item}, #{state}"
1560
+ end
1561
+ =end
1562
+ # i think only one is needed per object, so create once only
1563
+ def initialize item, item_selectable
1564
+ @item, @item_selectable =
1565
+ item, item_selectable
1566
+ end
1567
+ def set state, item_first=-1, item_last=-1, param_string=nil
1568
+ @state, @item_first, @item_last, @param_string =
1569
+ state, item_first, item_last, param_string
1570
+ @param_string = "Item event fired: #{item}, #{state}" if param_string.nil?
1571
+ end
1572
+ end
1573
+ ##
1574
+ # A button that may be switched off an on.
1575
+ # To be extended by RadioButton and checkbox.
1576
+ # TODO: add editable here nd prevent toggling if not so.
1577
+ class ToggleButton < Button
1578
+ dsl_accessor :onvalue, :offvalue
1579
+ dsl_accessor :value
1580
+ dsl_accessor :surround_chars
1581
+ dsl_accessor :variable # value linked to this variable which is a boolean
1582
+ dsl_accessor :display_length # 2009-01-06 00:10
1583
+
1584
+ # item_event
1585
+ def initialize form, config={}, &block
1586
+ super
1587
+ # no longer linked to text_variable, that was a misunderstanding
1588
+ @value ||= (@variable.nil? ? false : @variable.get_value(@name)==true)
1589
+ end
1590
+ def getvalue
1591
+ @value ? @onvalue : @offvalue
1592
+ end
1593
+ ##
1594
+ # is the button on or off
1595
+ # added 2008-12-09 19:05
1596
+ def checked?
1597
+ @value
1598
+ end
1599
+ alias :selected? :checked?
1600
+
1601
+ def getvalue_for_paint
1602
+ buttontext = getvalue()
1603
+ @text_offset = @surround_chars[0].length
1604
+ @surround_chars[0] + buttontext + @surround_chars[1]
1605
+ end
1606
+ def handle_key ch
1607
+ if ch == 32
1608
+ toggle
1609
+ else
1610
+ super
1611
+ end
1612
+ end
1613
+ ##
1614
+ # toggle the button value
1615
+ def toggle
1616
+ fire
1617
+ end
1618
+ def fire
1619
+ checked(!@value)
1620
+ # added ItemEvent on 2008-12-31 13:44
1621
+ @item_event = ItemEvent.new self, self if @item_event.nil?
1622
+ @item_event.set(@value ? :SELECTED : :DESELECTED)
1623
+ fire_handler :PRESS, @item_event # should the event itself be ITEM_EVENT
1624
+ # fire_handler :PRESS, @form
1625
+ # super
1626
+ end
1627
+ ##
1628
+ # set the value to true or false
1629
+ # user may programmatically want to check or uncheck
1630
+ def checked tf
1631
+ @value = tf
1632
+ if !@variable.nil?
1633
+ if @value
1634
+ @variable.set_value((@onvalue || 1), @name)
1635
+ else
1636
+ @variable.set_value((@offvalue || 0), @name)
1637
+ end
1638
+ end
1639
+ # call fire of button class 2008-12-09 17:49
1640
+ end
1641
+ end # class
1642
+ ##
1643
+ # A checkbox, may be selected or unselected
1644
+ # TODO hotkey should work here too.
1645
+ class CheckBox < ToggleButton
1646
+ dsl_accessor :align_right # the button will be on the right 2008-12-09 23:41
1647
+ # if a variable has been defined, off and on value will be set in it (default 0,1)
1648
+ def initialize form, config={}, &block
1649
+ @surround_chars = ['[', ']'] # 2008-12-23 23:16 added space in Button so overriding
1650
+ super
1651
+ end
1652
+ def getvalue
1653
+ @value
1654
+ end
1655
+
1656
+ def getvalue_for_paint
1657
+ buttontext = getvalue() ? "X" : " "
1658
+ dtext = @display_length.nil? ? @text : "%-*s" % [@display_length, @text]
1659
+ dtext = "" if @text.nil? # added 2009-01-13 00:41 since cbcellrenderer prints no text
1660
+ if @align_right
1661
+ @text_offset = 0
1662
+ @col_offset = dtext.length + @surround_chars[0].length + 1
1663
+ return "#{dtext} " + @surround_chars[0] + buttontext + @surround_chars[1]
1664
+ else
1665
+ pretext = @surround_chars[0] + buttontext + @surround_chars[1]
1666
+ @text_offset = pretext.length + 1
1667
+ @col_offset = @surround_chars[0].length
1668
+ #@surround_chars[0] + buttontext + @surround_chars[1] + " #{@text}"
1669
+ return pretext + " #{dtext}"
1670
+ end
1671
+ end
1672
+ end # class
1673
+ ##
1674
+ # A selectable button that has a text value. It is based on a Variable that
1675
+ # is shared by other radio buttons. Only one is selected at a time, unlike checkbox
1676
+ # 2008-11-27 18:45 just made this inherited from Checkbox
1677
+ class RadioButton < ToggleButton
1678
+ dsl_accessor :align_right # the button will be on the right 2008-12-09 23:41
1679
+ # if a variable has been defined, off and on value will be set in it (default 0,1)
1680
+ def initialize form, config={}, &block
1681
+ @surround_chars = ['(', ')'] if @surround_chars.nil?
1682
+ super
1683
+ end
1684
+ # all radio buttons will return the value of the selected value, not the offered value
1685
+ def getvalue
1686
+ #@text_variable.value
1687
+ @variable.get_value @name
1688
+ end
1689
+ def getvalue_for_paint
1690
+ buttontext = getvalue() == @value ? "o" : " "
1691
+ dtext = @display_length.nil? ? text : "%-*s" % [@display_length, text]
1692
+ if @align_right
1693
+ @text_offset = 0
1694
+ @col_offset = dtext.length + @surround_chars[0].length + 1
1695
+ return "#{dtext} " + @surround_chars[0] + buttontext + @surround_chars[1]
1696
+ else
1697
+ pretext = @surround_chars[0] + buttontext + @surround_chars[1]
1698
+ @text_offset = pretext.length + 1
1699
+ @col_offset = @surround_chars[0].length
1700
+ return pretext + " #{dtext}"
1701
+ end
1702
+ end
1703
+ def toggle
1704
+ @variable.set_value @value, @name
1705
+ # call fire of button class 2008-12-09 17:49
1706
+ fire
1707
+ end
1708
+ # added for bindkeys since that calls fire, not toggle - XXX i don't like this
1709
+ def fire
1710
+ @variable.set_value @value,@name
1711
+ super
1712
+ end
1713
+ ##
1714
+ # ideally this should not be used. But implemented for completeness.
1715
+ # it is recommended to toggle some other radio button than to uncheck this.
1716
+ def checked tf
1717
+ if tf
1718
+ toggle
1719
+ elsif !@variable.nil? and getvalue() != @value # XXX ???
1720
+ @variable.set_value "",""
1721
+ end
1722
+ end
1723
+ end # class radio
1724
+
1725
+ def self.startup
1726
+ VER::start_ncurses
1727
+ $log = Logger.new("view.log")
1728
+ $log.level = Logger::DEBUG
1729
+ end
1730
+
1731
+ end # module