ncumbra 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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