ncumbra 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|