rkumar-rbcurse 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.
Files changed (52) hide show
  1. data/CHANGELOG +1577 -0
  2. data/README.txt +310 -0
  3. data/examples/qdfilechooser.rb +68 -0
  4. data/examples/rfe.rb +853 -0
  5. data/examples/rfe_renderer.rb +69 -0
  6. data/examples/test1.rb +242 -0
  7. data/examples/test2.rb +498 -0
  8. data/examples/testcombo.rb +95 -0
  9. data/examples/testkeypress.rb +61 -0
  10. data/examples/testmenu.rb +105 -0
  11. data/examples/testtable.rb +266 -0
  12. data/examples/testtabp.rb +106 -0
  13. data/examples/testtodo.rb +532 -0
  14. data/examples/viewtodo.rb +512 -0
  15. data/lib/rbcurse.rb +7 -0
  16. data/lib/rbcurse/action.rb +31 -0
  17. data/lib/rbcurse/applicationheader.rb +57 -0
  18. data/lib/rbcurse/celleditor.rb +120 -0
  19. data/lib/rbcurse/checkboxcellrenderer.rb +69 -0
  20. data/lib/rbcurse/colormap.rb +133 -0
  21. data/lib/rbcurse/comboboxcellrenderer.rb +45 -0
  22. data/lib/rbcurse/defaultlistselectionmodel.rb +49 -0
  23. data/lib/rbcurse/keylabelprinter.rb +143 -0
  24. data/lib/rbcurse/listcellrenderer.rb +99 -0
  25. data/lib/rbcurse/listkeys.rb +33 -0
  26. data/lib/rbcurse/listscrollable.rb +216 -0
  27. data/lib/rbcurse/listselectable.rb +67 -0
  28. data/lib/rbcurse/mapper.rb +108 -0
  29. data/lib/rbcurse/orderedhash.rb +77 -0
  30. data/lib/rbcurse/rcombo.rb +243 -0
  31. data/lib/rbcurse/rdialogs.rb +183 -0
  32. data/lib/rbcurse/rform.rb +845 -0
  33. data/lib/rbcurse/rinputdataevent.rb +36 -0
  34. data/lib/rbcurse/rlistbox.rb +804 -0
  35. data/lib/rbcurse/rmenu.rb +666 -0
  36. data/lib/rbcurse/rmessagebox.rb +325 -0
  37. data/lib/rbcurse/rpopupmenu.rb +754 -0
  38. data/lib/rbcurse/rtabbedpane.rb +259 -0
  39. data/lib/rbcurse/rtable.rb +1296 -0
  40. data/lib/rbcurse/rtextarea.rb +673 -0
  41. data/lib/rbcurse/rtextview.rb +335 -0
  42. data/lib/rbcurse/rwidget.rb +1731 -0
  43. data/lib/rbcurse/scrollable.rb +301 -0
  44. data/lib/rbcurse/selectable.rb +94 -0
  45. data/lib/rbcurse/table/tablecellrenderer.rb +85 -0
  46. data/lib/rbcurse/table/tabledatecellrenderer.rb +102 -0
  47. data/lib/ver/keyboard.rb +150 -0
  48. data/lib/ver/keyboard2.rb +170 -0
  49. data/lib/ver/ncurses.rb +102 -0
  50. data/lib/ver/window.rb +369 -0
  51. data/test/test_rbcurse.rb +0 -0
  52. metadata +117 -0
@@ -0,0 +1,335 @@
1
+ =begin
2
+ * Name: TextView
3
+ * $Id$
4
+ * Description View text in this widget.
5
+ * Author: rkumar (arunachalesha)
6
+ TODO
7
+ * file created 2009-01-08 15:23
8
+ --------
9
+ * License:
10
+ Same as Ruby's License (http://www.ruby-lang.org/LICENSE.txt)
11
+
12
+ =end
13
+ require 'rubygems'
14
+ require 'ncurses'
15
+ require 'logger'
16
+ require 'rbcurse'
17
+ require 'rbcurse/listscrollable'
18
+
19
+ include Ncurses
20
+ include RubyCurses
21
+ module RubyCurses
22
+ extend self
23
+
24
+ ##
25
+ # A viewable read only box. Can scroll.
26
+ # Intention is to be able to change content dynamically - the entire list.
27
+ # Use set_content to set content, or just update the list attrib
28
+ # TODO -
29
+ # - searching, goto line - DONE
30
+ class TextView < Widget
31
+ include ListScrollable
32
+ dsl_accessor :height # height of viewport
33
+ dsl_accessor :title # set this on top
34
+ dsl_accessor :title_attrib # bold, reverse, normal
35
+ dsl_accessor :footer_attrib # bold, reverse, normal
36
+ dsl_accessor :list # the array of data to be sent by user
37
+ dsl_accessor :maxlen # max len to be displayed
38
+ attr_reader :toprow # the toprow in the view (offsets are 0)
39
+ # attr_reader :prow # the row on which cursor/focus is
40
+ attr_reader :winrow # the row in the viewport/window
41
+ dsl_accessor :print_footer
42
+
43
+ def initialize form, config={}, &block
44
+ @focusable = true
45
+ @editable = false
46
+ @left_margin = 1
47
+ @row = 0
48
+ @col = 0
49
+ @show_focus = false # don't highlight row under focus
50
+ @list = []
51
+ super
52
+ @row_offset = @col_offset = 1
53
+ @orig_col = @col
54
+ # this does result in a blank line if we insert after creating. That's required at
55
+ # present if we wish to only insert
56
+ @scrollatrow = @height-2
57
+ @content_rows = @list.length
58
+ @win = @form.window
59
+ #init_scrollable
60
+ print_borders
61
+ @maxlen ||= @width-2
62
+ init_vars
63
+ end
64
+ def init_vars
65
+ @curpos = @pcol = @toprow = @current_index = 0
66
+ end
67
+ ##
68
+ # send in a list
69
+ # e.g. set_content File.open("README.txt","r").readlines
70
+ # set wrap at time of passing :WRAP_NONE :WRAP_WORD
71
+ def set_content list, wrap = :WRAP_NONE
72
+ @wrap_policy = wrap
73
+ if list.is_a? String
74
+ if @wrap_policy == :WRAP_WORD
75
+ data = wrap_text list
76
+ @list = data.split("\n")
77
+ else
78
+ @list = list.split("\n")
79
+ end
80
+ elsif list.is_a? Array
81
+ if @wrap_policy == :WRAP_WORD
82
+ data = wrap_text list.join(" ")
83
+ @list = data.split("\n")
84
+ else
85
+ @list = list
86
+ end
87
+ else
88
+ raise "set_content expects Array not #{list.class}"
89
+ end
90
+ end
91
+ ## display this row on top
92
+ def top_row(*val)
93
+ if val.empty?
94
+ @toprow
95
+ else
96
+ @toprow = val[0] || 0
97
+ #@prow = val[0] || 0
98
+ end
99
+ @repaint_required = true
100
+ end
101
+ ## ---- for listscrollable ---- ##
102
+ def scrollatrow
103
+ @height - 2
104
+ end
105
+ def row_count
106
+ @list.length
107
+ end
108
+ ##
109
+ # returns row of first match of given regex (or nil if not found)
110
+ def find_first_match regex
111
+ @list.each_with_index do |row, ix|
112
+ return ix if !row.match(regex).nil?
113
+ end
114
+ return nil
115
+ end
116
+ def rowcol
117
+ #$log.debug "textarea rowcol : #{@row+@row_offset+@winrow}, #{@col+@col_offset}"
118
+ #return @row+@row_offset+@winrow, @col+@col_offset
119
+ #return @row+@row_offset+@winrow, @col+@col_offset
120
+ return @row+@row_offset, @col+@col_offset
121
+ end
122
+ def wrap_text(txt, col = @maxlen)
123
+ $log.debug "inside wrap text for :#{txt}"
124
+ txt.gsub(/(.{1,#{col}})( +|$\n?)|(.{1,#{col}})/,
125
+ "\\1\\3\n")
126
+ end
127
+ def print_borders
128
+ window = @form.window
129
+ color = $datacolor
130
+ window.print_border @row, @col, @height, @width, color
131
+ print_title
132
+ =begin
133
+ hline = "+%s+" % [ "-"*(width-((1)*2)) ]
134
+ hline2 = "|%s|" % [ " "*(width-((1)*2)) ]
135
+ window.printstring(row=startrow, col=startcol, hline, color)
136
+ print_title
137
+ (startrow+1).upto(startrow+height-1) do |row|
138
+ window.printstring( row, col=startcol, hline2, color)
139
+ end
140
+ window.printstring( startrow+height, col=startcol, hline, color)
141
+ =end
142
+
143
+ end
144
+ def print_title
145
+ @form.window.printstring( @row, @col+(@width-@title.length)/2, @title, $datacolor, @title_attrib) unless @title.nil?
146
+ end
147
+ def print_foot
148
+ @footer_attrib ||= Ncurses::A_REVERSE
149
+ footer = "R: #{@current_index+1}, C: #{@curpos+@pcol}, #{@list.length} lines "
150
+ @form.window.printstring( @row + @height, @col+2, footer, $datacolor, @footer_attrib)
151
+ end
152
+ ### FOR scrollable ###
153
+ def get_content
154
+ @list
155
+ end
156
+ def get_window
157
+ @form.window
158
+ end
159
+ ### FOR scrollable ###
160
+ def repaint # textview
161
+ return unless @repaint_required
162
+ paint
163
+ print_foot if @print_footer
164
+ end
165
+ def getvalue
166
+ @list
167
+ end
168
+ # textview
169
+ # [ ] scroll left right DONE
170
+ def handle_key ch
171
+ @buffer = @list[@current_index]
172
+ if @buffer.nil? and row_count == 0
173
+ @list << "\r"
174
+ @buffer = @list[@current_index]
175
+ end
176
+ return if @buffer.nil?
177
+ $log.debug " before: curpos #{@curpos} blen: #{@buffer.length}"
178
+ if @curpos > @buffer.length
179
+ addcol((@buffer.length-@curpos)+1)
180
+ @curpos = @buffer.length
181
+ set_form_col
182
+ end
183
+ $log.debug "TV after loop : curpos #{@curpos} blen: #{@buffer.length}"
184
+ #pre_key
185
+ case ch
186
+ when ?\C-n
187
+ scroll_forward
188
+ when ?\C-p
189
+ scroll_backward
190
+ when ?0, ?\C-[
191
+ goto_start #start of buffer # cursor_start
192
+ when ?\C-]
193
+ goto_end # end / bottom cursor_end
194
+ when KEY_UP
195
+ #select_prev_row
196
+ ret = up
197
+ check_curpos
198
+ #addrowcol -1,0 if ret != -1 or @winrow != @oldwinrow # positions the cursor up
199
+ #@form.row = @row + 1 + @winrow
200
+ when KEY_DOWN
201
+ ret = down
202
+ check_curpos
203
+ #@form.row = @row + 1 + @winrow
204
+ when KEY_LEFT
205
+ cursor_backward
206
+ when KEY_RIGHT
207
+ cursor_forward
208
+ when KEY_BACKSPACE, 127
209
+ cursor_backward
210
+ when 330
211
+ cursor_backward
212
+ when ?\C-a
213
+ # take care of data that exceeds maxlen by scrolling and placing cursor at start
214
+ set_form_col 0
215
+ @pcol = 0
216
+ when ?\C-e
217
+ # take care of data that exceeds maxlen by scrolling and placing cursor at end
218
+ blen = @buffer.rstrip.length
219
+ if blen < @maxlen
220
+ set_form_col blen
221
+ else
222
+ @pcol = blen-@maxlen
223
+ #wrong curpos wiill be reported
224
+ set_form_col @maxlen-1
225
+ end
226
+ else
227
+ $log.debug("TEXTVIEW XXX ch #{ch}")
228
+ return :UNHANDLED
229
+ end
230
+ #post_key
231
+ # XXX 2008-11-27 13:57 trying out
232
+ set_form_row
233
+ end
234
+ # newly added to check curpos when moving up or down
235
+ def check_curpos
236
+ @buffer = @list[@current_index]
237
+ # if the cursor is ahead of data in this row then move it back
238
+ if @pcol+@curpos > @buffer.length
239
+ addcol((@pcol+@buffer.length-@curpos)+1)
240
+ @curpos = @buffer.length
241
+
242
+ # even this row is gt maxlen, i.e., scrolled right
243
+ if @curpos > @maxlen
244
+ @pcol = @curpos - @maxlen
245
+ @curpos = @maxlen-1
246
+ else
247
+ # this row is within maxlen, make scroll 0
248
+ @pcol=0
249
+ end
250
+ set_form_col
251
+ end
252
+ end
253
+ # set cursor on correct column tview
254
+ def set_form_col col=@curpos
255
+ @curpos = col
256
+ @curpos = @maxlen if @curpos > @maxlen
257
+ @form.col = @orig_col + @col_offset + @curpos
258
+ @repaint_required = true
259
+ end
260
+ def cursor_forward
261
+ if @curpos < @width and @curpos < @maxlen-1 # else it will do out of box
262
+ @curpos += 1
263
+ addcol 1
264
+ else
265
+ # XXX 2008-11-26 23:03 trying out
266
+ @pcol += 1 if @pcol <= @buffer.length
267
+ end
268
+ set_form_col
269
+ @repaint_required = true
270
+ end
271
+ def addcol num
272
+ @repaint_required = true
273
+ @form.addcol num
274
+ end
275
+ def addrowcol row,col
276
+ @repaint_required = true
277
+ @form.addrowcol row, col
278
+ end
279
+ def cursor_backward
280
+ if @curpos > 0
281
+ @curpos -= 1
282
+ set_form_col
283
+ #addcol -1
284
+ elsif @pcol > 0 # XXX added 2008-11-26 23:05
285
+ @pcol -= 1
286
+ end
287
+ @repaint_required = true
288
+ end
289
+ # gives offset of next line, does not move
290
+ def next_line
291
+ @list[@current_index+1]
292
+ end
293
+ def do_relative_row num
294
+ yield @list[@current_index+num]
295
+ end
296
+ def paint
297
+ print_borders if @to_print_borders == 1 # do this once only, unless everything changes
298
+ rc = row_count
299
+ maxlen = @maxlen ||= @width-2
300
+ tm = get_content
301
+ tr = @toprow
302
+ acolor = get_color $datacolor
303
+ h = scrollatrow()
304
+ r,c = rowcol
305
+ 0.upto(h) do |hh|
306
+ crow = tr+hh
307
+ if crow < rc
308
+ #focussed = @current_index == crow ? true : false
309
+ #selected = is_row_selected crow
310
+ content = tm[crow].chomp
311
+ content.gsub!(/\t/, ' ') # don't display tab
312
+ content.gsub!(/[^[:print:]]/, '') # don't display non print characters
313
+ if !content.nil?
314
+ if content.length > maxlen # only show maxlen
315
+ content = content[@pcol..@pcol+maxlen-1]
316
+ else
317
+ content = content[@pcol..-1]
318
+ end
319
+ end
320
+ #renderer = get_default_cell_renderer_for_class content.class.to_s
321
+ #renderer = cell_renderer()
322
+ #renderer.repaint @form.window, r+hh, c+(colix*11), content, focussed, selected
323
+ #renderer.repaint @form.window, r+hh, c, content, focussed, selected
324
+ @form.window.printstring r+hh, c, "%-*s" % [@width-2,content], acolor, @attr
325
+
326
+ else
327
+ # clear rows
328
+ @form.window.printstring r+hh, c, " " * (@width-2), acolor,@attr
329
+ end
330
+ end
331
+ @table_changed = false
332
+ @repaint_required = false
333
+ end
334
+ end # class textview
335
+ end # modul
@@ -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