ncumbra 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
data/lib/umbra/form.rb ADDED
@@ -0,0 +1,473 @@
1
+ require 'umbra/eventhandler' # for register_events and fire_handler etc
2
+ require 'umbra/keymappinghandler' # for bind_key and process_key
3
+ ##
4
+ # Manages the controls/widgets on a screen.
5
+ # Manages traversal, rendering and events of all widgets that are associated with it
6
+ # via the +add_widget+ method.
7
+ #
8
+ # Passes keys pressed by user to the current field.
9
+ # Any keys that are not handled by the current field, are handled by the form if the application
10
+ # has bound the key via +bind_key+.
11
+ # NOTE : 2018-03-08 - now using @focusables instead of @widgets in traversal.
12
+ # active_index is now index into focusables.
13
+ module Umbra
14
+ class Form
15
+ # array of widgets, and those that can be traversed
16
+ attr_reader :widgets, :focusables
17
+
18
+ # related window pointer used for printing or other FFI calls
19
+ attr_accessor :window
20
+
21
+ # cursor row and col # 2018-03-20 - this is bad as widgets update it. it should be picked up from focussed widget
22
+ # 2018-03-22 - removing access to it
23
+ #attr_accessor :row, :col
24
+
25
+ # index of active widget inside focusables array
26
+ attr_accessor :active_index
27
+
28
+ # name given to form for debugging
29
+ attr_accessor :name
30
+
31
+ include EventHandler
32
+ include KeyMappingHandler
33
+ def initialize win, &block
34
+ @window = win
35
+ @widgets = []
36
+ @active_index = nil
37
+ @row = @col = 0 # 2018-03-07 - umbra
38
+ @focusables = [] # focusable components
39
+ instance_eval &block if block_given?
40
+ @name ||= "" # for debugging
41
+
42
+ # for storing error message NOT_SURE
43
+ #$error_message ||= Variable.new ""
44
+
45
+ map_keys unless @keys_mapped
46
+ end
47
+ ##
48
+ # Add given widget to widget list and returns self
49
+ # A widget must be added to a Form for it to be painted and focussed.
50
+ # @param [Widget] widget to display on form
51
+ # @return [Form] pointer to self
52
+ def add_widget *widget
53
+ widget.each do |w|
54
+ next if @widgets.include? w
55
+ # NOTE: if form created with nil window (messagebox), then this would have to happen later
56
+ w.graphic = @window if @window # 2018-03-19 - prevent widget from needing to call form back
57
+ w._form = self # 2018-04-20 - so that update_focusables can be called.
58
+ @widgets << w
59
+ end
60
+ return self
61
+ end
62
+
63
+ # remove a widget from form.
64
+ # Will not be displayed or focussed.
65
+ # @param [Widget] widget to remove from form
66
+ def remove_widget widget
67
+ @widgets.delete widget
68
+ @focusables.delete widget
69
+ end
70
+ # maintain a list of focusable objects so form can traverse between them easily.
71
+ def update_focusables
72
+ $log.debug "1 inside update_focusables #{@focusables.count} "
73
+ @focusables = @widgets.select { |w| w.focusable }
74
+ $log.debug "2 inside update_focusables #{@focusables.count} "
75
+ end
76
+ # Decide layout of objects. User has to call this after creating components
77
+ # More may come here.
78
+ def pack
79
+
80
+ update_focusables
81
+
82
+ # set up hotkeys for buttons and labels with mnemonics and labels.
83
+ @widgets.each do |w|
84
+ #$log.debug " FOCUSABLES #{w.name} #{w.to_s} #{w.class}"
85
+ if w.respond_to? :mnemonic
86
+ if w.mnemonic
87
+ ch = w.mnemonic.downcase()[0].ord
88
+ # meta key
89
+ mch = ?\M-a.getbyte(0) + (ch - ?a.getbyte(0))
90
+
91
+ if w.respond_to? :fire
92
+ #$log.debug " setting hotkey #{mch} to button #{w} "
93
+ self.bind_key(mch, "hotkey for button #{w} ") { w.fire }
94
+ else
95
+ # case of labels and labeled field
96
+ #$log.debug " setting hotkey #{mch} to field #{w} "
97
+ self.bind_key(mch, "hotkey for field #{w.related_widget} ") {
98
+
99
+ #$log.debug " HOTKEY got key #{mch} : for #{w.related_widget} "
100
+ self.select_field w.related_widget }
101
+ end
102
+ end
103
+ end
104
+ end
105
+ @active_index = 0 if @focusables.size > 0
106
+ # 2018-04-14 - why the repaint here ? commenting off. Gave error in messagbox if no window yet.
107
+ #repaint
108
+ self
109
+ end
110
+
111
+
112
+ # form repaint,calls repaint on each widget which will repaint it only if it has been modified since last call.
113
+ # called after each keypress and on select_field.
114
+
115
+ def repaint
116
+ $log.debug " form repaint:#{self}, #{@name} , r #{@row} c #{@col} " if $log.debug?
117
+ @widgets.each do |f|
118
+ next if f.visible == false
119
+ #f.repaint
120
+ # changed on 2018-03-21 - so widgets don't need to do this.
121
+ if f.repaint_required
122
+ f.graphic = @window unless f.graphic # messageboxes may not have a window till very late
123
+ f.repaint
124
+ f.repaint_required = false
125
+ end
126
+ end
127
+
128
+ # get curpos of active widget 2018-03-21 - form is taking control of this now.
129
+ f = get_current_field
130
+ if f
131
+ @row, @col = f.rowcol
132
+ _setpos
133
+ end
134
+ @window.wrefresh
135
+ end
136
+ # @return [Widget, nil] current field, nil if no focusable field
137
+ def get_current_field
138
+ #select_next_field if @active_index == -1
139
+ return nil if @active_index.nil? # for forms that have no focusable field 2009-01-08 12:22
140
+ @focusables[@active_index]
141
+ end
142
+ alias :current_widget :get_current_field
143
+ # take focus to first focusable field
144
+ # we shoud not send to select_next. have a separate method to avoid bugs.
145
+ # but check current_field, in case called from anotehr field TODO FIXME
146
+ def select_first_field
147
+ select_field 0
148
+ end
149
+
150
+ # take focus to last field on form
151
+ # 2018-03-08 - WHY IS THIS REQUIRED NOT_SURE
152
+ def select_last_field
153
+ raise
154
+ return nil if @active_index.nil? # for forms that have no focusable field 2009-01-08 12:22
155
+ i = @focusables.length -1
156
+ select_field i
157
+ end
158
+
159
+
160
+
161
+ ##
162
+ # puts focus on the given field/widget index
163
+ # @param index of field in @widgets (or can be a Widget too)
164
+ # XXX if called externally will not run a on_leave of previous field
165
+ def select_field ix0
166
+ if ix0.is_a? Widget
167
+ ix0 = @focusables.index(ix0)
168
+ end
169
+ return if @focusables.nil? or @focusables.empty?
170
+ $log.debug "inside select_field : #{ix0} ai #{@active_index}"
171
+ f = @focusables[ix0]
172
+ return if !f.focusable
173
+ if f.focusable
174
+ @active_index = ix0
175
+ @row, @col = f.rowcol
176
+ on_enter f
177
+ # the wmove will be overwritten by repaint later, better to set row col
178
+ _setrowcol @row, @col # 2018-03-21 - maybe this should be set after the repaint
179
+
180
+ repaint # 2018-03-21 - handle_key calls repaint, is this for cases not involving keypress ?
181
+ @window.refresh
182
+ else
183
+ $log.debug "inside select field ENABLED FALSE : act #{@active_index} ix0 #{ix0}"
184
+ end
185
+ end
186
+ ##
187
+ # run validate_field on a field, usually whatevers current
188
+ # before transferring control
189
+ # We should try to automate this so developer does not have to remember to call it.
190
+ # # @param field object
191
+ # @return [0, -1] for success or failure
192
+ # NOTE : catches exception and sets $error_message, check if -1
193
+ def validate_field f=@focusables[@active_index]
194
+ begin
195
+ on_leave f
196
+ rescue => err
197
+ $log.error "form: validate_field caught EXCEPTION #{err}"
198
+ $log.error(err.backtrace.join("\n"))
199
+ # $error_message = "#{err}" # changed 2010
200
+ #$error_message.value = "#{err}" # 2018-03-18 - commented off since no Variable any longer
201
+ FFI::NCurses.beep
202
+ return -1
203
+ end
204
+ return 0
205
+ end
206
+ # put focus on next field
207
+ # will cycle by default, unless navigation policy not :CYCLICAL
208
+ # in which case returns :NO_NEXT_FIELD.
209
+ # FIXME: in the beginning it comes in as -1 and does an on_leave of last field
210
+ # 2018-03-07 - UMBRA: let us force user to run validation when he does next field
211
+ def select_next_field
212
+ return :UNHANDLED if @focusables.nil? || @focusables.empty?
213
+ #$log.debug "insdie sele nxt field : #{@active_index} WL:#{@widgets.length}"
214
+ if @active_index.nil? || @active_index == -1 # needs to be tested out A LOT
215
+ # what is this silly hack for still here 2014-04-24 - 13:04 DELETE FIXME
216
+ @active_index = -1
217
+ @active_index = 0 # 2018-03-08 - NOT_SURE
218
+ end
219
+ f = @focusables[@active_index]
220
+ # we need to call on_leave of this field or else state will never change back to normal TODO
221
+ on_leave f
222
+ #index = @focusables.index(f)
223
+ index = @active_index
224
+ index = index ? index+1 : 0
225
+ #f = @focusables[index]
226
+ index = 0 if index >= @focusables.length # CYCLICAL 2018-03-11 -
227
+ f = @focusables[index]
228
+ if f
229
+ select_field f
230
+ return 0
231
+ end
232
+ #
233
+ $log.debug "inside sele nxt field : NO NEXT #{@active_index} WL:#{@widgets.length}"
234
+ return :NO_NEXT_FIELD
235
+ end
236
+ ##
237
+ # put focus on previous field
238
+ # will cycle by default, unless navigation policy not :CYCLICAL
239
+ # in which case returns :NO_PREV_FIELD.
240
+ # @return [nil, :NO_PREV_FIELD] nil if cyclical and it finds a field
241
+ # if not cyclical, and no more fields then :NO_PREV_FIELD
242
+ def select_prev_field
243
+ return :UNHANDLED if @focusables.nil? or @focusables.empty?
244
+ #$log.debug "insdie sele prev field : #{@active_index} WL:#{@widgets.length}"
245
+ if @active_index.nil?
246
+ @active_index = @focusables.length
247
+ end
248
+
249
+ f = @focusables[@active_index]
250
+ on_leave f
251
+ index = @active_index
252
+ index -= 1
253
+ index = @focusables.length-1 if index < 0 # CYCLICAL 2018-03-11 -
254
+ f = @focusables[index]
255
+ if f
256
+ select_field f
257
+ return
258
+ end
259
+
260
+ return :NO_PREV_FIELD
261
+ end
262
+
263
+ private
264
+ # New attempt at setting cursor using absolute coordinates
265
+ # Also, trying NOT to go up. let this pad or window print cursor.
266
+ # 2018-03-21 - we should prevent other widgets from calling this. Tehy need to set their own offsets
267
+ # so form picks up the correct one.
268
+ # 2018-03-21 - renamed to _setrowcol so other programs calling it will bork.
269
+ def _setrowcol r, c
270
+ @row = r unless r.nil?
271
+ @col = c unless c.nil?
272
+ end
273
+ private
274
+ ##
275
+ # move cursor to where the fields row and col are
276
+ def _setpos r=@row, c=@col
277
+ #$log.debug "setpos : (#{self.name}) #{r} #{c} XXX"
278
+ ## adding just in case things are going out of bounds of a parent and no cursor to be shown
279
+ return if r.nil? or c.nil? # added 2009-12-29 23:28 BUFFERED
280
+ return if r<0 or c<0 # added 2010-01-02 18:49 stack too deep coming if goes above screen
281
+ @window.wmove r,c
282
+ end
283
+ ##
284
+ # form's trigger, fired when any widget loses focus
285
+ # NOTE: Do NOT override
286
+ # This wont get called in editor components in tables, since they are formless
287
+ def on_leave f
288
+ return if f.nil? || !f.focusable # added focusable, else label was firing
289
+ $log.debug "Form setting state of #{f.name} to NORMAL"
290
+ f.state = :NORMAL
291
+ # 2018-03-11 - trying out, there can be other things a widget may want to do on entry and exit
292
+ if f.highlight_color_pair || f.highlight_attr
293
+ f.repaint_required = true
294
+ end
295
+ f.on_leave if f.respond_to? :on_leave
296
+ end
297
+ # form calls on_enter of each object.
298
+ # However, if a multicomponent calls on_enter of a widget, this code will
299
+ # not be triggered. The highlighted part
300
+ # 2018-03-07 - NOT_SURE
301
+ def on_enter f
302
+ return if f.nil? || !f.focusable # added focusable, else label was firing 2010-09
303
+
304
+ f.state = :HIGHLIGHTED
305
+ # If the widget has a color defined for focussed, set repaint
306
+ # otherwise it will not be repainted unless user edits !
307
+ if f.highlight_color_pair || f.highlight_attr
308
+ f.repaint_required = true
309
+ end
310
+
311
+ f.modified = false
312
+ f.on_enter if f.respond_to? :on_enter
313
+ end
314
+
315
+ def _process_key keycode, object, window
316
+ return :UNHANDLED if @_key_map.nil?
317
+ blk = @_key_map[keycode]
318
+ $log.debug "XXX: _process key keycode #{keycode} #{blk.class}, #{self.class} "
319
+ return :UNHANDLED if blk.nil?
320
+
321
+ if blk.is_a? Symbol
322
+ if respond_to? blk
323
+ return send(blk, *@_key_args[keycode])
324
+ else
325
+ ## 2013-03-05 - 19:50 why the hell is there an alert here, nowhere else
326
+ alert "This ( #{self.class} ) does not respond to #{blk.to_s} [PROCESS-KEY]"
327
+ # added 2013-03-05 - 19:50 so called can know
328
+ return :UNHANDLED
329
+ end
330
+ else
331
+ $log.debug "rwidget BLOCK called _process_key " if $log.debug?
332
+ return blk.call object, *@_key_args[keycode]
333
+ end
334
+ end # }}}
335
+
336
+ public
337
+ # e.g. process_key ch, self {{{
338
+ # returns UNHANDLED if no block for it
339
+ # after form handles basic keys, it gives unhandled key to current field, if current field returns
340
+ # unhandled, then it checks this map.
341
+ # Please update widget with any changes here. TODO: match regexes as in mapper
342
+
343
+ def process_key keycode, object
344
+ return _process_key keycode, object, @window
345
+ end # }}}
346
+
347
+ #
348
+ # NOTE: These mappings will only trigger if the current field
349
+ # does not use them in handle_key
350
+ #
351
+ def map_keys
352
+ return if @keys_mapped
353
+ #bind_key(FFI::NCurses::KEY_F1, 'help') { hm = help_manager(); hm.display_help }
354
+ #bind_key(FFI::NCurses::KEY_F9, "Print keys", :print_key_bindings) # show bindings, tentative on F9
355
+ @keys_mapped = true
356
+ end
357
+
358
+ =begin
359
+ # repaint all # {{{
360
+ # this forces a repaint of all visible widgets and has been added for the case of overlapping
361
+ # windows, since a black rectangle is often left when a window is destroyed. This is internally
362
+ # triggered whenever a window is destroyed, and currently only for root window.
363
+ # NOTE: often the window itself or spaces between widgets also gets cleared, so basically
364
+ # the window itself may need recreating ? 2014-08-18 - 21:03
365
+ def repaint_all_widgets
366
+ $log.debug " REPAINT ALL in FORM called "
367
+ raise "it has come to repaint_all"
368
+ @widgets.each do |w|
369
+ next if w.visible == false
370
+ #next if w.class.to_s == "Canis::MenuBar"
371
+ $log.debug " ---- REPAINT ALL #{w.name} "
372
+ #w.repaint_required true
373
+ w.repaint_all true
374
+ w.repaint
375
+ end
376
+ $log.debug " REPAINT ALL in FORM complete "
377
+ # place cursor on current_widget
378
+ _setpos
379
+ end # }}}
380
+ =end
381
+ ## forms handle keys {{{
382
+ # mainly traps tab and backtab to navigate between widgets.
383
+ # I know some widgets will want to use tab, e.g edit boxes for entering a tab
384
+ # or for completion.
385
+ # @throws FieldValidationException
386
+ # NOTE : please rescue exceptions when you use this in your main loop and alert() user
387
+ #
388
+ def handle_key(ch)
389
+ handled = :UNHANDLED
390
+
391
+ case ch
392
+ when -1
393
+ #repaint # only for continuous updates, and will need to use wtimeout and not nodelay in getch
394
+ return
395
+ =begin
396
+ when 1000, 12
397
+ # NOTE this works if widgets cover entire screen like text areas and lists but not in
398
+ # dialogs where there is blank space. only widgets are painted.
399
+ # testing out 12 is C-l
400
+ $log.debug " form REFRESH_ALL repaint_all HK #{ch} #{self}, #{@name} "
401
+ repaint_all_widgets
402
+ return
403
+ when FFI::NCurses::KEY_RESIZE # SIGWINCH # UNTESTED XXX
404
+ # note that in windows that have dialogs or text painted on window such as title or
405
+ # box, the clear call will clear it out. these are not redrawn.
406
+ lines = FFI::NCurses.LINES
407
+ cols = FFI::NCurses.COLS
408
+ x = FFI::NCurses.stdscr.getmaxy
409
+ y = FFI::NCurses.stdscr.getmaxx
410
+ $log.debug " form RESIZE HK #{ch} #{self}, #{@name}, #{ch}, x #{x} y #{y} lines #{lines} , cols: #{cols} "
411
+ #alert "SIGWINCH WE NEED TO RECALC AND REPAINT resize #{lines}, #{cols}: #{x}, #{y} "
412
+
413
+ # next line may be causing flicker, can we do without.
414
+ FFI::NCurses.endwin
415
+ @window.wrefresh
416
+ @window.wclear
417
+ if @layout_manager
418
+ @layout_manager.do_layout
419
+ # we need to redo statusline and others that layout ignores
420
+ else
421
+ @widgets.each { |e| e.repaint_all(true) } # trying out
422
+ end
423
+ ## added RESIZE on 2012-01-5
424
+ ## stuff that relies on last line such as statusline dock etc will need to be redrawn.
425
+ fire_handler :RESIZE, self
426
+ =end
427
+ else
428
+ field = get_current_field
429
+ handled = :UNHANDLED
430
+ handled = field.handle_key ch unless field.nil? # no field focussable
431
+ $log.debug "handled inside Form #{ch} from #{field} got #{handled} "
432
+ # some widgets like textarea and list handle up and down
433
+ if handled == :UNHANDLED or handled == -1 or field.nil?
434
+ case ch
435
+ when FFI::NCurses::KEY_TAB, ?\M-\C-i.getbyte(0) # tab and M-tab in case widget eats tab (such as Table)
436
+ ret = select_next_field
437
+ return ret if ret == :NO_NEXT_FIELD
438
+ # alt-shift-tab or backtab (in case Table eats backtab)
439
+ when FFI::NCurses::KEY_BTAB, 481 ## backtab added 2008-12-14 18:41
440
+ ret = select_prev_field
441
+ return ret if ret == :NO_PREV_FIELD
442
+ when FFI::NCurses::KEY_UP
443
+ ret = select_prev_field
444
+ return ret if ret == :NO_PREV_FIELD
445
+ when FFI::NCurses::KEY_DOWN
446
+ ret = select_next_field
447
+ return ret if ret == :NO_NEXT_FIELD
448
+ else
449
+ #$log.debug " before calling process_key in form #{ch} " if $log.debug?
450
+ ret = process_key ch, self
451
+ # seems we need to flushinp in case composite has pushed key
452
+ $log.debug "FORM process_key #{ch} got ret #{ret} in #{self}, flushing input "
453
+ # 2014-06-01 - 17:01 added flush, maybe at some point we could do it only if unhandled
454
+ # in case some method wishes to actually push some keys
455
+ FFI::NCurses.flushinp
456
+ return :UNHANDLED if ret == :UNHANDLED
457
+ end
458
+ elsif handled == :NO_NEXT_FIELD || handled == :NO_PREV_FIELD # 2011-10-4
459
+ return handled
460
+ end
461
+ end
462
+ $log.debug " form before repaint #{self} , #{@name}, ret #{ret}"
463
+ repaint
464
+ ret || 0 # 2011-10-17
465
+ end # }}}
466
+
467
+ # 2010-02-07 14:50 to aid in debugging and comparing log files.
468
+ def to_s; @name || self; end
469
+
470
+ ## ADD HERE FORM
471
+ end
472
+
473
+ end # module
@@ -0,0 +1,96 @@
1
+ # ----------------------------------------------------------------------------- #
2
+ # File: keymappinghandler.rb
3
+ # Description: methods for mapping methods or blocks to keys
4
+ # Author: j kepler http://github.com/mare-imbrium/canis/
5
+ # Date: 2018-04-05 - 08:34
6
+ # License: MIT
7
+ # Last update: 2018-04-17 08:59
8
+ # ----------------------------------------------------------------------------- #
9
+ # keymappinghandler.rb Copyright (C) 2018 j kepler
10
+
11
+ module Umbra
12
+ module KeyMappingHandler
13
+
14
+ # bind a method to a key.
15
+ # @examples
16
+ # -- call cursor_home on pressing C-a. The symbol will also act as documentation for the key
17
+ # bind_key ?C-a, :cursor_home
18
+ # -- call collapse_parent on pressing x. The string will be the documentation for the key
19
+ # bind_key(?x, 'collapse parent'){ collapse_parent() }
20
+ #
21
+ def bind_key keycode, *args, &blk
22
+ #$log.debug " #{@name} bind_key received #{keycode} "
23
+ @_key_map ||= {}
24
+ #
25
+ # added on 2011-12-4 so we can pass a description for a key and print it
26
+ # The first argument may be a string, it will not be removed
27
+ # so existing programs will remain as is.
28
+ @key_label ||= {}
29
+ if args[0].is_a?(String) || args[0].is_a?(Symbol)
30
+ @key_label[keycode] = args[0]
31
+ else
32
+ @key_label[keycode] = :unknown
33
+ end
34
+
35
+ if !block_given?
36
+ blk = args.pop
37
+ raise "If block not passed, last arg should be a method symbol" if !blk.is_a? Symbol
38
+ #$log.debug " #{@name} bind_key received a symbol #{blk} "
39
+ end
40
+ case keycode
41
+ when String
42
+ # single assignment
43
+ keycode = keycode.getbyte(0)
44
+ when Array
45
+ # 2018-03-10 - unused now delete
46
+ raise "unused"
47
+ else
48
+ #$log.debug " assigning #{keycode} to _key_map for #{self.class}, #{@name}" if $log.debug?
49
+ end
50
+ @_key_map[keycode] = blk
51
+ @_key_args ||= {}
52
+ @_key_args[keycode] = args
53
+ self
54
+ end
55
+
56
+ def bind_keys keycodes, *args, &blk
57
+ keycodes.each { |k| bind_key k, *args, &blk }
58
+ end
59
+ ##
60
+ # remove a binding that you don't want
61
+ def unbind_key keycode
62
+ @_key_args.delete keycode unless @_key_args.nil?
63
+ @_key_map.delete keycode unless @_key_map.nil?
64
+ end
65
+
66
+ # e.g. process_key ch, self
67
+ # returns UNHANDLED if no block for it
68
+ # after form handles basic keys, it gives unhandled key to current field, if current field returns
69
+ # unhandled, then it checks this map.
70
+ def process_key keycode, object
71
+ return _process_key keycode, object, @graphic
72
+ end
73
+
74
+ def _process_key keycode, object, window
75
+ return :UNHANDLED if @_key_map.nil?
76
+ blk = @_key_map[keycode]
77
+ $log.debug "XXX: _process key keycode #{keycode} #{blk.class}, #{self.class} "
78
+ return :UNHANDLED if blk.nil?
79
+
80
+ if blk.is_a? Symbol
81
+ if respond_to? blk
82
+ return send(blk, *@_key_args[keycode])
83
+ else
84
+ ## 2013-03-05 - 19:50 why the hell is there an alert here, nowhere else
85
+ $log.error "This ( #{self.class} ) does not respond to #{blk.to_s} [PROCESS-KEY]"
86
+ # added 2013-03-05 - 19:50 so called can know
87
+ return :UNHANDLED
88
+ end
89
+ else
90
+ $log.debug "rwidget BLOCK called _process_key " if $log.debug?
91
+ return blk.call object, *@_key_args[keycode]
92
+ end
93
+ end
94
+
95
+ end # module KeyMappingHandler
96
+ end # module
@@ -0,0 +1,95 @@
1
+ # ----------------------------------------------------------------------------- #
2
+ # File: label.rb
3
+ # Description: an ncurses label
4
+ # The preferred way of printing text on screen, esp if you want to modify it at run time.
5
+ # Author: j kepler http://github.com/mare-imbrium/canis/
6
+ # Date: 2018-03-08 - 14:04
7
+ # License: MIT
8
+ # Last update: 2018-04-21 23:55
9
+ # ----------------------------------------------------------------------------- #
10
+ # label.rb Copyright (C) 2018- j kepler
11
+ #
12
+ require 'umbra/widget'
13
+ module Umbra
14
+ # a text label.
15
+ # when creating use +text=+ to set text. Optionally use +justify+ and +width+.
16
+ class Label < Widget
17
+
18
+ # justify required a display length, esp if center.
19
+ attr_accessor :justify #:right, :left, :center
20
+ attr_accessor :mnemonic # alt-key that passes focus to related field
21
+ attr_accessor :related_widget # field related to this label. See +mnemonic+.
22
+
23
+ def initialize config={}, &block
24
+
25
+ @text = config.fetch(:text, "NOTFOUND")
26
+ @editable = false
27
+ @focusable = false
28
+ # we have some processing for when a form is attached, registering a hotkey
29
+ #register_events :FORM_ATTACHED
30
+ super
31
+ @justify ||= :left
32
+ @name ||= @text
33
+ @width ||= @text.length # 2018-04-14 - added for messageboxes
34
+ @repaint_required = true
35
+ end
36
+ #
37
+ # get the value for the label
38
+ def getvalue
39
+ @text
40
+ end
41
+
42
+
43
+ ##
44
+ # NOTE: width can be nil, i have not set a default, containers asking width can crash. WHY NOT ?
45
+ def repaint
46
+ return unless @repaint_required
47
+ raise "Label row or col is nil #{@row} , #{@col}, #{@text} " if @row.nil? || @col.nil?
48
+ r,c = rowcol
49
+ $log.debug "label repaint #{r} #{c} #{@text} "
50
+
51
+ # value often nil so putting blank, but usually some application error
52
+ value = getvalue_for_paint || ""
53
+
54
+ if value.is_a? Array
55
+ value = value.join " "
56
+ end
57
+ # ensure we do not exceed
58
+ if @width
59
+ if value.length > @width
60
+ value = value[0..@width-1]
61
+ end
62
+ end
63
+ len = @width || value.length
64
+ acolor = @color_pair || 0
65
+ str = @justify.to_sym == :right ? "%*s" : "%-*s" # added 2008-12-22 19:05
66
+
67
+ #@graphic ||= @form.window
68
+ # clear the area
69
+ @graphic.printstring r, c, " " * len , acolor, @attr
70
+ if @justify.to_sym == :center
71
+ padding = (@width - value.length)/2
72
+ value = " "*padding + value + " "*padding # so its cleared if we change it midway
73
+ end
74
+ @graphic.printstring r, c, str % [len, value], acolor, @attr
75
+ if @mnemonic
76
+ ulindex = value.index(@mnemonic) || value.index(@mnemonic.swapcase)
77
+ @graphic.mvchgat(y=r, x=c+ulindex, max=1, BOLD|UNDERLINE, acolor, nil)
78
+ end
79
+ @repaint_required = false
80
+ end
81
+ # Added 2011-10-22 to prevent some naive components from putting focus here.
82
+ def on_enter
83
+ raise "Cannot enter Label"
84
+ end
85
+ def on_leave
86
+ raise "Cannot leave Label"
87
+ end
88
+ # overriding so that label is redrawn, since this is the main property that is used.
89
+ def text=(_text)
90
+ @text = _text
91
+ self.touch
92
+ end
93
+ # ADD HERE LABEL
94
+ end # }}}
95
+ end # module