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.
- checksums.yaml +7 -0
- data/.gitignore +25 -0
- data/Gemfile +6 -0
- data/LICENSE +21 -0
- data/README.md +48 -0
- data/README.md.bak +15 -0
- data/Rakefile +2 -0
- data/bin/console +14 -0
- data/bin/setup +8 -0
- data/examples/ex1.rb +85 -0
- data/examples/ex2.rb +128 -0
- data/examples/ex21.rb +136 -0
- data/examples/ex3.rb +163 -0
- data/examples/ex4.rb +142 -0
- data/examples/ex5.rb +103 -0
- data/examples/exbox.rb +141 -0
- data/examples/exm1.rb +137 -0
- data/examples/keys.rb +67 -0
- data/examples/tt.rb +462 -0
- data/lib/umbra/box.rb +137 -0
- data/lib/umbra/button.rb +130 -0
- data/lib/umbra/buttongroup.rb +96 -0
- data/lib/umbra/checkbox.rb +42 -0
- data/lib/umbra/dialog.rb +214 -0
- data/lib/umbra/eventhandler.rb +134 -0
- data/lib/umbra/field.rb +503 -0
- data/lib/umbra/form.rb +473 -0
- data/lib/umbra/keymappinghandler.rb +96 -0
- data/lib/umbra/label.rb +95 -0
- data/lib/umbra/labeledfield.rb +97 -0
- data/lib/umbra/listbox.rb +384 -0
- data/lib/umbra/menu.rb +93 -0
- data/lib/umbra/messagebox.rb +348 -0
- data/lib/umbra/pad.rb +340 -0
- data/lib/umbra/radiobutton.rb +71 -0
- data/lib/umbra/textbox.rb +417 -0
- data/lib/umbra/togglebutton.rb +140 -0
- data/lib/umbra/version.rb +3 -0
- data/lib/umbra/widget.rb +220 -0
- data/lib/umbra/window.rb +270 -0
- data/lib/umbra.rb +47 -0
- data/umbra.gemspec +27 -0
- metadata +127 -0
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
|
data/lib/umbra/label.rb
ADDED
@@ -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
|