rbhex-layouts 1.1.0.alpha4

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.
@@ -0,0 +1,1205 @@
1
+ =begin
2
+ * Name: Monitor
3
+ * Description: Bootstrapped base class for 'monitor style applications'
4
+ * Author: Chris Arcand (@chrisarcand)
5
+ * Based on rkumar's experimental App class found in rbhex-core
6
+ * License:
7
+ Same as Ruby's License (http://www.ruby-lang.org/LICENSE.txt)
8
+ =end
9
+
10
+ require 'logger'
11
+ require 'rbhex/core'
12
+ require 'rbhex/core/util/widgetshortcuts'
13
+
14
+ include Rbhex
15
+ include Rbhex::Utils
16
+ include Io
17
+ module Rbhex
18
+ extend self
19
+
20
+ class Widget
21
+ def changed *args, &block
22
+ bind :CHANGED, *args, &block
23
+ end
24
+ def leave *args, &block
25
+ bind :LEAVE, *args, &block
26
+ end
27
+ def enter *args, &block
28
+ bind :ENTER, *args, &block
29
+ end
30
+ # actually we already have command() for buttons
31
+ def click *args, &block
32
+ bind :PRESS, *args, &block
33
+ end
34
+ end
35
+
36
+ class CheckBox
37
+ # a little dicey XXX
38
+ def text(*val)
39
+ if val.empty?
40
+ @value ? @onvalue : @offvalue
41
+ else
42
+ super
43
+ end
44
+ end
45
+ end
46
+
47
+ class Monitor
48
+ include Rbhex::WidgetShortcuts
49
+
50
+ attr_reader :config
51
+ attr_reader :form
52
+ attr_reader :window
53
+ attr_writer :quit_key
54
+ # the row on which to prompt user for any inputs
55
+ #attr_accessor :prompt_row # 2011-10-17 14:06:22
56
+
57
+ # TODO: i should be able to pass window coords here in config
58
+ # :title
59
+ def initialize config={}, &block
60
+ #$log.debug " inside constructor of APP #{config} "
61
+ @config = config
62
+
63
+
64
+ widget_shortcuts_init
65
+ #@app_row = @app_col = 0
66
+ #@stack = [] # stack's coordinates
67
+ #@flowstack = []
68
+ @variables = {}
69
+ # if we are creating child objects then we will not use outer form. this object will manage
70
+ @current_object = []
71
+ @_system_commands = %w{ bind_global bind_component field_help_text }
72
+
73
+ init_vars
74
+ $log.debug "XXX APP CONFIG: #{@config} " if $log.debug?
75
+ run &block
76
+ end
77
+ def init_vars
78
+ @quit_key ||= FFI::NCurses::KEY_F10
79
+ # actually this should be maintained inside ncurses pack, so not loaded 2 times.
80
+ # this way if we call an app from existing program, App won't start ncurses.
81
+ unless $ncurses_started
82
+ init_ncurses
83
+ end
84
+ $lastline = Ncurses.LINES - 1
85
+ #@message_row = Ncurses.LINES-1
86
+ #@prompt_row = @message_row # hope to use for ask etc # 2011-10-17 14:06:27
87
+ unless $log
88
+ path = File.join(ENV["LOGDIR"] || "./" ,"rbc13.log")
89
+ file = File.open(path, File::WRONLY|File::TRUNC|File::CREAT)
90
+ $log = Logger.new(path)
91
+ $log.level = Logger::DEBUG # change to warn when you've tested your app.
92
+ colors = Ncurses.COLORS
93
+ $log.debug "START #{colors} colors --------- #{$0} win: #{@window} "
94
+ end
95
+ end
96
+ def logger; return $log; end
97
+ def close
98
+ raw_message_destroy
99
+ $log.debug " INSIDE CLOSE, #{@stop_ncurses_on_close} "
100
+ @window.destroy if !@window.nil?
101
+ $log.debug " INSIDE CLOSE, #{@stop_ncurses_on_close} "
102
+ if @stop_ncurses_on_close
103
+ $tt.destroy if $tt # added on 2011-10-9 since we created a window, but only hid it after use
104
+ VER::stop_ncurses
105
+ $log.debug " CLOSING NCURSES"
106
+ end
107
+ #p $error_message.value unless $error_message.value.nil?
108
+ $log.debug " CLOSING APP"
109
+ #end
110
+ end
111
+ # not sure, but user shuld be able to trap keystrokes if he wants
112
+ # but do i still call handle_key if he does, or give him total control.
113
+ # But loop is already called by framework
114
+ def loop &block
115
+ @form.repaint
116
+ @window.wrefresh
117
+ Ncurses::Panel.update_panels
118
+ @break_key = ?\C-q.getbyte(0)
119
+ # added this extra loop since from some places we exit using throw :close
120
+ # amd that was in a much higher place, and was getting us right out, with
121
+ # no chance of user canceling quit. This extra loop allows us to remain
122
+ # added on 2011-11-24
123
+ while true
124
+ catch :close do
125
+ while((ch = @window.getchar()) != 999 )
126
+ #break if ch == @break_key
127
+ if ch == @break_key || ch == @quit_key
128
+ #stopping = @window.fire_close_handler
129
+ #break if stopping.nil? || stopping
130
+ break
131
+ end
132
+
133
+ if @keyblock
134
+ str = keycode_tos ch
135
+ @keyblock.call(str.gsub(/-/, "_").to_sym) # not used ever
136
+ end
137
+
138
+ yield ch if block # <<<----
139
+ # this is what the user should have control ove. earlier we would put this in
140
+ # a try catch block so user could do what he wanted with the error. Now we
141
+ # need to get it to him somehow, perhaps through a block or on_error event
142
+ begin
143
+ @form.handle_key ch
144
+ rescue => err
145
+ $log.debug( "handle_key rescue reached ")
146
+ $log.debug( err.to_s)
147
+ $log.debug(err.backtrace.join("\n"))
148
+ textdialog [err.to_s, *err.backtrace], :title => "Exception"
149
+ end
150
+ #@form.repaint # was this duplicate ?? handle calls repaint not needed
151
+ @window.wrefresh
152
+ end
153
+ end # catch
154
+ stopping = @window.fire_close_handler
155
+ @window.wrefresh
156
+ break if stopping.nil? || stopping
157
+ end # while
158
+ end
159
+ # if calling loop separately better to call this, since it will shut off ncurses
160
+ # and print error on screen.
161
+ def safe_loop &block
162
+ begin
163
+ loop &block
164
+ rescue => ex
165
+ $log.debug( "APP.rb rescue reached ")
166
+ $log.debug( ex) if ex
167
+ $log.debug(ex.backtrace.join("\n")) if ex
168
+ ensure
169
+ close
170
+ # putting it here allows it to be printed on screen, otherwise it was not showing at all.
171
+ if ex
172
+ puts "========== EXCEPTION =========="
173
+ p ex
174
+ puts "==============================="
175
+ puts(ex.backtrace.join("\n"))
176
+ end
177
+ end
178
+ end
179
+ # returns a symbol of the key pressed
180
+ # e.g. :C_c for Ctrl-C
181
+ # :Space, :bs, :M_d etc
182
+ def keypress &block
183
+ @keyblock = block
184
+ end
185
+ # updates a global var with text. Calling app has to set up a Variable with that name and attach to
186
+ # a label so it can be printed.
187
+ def message text
188
+ $status_message.value = text # trying out 2011-10-9
189
+ #@message.value = text # 2011-10-17 14:07:01
190
+ end
191
+
192
+ # during a process, when you wish to update status, since ordinarily the thread is busy
193
+ # and form does not get control back, so the window won't refresh.
194
+ # This will only update on keystroke since it uses statusline
195
+ # @deprecated please use {#status_line} instead of a message label
196
+ def message_immediate text
197
+ $log.warn "DEPRECATED, use message(), or rb_puts or use status_window"
198
+ $status_message.value = text # trying out 2011-10-9 user needs to use in statusline command
199
+ # 2011-10-17 knocking off label, should be printed on status_line
200
+ end
201
+ # Usage: application is inside a long processing loop and wishes to print ongoing status
202
+ # NOTE: if you use this, you must use raw_message_destroy at some stage, after processing
203
+ # or on_leave of object.
204
+ # @deprecated Use say_with_pause or use rdialogs status_window, see test2.rb
205
+ def raw_message text, config={}, &blk
206
+ $raw_window ||= one_line_window last_line(), config, &blk
207
+ width = $raw_window.width == 0 ? FFI::NCurses.COLS : $raw_window.width
208
+ text = "%-*s" % [width, text]
209
+
210
+ $raw_window.attron(Ncurses.COLOR_PAIR($normalcolor) )
211
+ $raw_window.printstring 0,0,text, $normalcolor #, 'normal' if @title
212
+ $raw_window.wrefresh
213
+
214
+ end
215
+ def raw_message_destroy
216
+ if $raw_window
217
+ $raw_window.destroy
218
+ $raw_window = nil
219
+ end
220
+ end
221
+ # shows a simple progress bar on last row, using stdscr
222
+ # @param [Float, Array<Fixnum,Fixnum>] percentage, or part/total
223
+ # If Array of two numbers is given then also print part/total on left of bar
224
+ # @deprecated - don't use stdscr at all, use rdialogs status_window (see test2.rb)
225
+ def raw_progress arg
226
+ $log.warning "WARNING: don't use this method as it uses stdscr"
227
+ row = @message_label ? @message_label.row : Ncurses.LINES-1
228
+ s = nil
229
+ case arg
230
+ when Array
231
+ #calculate percentage
232
+ pc = (arg[0]*1.0)/arg[1]
233
+ # print items/total also
234
+ s = "%-10s" % "(#{arg[0]}/#{arg[1]})"
235
+ when
236
+ Float
237
+ pc = arg
238
+ end
239
+ scr = Ncurses.stdscr
240
+ endcol = Ncurses.COLS-1
241
+ startcol = endcol - 12
242
+ stext = ("=" * (pc*10).to_i)
243
+ text = "[" + "%-10s" % stext + "]"
244
+ Ncurses.mvprintw( row ,startcol-10, s) if s
245
+ Ncurses.mvprintw row ,startcol, text
246
+ #scr.refresh() # XXX FFI NW
247
+
248
+ end
249
+ # used only by LiveConsole, if enables in an app, usually only during testing.
250
+ def get_binding
251
+ return binding()
252
+ end
253
+ #
254
+ # suspends curses so you can play around on the shell
255
+ # or in cooked mode like Vim does. Expects a block to be passed.
256
+ # Purpose: you can print some stuff without creating a window, or
257
+ # just run shell commands without coming out.
258
+ # NOTE: if you pass clear as true, then the screen will be cleared
259
+ # and you can use puts or print to print. You may have to flush.
260
+ # However, with clear as false, the screen will not be cleared. You
261
+ # will have to print using printw, and if you expect user input
262
+ # you must do a "system /bin/stty sane"
263
+ # If you print stuff, you will have to put a getch() or system("read")
264
+ # to pause the screen.
265
+ def suspend clear=true
266
+ return unless block_given?
267
+ Ncurses.def_prog_mode
268
+ if clear
269
+ Ncurses.endwin
270
+ # NOTE: avoid false since screen remains half off
271
+ # too many issues
272
+ else
273
+ system "/bin/stty sane"
274
+ end
275
+ yield if block_given?
276
+ Ncurses.reset_prog_mode
277
+ if !clear
278
+ # Hope we don't screw your terminal up with this constantly.
279
+ VER::stop_ncurses
280
+ VER::start_ncurses
281
+ #@form.reset_all # not required
282
+ end
283
+ @form.repaint
284
+ @window.wrefresh
285
+ Ncurses::Panel.update_panels
286
+ end
287
+ def get_all_commands
288
+ opts = @_system_commands.dup
289
+ if respond_to? :get_commands
290
+ opts.push(*get_commands())
291
+ end
292
+ opts
293
+ end
294
+ # bind a key to a method at global (form) level
295
+ # Note that individual component may be overriding this.
296
+ # FIXME: why are we using rawmessage and then getchar when ask would suffice
297
+ def bind_global
298
+ opts = get_all_commands
299
+ cmd = rb_gets("Select a command (<tab> for choices) : ", opts)
300
+ if cmd.nil? || cmd == ""
301
+ rb_puts "Aborted."
302
+ return
303
+ end
304
+ key = []
305
+ str = ""
306
+ #raw_message "Enter one or 2 keys. Finish with ENTER. Enter first key:"
307
+ #ch = @window.getchar()
308
+ #raw_message_destroy
309
+ #if [KEY_ENTER, 10, 13, ?\C-g.getbyte(0)].include? ch
310
+ #say_with_pause "Aborted."
311
+ #return
312
+ #end
313
+ # the next is fine but does not allow user to enter a control or alt or function character
314
+ # since it uses Field. It is fine if you want to force alphanum input
315
+ ch = rb_getchar("Enter one or two keys. Finish with <ENTER>. Enter first key:")
316
+ unless ch
317
+ rb_puts "Aborted. <Press a key>"
318
+ return
319
+ end
320
+ key << ch
321
+ str << keycode_tos(ch)
322
+ ch = rb_getchar "Got #{str}. Enter second key or hit return:"
323
+ unless ch
324
+ rb_puts "Aborted. <Press a key>"
325
+ return
326
+ end
327
+ if ch == KEY_ENTER || ch == 13
328
+ else
329
+ key << ch
330
+ str << keycode_tos(ch)
331
+ end
332
+ if !key.empty?
333
+ rb_puts "Binding #{cmd} to #{str}. "
334
+ key = key[0] if key.size == 1
335
+ #@form.bind_key(key, cmd.to_sym) # not finding it, getting called by that comp
336
+ @form.bind_key(key){ send(cmd.to_sym) }
337
+ end
338
+ end
339
+ def bind_component
340
+ #rb_puts "Todo. ", :color_pair => get_color($promptcolor, :red, :black)
341
+ print_error_message "Todo this. "
342
+ # the idea here is to get the current component
343
+ # and bind some keys to some methods.
344
+ # however, how do we divine the methods we can map to
345
+ # and also in some cases the components itself has multiple components
346
+ end
347
+ # displays help_text associated with field. 2011-10-15
348
+ def field_help_text
349
+ f = @form.get_current_field
350
+ if f.respond_to?('help_text')
351
+ h = f.help_text
352
+ h = "No help text defined for this field.\nTry F1, or press '?' for key-bindings." unless h
353
+ textdialog "#{h}", :title => "Widget Help Text"
354
+ else
355
+ alert "Could not get field #{f} or does not respond to helptext. Try F1 or '?'"
356
+ end
357
+ end
358
+ # prompts user for a command. we need to get this back to the calling app
359
+ # or have some block stuff TODO
360
+ # Actually, this is naive, you would want to pass some values in like current data value
361
+ # or lines ??
362
+ # Also may want command completion, or help so all commands can be displayed
363
+ # NOTE: This is gonna change very soon - 2012-01-8
364
+ def get_command_from_user choices=["quit","help", "suspend", "shell_output"]
365
+ @_command_history ||= Array.new
366
+ str = rb_gets("Cmd: ", choices) { |q| q.default = @_previous_command; q.history = @_command_history }
367
+ @_command_history << str unless @_command_history.include? str
368
+ # shell the command
369
+ if str =~ /^!/
370
+ str = str[1..-1]
371
+ suspend(false) {
372
+ #system(str);
373
+ $log.debug "XXX STR #{str} " if $log.debug?
374
+
375
+ output=`#{str}`
376
+ system("echo ' ' ");
377
+ $log.debug "XXX output #{output} " if $log.debug?
378
+ system("echo '#{output}' ");
379
+ system("echo Press Enter to continue.");
380
+ system("read");
381
+ }
382
+ return nil # i think
383
+ else
384
+ # TODO
385
+ # here's where we can take internal commands
386
+ #alert "[#{str}] string did not match :!"
387
+ str = str.to_s #= str[1..-1]
388
+ cmdline = str.split
389
+ cmd = cmdline.shift #.to_sym
390
+ return unless cmd # added 2011-09-11 FFI
391
+ f = @form.get_current_field
392
+ if respond_to?(cmd, true)
393
+ if cmd == "close"
394
+ throw :close # other seg faults in del_panel window.destroy executes 2x
395
+ else
396
+ res = send cmd, *cmdline
397
+ end
398
+ elsif f.respond_to?(cmd, true)
399
+ res = f.send(cmd, *cmdline)
400
+ else
401
+ alert "App: #{self.class} does not respond to #{cmd} "
402
+ ret = false
403
+ # what is this execute_this: some kind of general routine for all apps ?
404
+ ret = execute_this(cmd, *cmdline) if respond_to?(:execute_this, true)
405
+ rb_puts("#{self.class} does not respond to #{cmd} ", :color_pair => $promptcolor) unless ret
406
+ # should be able to say in red as error
407
+ end
408
+ end
409
+ end
410
+ #
411
+ # @group methods to create widgets easily
412
+ #
413
+ # process arguments based on datatype, perhaps making configuration
414
+ # of some components easier for caller avoiding too much boiler plate code
415
+ #
416
+ # create a field
417
+ def OLDfield *args, &block
418
+ config = {}
419
+ events = [ :CHANGED, :LEAVE, :ENTER, :CHANGE ]
420
+ block_event = :CHANGED # LEAVE, ENTER, CHANGE
421
+
422
+ _process_args args, config, block_event, events
423
+ config.delete(:title)
424
+ _position config
425
+ # hope next line doesn't bonk anything
426
+ config[:display_length] ||= @stack.last.width if @stack.last # added here not sure 2010-11-17 18:43
427
+ field = Field.new @form, config
428
+ # shooz uses CHANGED, which is equivalent to our CHANGE. Our CHANGED means modified and exited
429
+ if block
430
+ field.bind(block_event, &block)
431
+ end
432
+ return field
433
+ end
434
+ #instance_eval &block if block_given?
435
+ # or
436
+ #@blk = block # for later execution using @blk.call()
437
+ #colorlabel = Label.new @form, {'text' => "Select a color:", "row" => row, "col" => col, "color"=>"cyan", "mnemonic" => 'S'}
438
+ #var = Rbhex::Label.new @form, {'text_variable' => $results, "row" => r, "col" => fc}
439
+
440
+ def OLDlabel *args
441
+ events = block_event = nil
442
+ config = {}
443
+ _process_args args, config, block_event, events
444
+ config[:text] ||= config[:name]
445
+ config[:height] ||= 1
446
+ config.delete(:title)
447
+ _position(config)
448
+ label = Label.new @form, config
449
+ # shooz uses CHANGED, which is equivalent to our CHANGE. Our CHANGED means modified and exited
450
+ return label
451
+ end
452
+ alias :text :label
453
+ def OLDbutton *args, &block
454
+ config = {}
455
+ events = [ :PRESS, :LEAVE, :ENTER ]
456
+ block_event = :PRESS
457
+
458
+ _process_args args, config, block_event, events
459
+ config[:text] ||= config[:name]
460
+ config.delete(:title)
461
+ # flow gets precedence over stack
462
+ _position(config)
463
+ button = Button.new @form, config
464
+ # shooz uses CHANGED, which is equivalent to our CHANGE. Our CHANGED means modified and exited
465
+ if block
466
+ button.bind(block_event, &block)
467
+ end
468
+ return button
469
+ end
470
+ #
471
+ # create a list
472
+ # Since we are mouseless, one can traverse without selection. So we have a different
473
+ # way of selecting row/s and traversal. XXX this aspect of LB's has always troubled me hugely.
474
+ def OLDedit_list *args, &block # earlier list_box
475
+ config = {}
476
+ # TODO confirm events
477
+ # listdataevent has interval added and interval removed, due to multiple
478
+ # selection, we have to make that simple for user here.
479
+ events = [ :LEAVE, :ENTER, :ENTER_ROW, :LEAVE_ROW, :LIST_DATA_EVENT ]
480
+ # TODO how to do this so he gets selected row easily
481
+ block_event = :ENTER_ROW
482
+
483
+ _process_args args, config, block_event, events
484
+ # naive defaults, since list could be large or have very long items
485
+ # usually user will provide
486
+ if !config.has_key? :height
487
+ ll = 0
488
+ ll = config[:list].length + 2 if config.has_key? :list
489
+ config[:height] ||= ll
490
+ config[:height] = 15 if config[:height] > 20
491
+ end
492
+ if @current_object.empty?
493
+ $log.debug "1 APP LB w: #{config[:width]} ,#{config[:name]} "
494
+ config[:width] ||= @stack.last.width if @stack.last
495
+ $log.debug "2 APP LB w: #{config[:width]} "
496
+ config[:width] ||= longest_in_list(config[:list])+2
497
+ $log.debug "3 APP LB w: #{config[:width]} "
498
+ end
499
+ # if no width given, expand to flows width XXX SHOULD BE NOT EXPAND ?
500
+ #config[:width] ||= @stack.last.width if @stack.last
501
+ #if config.has_key? :choose
502
+ config[:default_values] = config.delete :choose
503
+ # we make the default single unless specified
504
+ config[:selection_mode] = :single unless config.has_key? :selection_mode
505
+ if @current_object.empty?
506
+ if @instack
507
+ # most likely you won't have row and col. should we check or just go ahead
508
+ col = @stack.last.margin
509
+ config[:row] = @app_row
510
+ config[:col] = col
511
+ @app_row += config[:height] # this needs to take into account height of prev object
512
+ end
513
+ end
514
+ useform = nil
515
+ useform = @form if @current_object.empty?
516
+ field = EditList.new useform, config # earlier ListBox
517
+ # shooz uses CHANGED, which is equivalent to our CHANGE. Our CHANGED means modified and exited
518
+ if block
519
+ # this way you can't pass params to the block
520
+ field.bind(block_event, &block)
521
+ end
522
+ return field
523
+ end
524
+
525
+ # toggle button
526
+ def OLDtoggle *args, &block
527
+ config = {}
528
+ # TODO confirm events
529
+ events = [ :PRESS, :LEAVE, :ENTER ]
530
+ block_event = :PRESS
531
+ _process_args args, config, block_event, events
532
+ config[:text] ||= longest_in_list2( [config[:onvalue], config[:offvalue]])
533
+ #config[:onvalue] # needed for flow, we need a better way FIXME
534
+ _position(config)
535
+ toggle = ToggleButton.new @form, config
536
+ if block
537
+ toggle.bind(block_event, &block)
538
+ end
539
+ return toggle
540
+ end
541
+ # check button
542
+ def OLDcheck *args, &block
543
+ config = {}
544
+ # TODO confirm events
545
+ events = [ :PRESS, :LEAVE, :ENTER ]
546
+ block_event = :PRESS
547
+ _process_args args, config, block_event, events
548
+ _position(config)
549
+ toggle = CheckBox.new @form, config
550
+ if block
551
+ toggle.bind(block_event, &block)
552
+ end
553
+ return toggle
554
+ end
555
+ # radio button
556
+ def OLDradio *args, &block
557
+ config = {}
558
+ # TODO confirm events
559
+ events = [ :PRESS, :LEAVE, :ENTER ]
560
+ block_event = :PRESS
561
+ _process_args args, config, block_event, events
562
+ a = config[:group]
563
+ # FIXME we should check if user has set a varialbe in :variable.
564
+ # we should create a variable, so he can use it if he wants.
565
+ if @variables.has_key? a
566
+ v = @variables[a]
567
+ else
568
+ v = Variable.new
569
+ @variables[a] = v
570
+ end
571
+ config[:variable] = v
572
+ config.delete(:group)
573
+ _position(config)
574
+ radio = RadioButton.new @form, config
575
+ if block
576
+ radio.bind(block_event, &block)
577
+ end
578
+ return radio
579
+ end
580
+ # editable text area
581
+ def OLDtextarea *args, &block
582
+ require 'rbhex/core/widgets/rtextarea'
583
+ config = {}
584
+ # TODO confirm events many more
585
+ events = [ :CHANGE, :LEAVE, :ENTER ]
586
+ block_event = events[0]
587
+ _process_args args, config, block_event, events
588
+ config[:width] = config[:display_length] unless config.has_key? :width
589
+ _position(config)
590
+ # if no width given, expand to flows width
591
+ config[:width] ||= @stack.last.width if @stack.last
592
+ useform = nil
593
+ useform = @form if @current_object.empty?
594
+ w = TextArea.new useform, config
595
+ if block
596
+ w.bind(block_event, &block)
597
+ end
598
+ return w
599
+ end
600
+ # similar definitions for textview and resultsettextview
601
+ # NOTE This is not allowing me to send blocks,
602
+ # so do not use for containers
603
+ {
604
+ 'rbhex/core/widgets/rtextview' => 'TextView',
605
+ 'rbhex/experimental/resultsettextview' => 'ResultsetTextView',
606
+ 'rbhex/core/widgets/rcontainer' => 'Container',
607
+ 'rbhex/extras/rcontainer2' => 'Container2',
608
+ }.each_pair {|k,p|
609
+ eval(
610
+ "def OLD#{p.downcase} *args, &block
611
+ require \"#{k}\"
612
+ config = {}
613
+ # TODO confirm events many more
614
+ events = [ :PRESS, :LEAVE, :ENTER ]
615
+ block_event = events[0]
616
+ _process_args args, config, block_event, events
617
+ config[:width] = config[:display_length] unless config.has_key? :width
618
+ _position(config)
619
+ # if no width given, expand to flows width
620
+ config[:width] ||= @stack.last.width if @stack.last
621
+ raise \"height needed for #{p.downcase}\" if !config.has_key? :height
622
+ useform = nil
623
+ useform = @form if @current_object.empty?
624
+ w = #{p}.new useform, config
625
+ if block
626
+ w.bind(block_event, &block)
627
+ end
628
+ return w
629
+ end"
630
+ )
631
+ }
632
+
633
+ # table widget
634
+ # @example
635
+ # data = [["Roger",16,"SWI"], ["Phillip",1, "DEU"]]
636
+ # colnames = ["Name", "Wins", "Place"]
637
+ # t = table :width => 40, :height => 10, :columns => colnames, :data => data, :estimate_widths => true
638
+ # other options are :column_widths => [12,4,12]
639
+ # :size_to_fit => true
640
+ def edit_table *args, &block # earlier table
641
+ require 'rbhex/extras/widgets/rtable'
642
+ config = {}
643
+ # TODO confirm events many more
644
+ events = [ :ENTER_ROW, :LEAVE, :ENTER ]
645
+ block_event = events[0]
646
+ _process_args args, config, block_event, events
647
+ # if user is leaving out width, then we don't want it in config
648
+ # else Widget will put a value of 10 as default, overriding what we've calculated
649
+ if config.has_key? :display_length
650
+ config[:width] = config[:display_length] unless config.has_key? :width
651
+ end
652
+ ext = config.delete :extended_keys
653
+
654
+ model = nil
655
+ _position(config)
656
+ # if no width given, expand to flows width
657
+ config[:width] ||= @stack.last.width if @stack.last
658
+ w = Table.new @form, config
659
+ if ext
660
+ require 'rbhex/extras/include/tableextended'
661
+ # so we can increase and decrease column width using keys
662
+ w.extend TableExtended
663
+ w.bind_key(?w){ w.next_column }
664
+ w.bind_key(?b){ w.previous_column }
665
+ w.bind_key(?+) { w.increase_column }
666
+ w.bind_key(?-) { w.decrease_column }
667
+ w.bind_key([?d, ?d]) { w.table_model.delete_at w.current_index }
668
+ w.bind_key(?u) { w.table_model.undo w.current_index}
669
+ end
670
+ if block
671
+ w.bind(block_event, &block)
672
+ end
673
+ return w
674
+ end
675
+ # print a title on first row
676
+ def title string, config={}
677
+ ## TODO center it
678
+ @window.printstring 1, 30, string, $normalcolor, 'reverse'
679
+ end
680
+ # print a sutitle on second row
681
+ def subtitle string, config={}
682
+ @window.printstring 2, 30, string, $datacolor, 'normal'
683
+ end
684
+ # menu bar
685
+
686
+ # creates a blank row
687
+ def OLDblank rows=1, config={}
688
+ @app_row += rows
689
+ end
690
+ # displays a horizontal line
691
+ # takes col (column to start from) from current stack
692
+ # take row from app_row
693
+ #
694
+ # requires width to be passed in config, else defaults to 20
695
+ # @example
696
+ # hline :width => 55
697
+ def hline config={}
698
+ row = config[:row] || @app_row
699
+ width = config[:width] || 20
700
+ _position config
701
+ col = config[:col] || 1
702
+ @color_pair = config[:color_pair] || $datacolor
703
+ @attrib = config[:attrib] || Ncurses::A_NORMAL
704
+ @window.attron(Ncurses.COLOR_PAIR(@color_pair) | @attrib)
705
+ @window.mvwhline( row, col, FFI::NCurses::ACS_HLINE, width)
706
+ @window.attron(Ncurses.COLOR_PAIR(@color_pair) | @attrib)
707
+ @app_row += 1
708
+ end
709
+
710
+ def TODOmultisplit *args, &block
711
+ require 'rbhex/extras/widgets/rmultisplit'
712
+ config = {}
713
+ events = [ :PROPERTY_CHANGE, :LEAVE, :ENTER ]
714
+ block_event = events[0]
715
+ _process_args args, config, block_event, events
716
+ _position(config)
717
+ # if no width given, expand to flows width
718
+ config[:width] ||= @stack.last.width if @stack.last
719
+ config.delete :title
720
+ useform = nil
721
+ useform = @form if @current_object.empty?
722
+
723
+ w = MultiSplit.new useform, config
724
+ #if block
725
+ #w.bind(block_event, w, &block)
726
+ #end
727
+ if block_given?
728
+ @current_object << w
729
+ #instance_eval &block if block_given?
730
+ yield w
731
+ @current_object.pop
732
+ end
733
+ return w
734
+ end
735
+ # create a readonly list
736
+ # I don't want to rename this to list, as that could lead to
737
+ # confusion, maybe rlist
738
+ def OLDlistbox *args, &block # earlier basic_list
739
+ require 'rbhex/core/widgets/rlist'
740
+ config = {}
741
+ #TODO check these
742
+ events = [ :LEAVE, :ENTER, :ENTER_ROW, :LEAVE_ROW, :LIST_DATA_EVENT ]
743
+ # TODO how to do this so he gets selected row easily
744
+ block_event = :ENTER_ROW
745
+ _process_args args, config, block_event, events
746
+ # some guesses at a sensible height for listbox
747
+ if !config.has_key? :height
748
+ ll = 0
749
+ ll = config[:list].length + 2 if config.has_key? :list
750
+ config[:height] ||= ll
751
+ config[:height] = 15 if config[:height] > 20
752
+ end
753
+ _position(config)
754
+ # if no width given, expand to flows width
755
+ config[:width] ||= @stack.last.width if @stack.last
756
+ config[:width] ||= longest_in_list(config[:list])+2
757
+ #config.delete :title
758
+ #config[:default_values] = config.delete :choose
759
+ config[:selection_mode] = :single unless config.has_key? :selection_mode
760
+ useform = nil
761
+ useform = @form if @current_object.empty?
762
+
763
+ w = List.new useform, config # NO BLOCK GIVEN
764
+ if block_given?
765
+ field.bind(block_event, &block)
766
+ end
767
+ return w
768
+ end
769
+ alias :basiclist :listbox # this alias will be removed
770
+ def TODOmaster_detail *args, &block
771
+ require 'rbhex/experimental/widgets/masterdetail'
772
+ config = {}
773
+ events = [:PROPERTY_CHANGE, :LEAVE, :ENTER ]
774
+ block_event = nil
775
+ _process_args args, config, block_event, events
776
+ #config[:height] ||= 10
777
+ _position(config)
778
+ # if no width given, expand to flows width
779
+ config[:width] ||= @stack.last.width if @stack.last
780
+ #config.delete :title
781
+ useform = nil
782
+ useform = @form if @current_object.empty?
783
+
784
+ w = MasterDetail.new useform, config # NO BLOCK GIVEN
785
+ if block_given?
786
+ @current_object << w
787
+ yield_or_eval &block
788
+ @current_object.pop
789
+ end
790
+ return w
791
+ end
792
+ # scrollbar attached to the right of a parent object
793
+ def OLDscrollbar *args, &block
794
+ require 'rbhex/core/widgets/scrollbar'
795
+ config = {}
796
+ events = [:PROPERTY_CHANGE, :LEAVE, :ENTER ] # # none really at present
797
+ block_event = nil
798
+ _process_args args, config, block_event, events
799
+ raise "parent needed for scrollbar" if !config.has_key? :parent
800
+ useform = nil
801
+ useform = @form if @current_object.empty?
802
+ sb = Scrollbar.new useform, config
803
+ end
804
+ # divider used to resize neighbouring components TOTEST XXX
805
+ def OLDdivider *args, &block
806
+ require 'rbhex/core/widgets/divider'
807
+ config = {}
808
+ events = [:PROPERTY_CHANGE, :LEAVE, :ENTER, :DRAG_EVENT ] # # none really at present
809
+ block_event = nil
810
+ _process_args args, config, block_event, events
811
+ useform = nil
812
+ useform = @form if @current_object.empty?
813
+ sb = Divider.new useform, config
814
+ end
815
+ # creates a simple readonly table, that allows users to click on rows
816
+ # and also on the header. Header clicking is for column-sorting.
817
+ def OLDcombo *args, &block
818
+ require 'rbhex/core/widgets/rcombo'
819
+ config = {}
820
+ events = [:PROPERTY_CHANGE, :LEAVE, :ENTER, :CHANGE, :ENTER_ROW, :PRESS ] # XXX
821
+ block_event = nil
822
+ _process_args args, config, block_event, events
823
+ _position(config)
824
+ # if no width given, expand to flows width
825
+ config[:width] ||= @stack.last.width if @stack.last
826
+ #config.delete :title
827
+ useform = nil
828
+ useform = @form if @current_object.empty?
829
+
830
+ w = ComboBox.new useform, config # NO BLOCK GIVEN
831
+ if block_given?
832
+ @current_object << w
833
+ yield_or_eval &block
834
+ @current_object.pop
835
+ end
836
+ return w
837
+ end
838
+
839
+ # ADD new widget above this
840
+
841
+ # @endgroup
842
+
843
+ # @group positioning of components
844
+
845
+ # line up vertically whatever comes in, ignoring r and c
846
+ # margin_top to add to margin of existing stack (if embedded) such as extra spacing
847
+ # margin to add to margin of existing stack, or window (0)
848
+ # NOTE: since these coordins are calculated at start
849
+ # therefore if window resized i can't recalculate.
850
+ #Stack = Struct.new(:margin_top, :margin, :width)
851
+ def OLDstack config={}, &block
852
+ @instack = true
853
+ mt = config[:margin_top] || 1
854
+ mr = config[:margin] || 0
855
+ # must take into account margin
856
+ defw = Ncurses.COLS - mr
857
+ config[:width] = defw if config[:width] == :EXPAND
858
+ w = config[:width] || [50, defw].min
859
+ s = Stack.new(mt, mr, w)
860
+ @app_row += mt
861
+ mr += @stack.last.margin if @stack.last
862
+ @stack << s
863
+ yield_or_eval &block if block_given?
864
+ @stack.pop
865
+ @instack = false if @stack.empty?
866
+ @app_row = 0 if @stack.empty?
867
+ end
868
+ # keep adding to right of previous and when no more space
869
+ # move down and continue fitting in.
870
+ # Useful for button positioning. Currently, we can use a second flow
871
+ # to get another row.
872
+ # TODO: move down when row filled
873
+ # TODO: align right, center
874
+ def OLDflow config={}, &block
875
+ @inflow = true
876
+ mt = config[:margin_top] || 0
877
+ @app_row += mt
878
+ col = @flowstack.last || @stack.last.margin || @app_col
879
+ col += config[:margin] || 0
880
+ @flowstack << col
881
+ @flowcol = col
882
+ yield_or_eval &block if block_given?
883
+ @flowstack.pop
884
+ @inflow = false if @flowstack.empty?
885
+ end
886
+
887
+ private
888
+ def quit
889
+ throw(:close)
890
+ end
891
+ def help; display_app_help; end
892
+ # Initialize curses
893
+ def init_ncurses
894
+ VER::start_ncurses # this is initializing colors via ColorMap.setup
895
+ #$ncurses_started = true
896
+ @stop_ncurses_on_close = true
897
+ end
898
+
899
+ # returns length of longest
900
+ def longest_in_list list #:nodoc:
901
+ longest = list.inject(0) do |memo,word|
902
+ memo >= word.length ? memo : word.length
903
+ end
904
+ longest
905
+ end
906
+ # returns longest item
907
+ # rows = list.max_by(&:length)
908
+ #
909
+ def longest_in_list2 list #:nodoc:
910
+ longest = list.inject(list[0]) do |memo,word|
911
+ memo.length >= word.length ? memo : word
912
+ end
913
+ longest
914
+ end
915
+
916
+ # if partial command entered then returns matches
917
+ def _resolve_command opts, cmd
918
+ return cmd if opts.include? cmd
919
+ matches = opts.grep Regexp.new("^#{cmd}")
920
+ end
921
+ # Now i am not creating this unless user wants it. Pls avoid it.
922
+ # Use either say_with_pause, or put $status_message in command of statusline
923
+ # @deprecated please use {#status_line} instead of a message label
924
+ def create_message_label row=Ncurses.LINES-1
925
+ @message_label = Rbhex::Label.new @form, {:text_variable => @message, :name=>"message_label",:row => row, :col => 0, :display_length => Ncurses.COLS, :height => 1, :color => :white}
926
+ end
927
+
928
+ def run &block
929
+ begin
930
+
931
+ # check if user has passed window coord in config, else root window
932
+ @window = VER::Window.root_window
933
+ awin = @window
934
+ catch(:close) do
935
+ @form = Form.new @window
936
+ @form.bind_key([?\C-x, ?c], 'suspend') { suspend(false) do
937
+ system("tput cup 26 0")
938
+ system("tput ed")
939
+ system("echo Enter C-d to return to application")
940
+ system (ENV['PS1']='\s-\v\$ ')
941
+ system(ENV['SHELL']);
942
+ end
943
+ }
944
+ # this is a very rudimentary default command executer, it does not
945
+ # allow tab completion. App should use M-x with names of commands
946
+ # as in appgmail
947
+ # NOTE: This is gonna change very soon - 2012-01-8
948
+ @form.bind_key(?:, 'prompt') {
949
+ str = get_command_from_user
950
+ }
951
+
952
+ # this M-x stuff has to be moved out so it can be used by all. One should be able
953
+ # to add_commands properly to this, and to C-x. I am thinking how to go about this,
954
+ # and what function M-x actually serves.
955
+
956
+ @form.bind_key(?\M-x, 'M-x commands'){
957
+ # TODO previous command to be default
958
+ opts = get_all_commands()
959
+ @_command_history ||= Array.new
960
+ # previous command should be in opts, otherwise it is not in this context
961
+ cmd = rb_gets("Command: ", opts){ |q| q.default = @_previous_command; q.history = @_command_history }
962
+ if cmd.nil? || cmd == ""
963
+ else
964
+ @_command_history << cmd unless @_command_history.include? cmd
965
+ cmdline = cmd.split
966
+ cmd = cmdline.shift
967
+ # check if command is a substring of a larger command
968
+ if !opts.include?(cmd)
969
+ rcmd = _resolve_command(opts, cmd) if !opts.include?(cmd)
970
+ if rcmd.size == 1
971
+ cmd = rcmd.first
972
+ elsif !rcmd.empty?
973
+ rb_puts "Cannot resolve #{cmd}. Matches are: #{rcmd} "
974
+ end
975
+ end
976
+ if respond_to?(cmd, true)
977
+ @_previous_command = cmd
978
+ #raw_message "calling #{cmd} "
979
+ begin
980
+ send cmd, *cmdline
981
+ rescue => exc
982
+ $log.error "ERR EXC: send throwing an exception now. Duh. IMAP keeps crashing haha !! #{exc} " if $log.debug?
983
+ if exc
984
+ $log.debug( exc)
985
+ $log.debug(exc.backtrace.join("\n"))
986
+ rb_puts exc.to_s
987
+ end
988
+ end
989
+ else
990
+ rb_puts("Command [#{cmd}] not supported by #{self.class} ", :color_pair => $promptcolor)
991
+ end
992
+ end
993
+ }
994
+ #@form.bind_key(KEY_F1, 'help'){ display_app_help } # NOT REQUIRED NOW 2012-01-7 since form does it
995
+ @form.bind_key([?q,?q], 'quit' ){ throw :close } if $log.debug?
996
+
997
+ #@message = Variable.new
998
+ #@message.value = ""
999
+ $status_message ||= Variable.new # remember there are multiple levels of apps
1000
+ $status_message.value = ""
1001
+ #$error_message.update_command { @message.set_value($error_message.value) }
1002
+ if block
1003
+ begin
1004
+ yield_or_eval &block if block_given? # modified 2010-11-17 20:36
1005
+ # how the hell does a user trap exception if the loop is hidden from him ? FIXME
1006
+ loop
1007
+ rescue => ex
1008
+ $log.debug( "APP.rb rescue reached ")
1009
+ $log.debug( ex) if ex
1010
+ $log.debug(ex.backtrace.join("\n")) if ex
1011
+ ensure
1012
+ close
1013
+ # putting it here allows it to be printed on screen, otherwise it was not showing at all.
1014
+ if ex
1015
+ puts "========== EXCEPTION =========="
1016
+ p ex
1017
+ puts "==============================="
1018
+ puts(ex.backtrace.join("\n"))
1019
+ end
1020
+ end
1021
+ nil
1022
+ else
1023
+ #@close_on_terminate = true
1024
+ self
1025
+ end #if block
1026
+ end # :close
1027
+ end
1028
+ end
1029
+ # TODO
1030
+ # process args, all widgets should call this
1031
+ def _process_args args, config, block_event, events #:nodoc:
1032
+ args.each do |arg|
1033
+ case arg
1034
+ when Array
1035
+ # please don't use this, keep it simple and use hash NOTE
1036
+ # we can use r,c, w, h
1037
+ row, col, display_length, height = arg
1038
+ config[:row] = row
1039
+ config[:col] = col
1040
+ config[:display_length] = display_length if display_length
1041
+ config[:width] = display_length if display_length
1042
+ # width for most XXX ?
1043
+ config[:height] = height if height
1044
+ when Hash
1045
+ config.merge!(arg)
1046
+ if block_event
1047
+ block_event = config.delete(:block_event){ block_event }
1048
+ raise "Invalid event. Use #{events}" unless events.include? block_event
1049
+ end
1050
+ when String
1051
+ config[:name] = arg
1052
+ config[:title] = arg # some may not have title
1053
+ #config[:text] = arg # some may not have title
1054
+ end
1055
+ end
1056
+ end # _process
1057
+ # position object based on whether in a flow or stack.
1058
+ # @app_row is prepared for next object based on this objects ht
1059
+ def OLD_position config #:nodoc:
1060
+ unless @current_object.empty?
1061
+ $log.debug " WWWW returning from position #{@current_object.last} "
1062
+ return
1063
+ end
1064
+ if @inflow
1065
+ #col = @flowstack.last
1066
+ config[:row] = @app_row
1067
+ config[:col] = @flowcol
1068
+ $log.debug " YYYY config #{config} "
1069
+ if config[:text]
1070
+ @flowcol += config[:text].length + 5 # 5 came from buttons
1071
+ else
1072
+ @flowcol += (config[:length] || 10) + 5 # trying out for combo
1073
+ end
1074
+ elsif @instack
1075
+ # most likely you won't have row and col. should we check or just go ahead
1076
+ # what if he has put it 2011-10-19 as in a container
1077
+ col = @stack.last.margin
1078
+ config[:row] ||= @app_row
1079
+ config[:col] ||= col
1080
+ @app_row += config[:height] || 1 #unless config[:no_advance]
1081
+ # TODO need to allow stack to have its spacing, but we don't have an object as yet.
1082
+ end
1083
+ end
1084
+ end # class
1085
+ end # module
1086
+ if $0 == __FILE__
1087
+ include Rbhex
1088
+ #app = App.new
1089
+ #window = app.window
1090
+ #window.printstring 2, 30, "Demo of Listbox - rbhex", $normalcolor, 'reverse'
1091
+ #app.logger.info "beforegetch"
1092
+ #window.getch
1093
+ #app.close
1094
+ # this was the yield example, but now we've moved to instance eval
1095
+ App.new do
1096
+ @window.printstring 0, 30, "Demo of Listbox - rbhex", $normalcolor, 'reverse'
1097
+ @window.printstring 1, 30, "Hit F1 to quit", $datacolor, 'normal'
1098
+ form = @form
1099
+ fname = "Search"
1100
+ r, c = 7, 30
1101
+ c += fname.length + 1
1102
+ #field1 = field( [r,c, 30], fname, :bgcolor => "cyan", :block_event => :CHANGE) do |fld|
1103
+ stack :margin_top => 2, :margin => 10 do
1104
+ lbl = label({:text => fname, :color=>'white',:bgcolor=>'red', :mnemonic=> 's'})
1105
+ field1 = field( [r,c, 30], fname, :bgcolor => "cyan",:block_event => :CHANGE) do |fld|
1106
+ message("You entered #{fld.getvalue}. To quit enter quit and tab out")
1107
+ if fld.getvalue == "quit"
1108
+ logger.info "you typed quit!"
1109
+ throw :close
1110
+ end
1111
+ end
1112
+ #field1.set_label Label.new @form, {:text => fname, :color=>'white',:bgcolor=>'red', :mnemonic=> 's'}
1113
+ field1.set_label( lbl )
1114
+ field1.enter do
1115
+ message "you entered this field"
1116
+ end
1117
+
1118
+ stack :margin_top => 2, :margin => 0 do
1119
+ #label( [8, 30, 60],{:text => "A label", :color=>'white',:bgcolor=>'blue'} )
1120
+ end
1121
+
1122
+ @bluelabel = label( [8, 30, 60],{:text => "B label", :color=>'white',:bgcolor=>'blue'} )
1123
+
1124
+ stack :margin_top => 2, :margin => 0 do
1125
+ toggle :onvalue => " Toggle Down ", :offvalue => " Untoggle ", :mnemonic => 'T', :value => true
1126
+
1127
+ toggle :onvalue => " On ", :offvalue => " Off ", :value => true do |e|
1128
+ alert "You pressed me #{e.state}"
1129
+ end
1130
+ check :text => "Check me!", :onvalue => "Checked", :offvalue => "Unchecked", :value => true do |e|
1131
+ # this works but long and complicated
1132
+ #@bluelabel.text = e.item.getvalue ? e.item.onvalue : e.item.offvalue
1133
+ @bluelabel.text = e.item.text
1134
+ end
1135
+ radio :text => "red", :value => "RED", :color => "red", :group => :colors
1136
+ radio :text => "green", :value => "GREEN", :color => "green", :group => :colors
1137
+ flow do
1138
+ button_row = 17
1139
+ ok_button = button( [button_row,30], "OK", {:mnemonic => 'O'}) do
1140
+ alert("About to dump data into log file!")
1141
+ message "Dumped data to log file"
1142
+ end
1143
+
1144
+ # using ampersand to set mnemonic
1145
+ cancel_button = button( [button_row, 40], "&Cancel" ) do
1146
+ if confirm("Do your really want to quit?")== :YES
1147
+ #throw(:close);
1148
+ quit
1149
+ else
1150
+ message "Quit aborted"
1151
+ end
1152
+ end # cancel
1153
+ button "Don't know"
1154
+ end
1155
+ flow :margin_top => 2 do
1156
+ button "Another"
1157
+ button "Line"
1158
+ end
1159
+ stack :margin_top => 2, :margin => 0 do
1160
+ @pbar = progress :width => 20, :bgcolor => 'white', :color => 'red'
1161
+ @pbar1 = progress :width => 20, :style => :old
1162
+ end
1163
+ end
1164
+ end # stack
1165
+ # lets make another column
1166
+ stack :margin_top => 2, :margin => 70 do
1167
+ l = label "Column 2"
1168
+ f1 = field "afield", :bgcolor => 'white', :color => 'black'
1169
+ listbox "A list", :list => ["Square", "Oval", "Rectangle", "Somethinglarge"], :choose => ["Square"]
1170
+ lb = listbox "Another", :list => ["Square", "Oval", "Rectangle", "Somethinglarge"] do |list|
1171
+ #f1.set_buffer list.text
1172
+ #f1.text list.text
1173
+ f1.text = list.text
1174
+ l.text = list.text.upcase
1175
+ end
1176
+ t = textarea :height => 10 do |e|
1177
+ #@bluelabel.text = e.to_s.tr("\n",' ')
1178
+ @bluelabel.text = e.text.gsub("\n"," ")
1179
+ len = e.source.get_text.length
1180
+ len = len % 20 if len > 20
1181
+ $log.debug " PBAR len of text is #{len}: #{len/20.0} "
1182
+ @pbar.fraction(len/20.0)
1183
+ @pbar1.fraction(len/20.0)
1184
+ i = ((len/20.0)*100).to_i
1185
+ @pbar.text = "completed:#{i}"
1186
+ end
1187
+ t.leave do |c|
1188
+ @bluelabel.text = c.get_text.gsub("\n"," ")
1189
+ end
1190
+
1191
+ end
1192
+
1193
+ # Allow user to get the keys
1194
+ keypress do |key|
1195
+ if key == :C_c
1196
+ message "You tried to cancel"
1197
+ #throw :close
1198
+ quit
1199
+ else
1200
+ #app.message "You pressed #{key}, #{char} "
1201
+ message "You pressed #{key}"
1202
+ end
1203
+ end
1204
+ end
1205
+ end