rbcurse 0.1.3 → 1.1.1

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.
Files changed (74) hide show
  1. data/CHANGELOG +126 -0
  2. data/Manifest.txt +53 -20
  3. data/README.markdown +423 -0
  4. data/Rakefile +3 -1
  5. data/examples/keytest.rb +177 -0
  6. data/examples/mpad2.rb +156 -0
  7. data/examples/newtesttabp.rb +121 -0
  8. data/examples/rfe.rb +48 -10
  9. data/examples/rfe_renderer.rb +4 -4
  10. data/examples/rvimsplit.rb +376 -0
  11. data/examples/sqlc.rb +97 -106
  12. data/examples/sqlm.rb +446 -0
  13. data/examples/test1.rb +4 -4
  14. data/examples/test2.rb +12 -12
  15. data/examples/testchars.rb +140 -0
  16. data/examples/testkeypress.rb +9 -4
  17. data/examples/testmulticomp.rb +72 -0
  18. data/examples/testscroller.rb +136 -0
  19. data/examples/testscrolllb.rb +86 -0
  20. data/examples/testscrollp.rb +87 -0
  21. data/examples/testscrollta.rb +80 -0
  22. data/examples/testscrolltable.rb +166 -0
  23. data/examples/testsplit.rb +87 -0
  24. data/examples/testsplit2.rb +123 -0
  25. data/examples/testsplit3.rb +215 -0
  26. data/examples/testsplit3_1.rb +244 -0
  27. data/examples/testsplit3a.rb +215 -0
  28. data/examples/testsplit3b.rb +237 -0
  29. data/examples/testsplitta.rb +148 -0
  30. data/examples/testsplittv.rb +142 -0
  31. data/examples/testsplittvv.rb +144 -0
  32. data/examples/testtable.rb +1 -1
  33. data/examples/testtabp.rb +3 -2
  34. data/examples/testtestw.rb +69 -0
  35. data/examples/testtodo.rb +5 -3
  36. data/examples/testtpane.rb +203 -0
  37. data/examples/testtpane2.rb +145 -0
  38. data/examples/testtpanetable.rb +199 -0
  39. data/examples/viewtodo.rb +5 -3
  40. data/lib/rbcurse.rb +1 -1
  41. data/lib/rbcurse/celleditor.rb +2 -2
  42. data/lib/rbcurse/colormap.rb +5 -5
  43. data/lib/rbcurse/defaultlistselectionmodel.rb +3 -3
  44. data/lib/rbcurse/io.rb +663 -0
  45. data/lib/rbcurse/listeditable.rb +306 -0
  46. data/lib/rbcurse/listkeys.rb +15 -15
  47. data/lib/rbcurse/listscrollable.rb +168 -27
  48. data/lib/rbcurse/mapper.rb +35 -13
  49. data/lib/rbcurse/rchangeevent.rb +28 -0
  50. data/lib/rbcurse/rform.rb +845 -0
  51. data/lib/rbcurse/rlistbox.rb +144 -34
  52. data/lib/rbcurse/rmessagebox.rb +10 -5
  53. data/lib/rbcurse/rmulticontainer.rb +325 -0
  54. data/lib/rbcurse/rmultitextview.rb +306 -0
  55. data/lib/rbcurse/rscrollform.rb +369 -0
  56. data/lib/rbcurse/rscrollpane.rb +511 -0
  57. data/lib/rbcurse/rsplitpane.rb +820 -0
  58. data/lib/rbcurse/rtabbedpane.rb +737 -109
  59. data/lib/rbcurse/rtabbedwindow.rb +326 -0
  60. data/lib/rbcurse/rtable.rb +220 -64
  61. data/lib/rbcurse/rtextarea.rb +340 -181
  62. data/lib/rbcurse/rtextview.rb +237 -101
  63. data/lib/rbcurse/rviewport.rb +203 -0
  64. data/lib/rbcurse/rwidget.rb +919 -95
  65. data/lib/rbcurse/scrollable.rb +7 -7
  66. data/lib/rbcurse/selectable.rb +4 -4
  67. data/lib/rbcurse/table/tablecellrenderer.rb +3 -0
  68. data/lib/rbcurse/undomanager.rb +181 -0
  69. data/lib/rbcurse/vieditable.rb +100 -0
  70. data/lib/ver/window.rb +471 -21
  71. metadata +66 -22
  72. data/README.txt +0 -312
  73. data/examples/testd.db +0 -0
  74. data/examples/todocsv.csv +0 -28
@@ -1,13 +1,25 @@
1
1
  =begin
2
2
  * Name: tabbed pane: can have multiple forms overlapping.
3
3
  * Description:
4
+ * A tabbed pane, mostly based (iirc) on the Terminal Preferences in OSX PPC 10.5.x
5
+ * Starting a new version using pads 2009-10-25 12:05
4
6
  * Author: rkumar
5
7
 
6
8
  --------
7
- * Date: 2008-12-13 13:06
9
+ * Date: 2009-10-25 12:05
8
10
  * License:
9
11
  Same as Ruby's License (http://www.ruby-lang.org/LICENSE.txt)
10
12
 
13
+ * 2010-02-28 09:47 - major cleanup and rewrite.
14
+ - Allow adding of component (in addition to form)
15
+ - Ideally, even form should be created and managed itself, why should TP have to repaint it?
16
+
17
+ NOTE:
18
+ Tp now does not create a form by default, since awefun you may want to just put in one component.
19
+ Pls use tp.form(tab) to get a form associated with the tab.
20
+ You may add as many tabs as you wish. To scroll tabs, traverse into the tab form and use the usual scroll keys M-l and M-h to scroll left and right.
21
+ #
22
+ # TODO : disable/hide tab ???
11
23
  =end
12
24
  require 'rubygems'
13
25
  require 'ncurses'
@@ -19,19 +31,21 @@ include RubyCurses
19
31
  module RubyCurses
20
32
  extend self
21
33
 
22
- # TODO : insert_tab, remove_tab, disable/hide tab
23
- # Hotkeys should be defined with ampersand, too.
24
- #
34
+ Event = Struct.new( :tab, :index, :event)
35
+
25
36
  # Multiple independent overlapping forms using the tabbed metaphor.
26
37
  class TabbedButton < RubyCurses::RadioButton
27
38
  def getvalue_for_paint
28
39
  @text
29
40
  end
41
+ def selected?
42
+ @variable.value == @value
43
+ end
30
44
  ##
31
45
  # highlight abd selected colors and attribs should perhaps be in a
32
46
  # structure, so user can override easily
33
47
  def repaint # tabbedbutton
34
- # $log.debug("BUTTon repaint : #{self.class()} r:#{@row} c:#{@col} #{getvalue_for_paint}" )
48
+ $log.debug("TabbedBUTTon repaint : #{self.class()} r:#{@row} c:#{@col} #{getvalue_for_paint}" )
35
49
  r,c = rowcol
36
50
  attribs = @attrs
37
51
  @highlight_foreground ||= $reversecolor
@@ -40,16 +54,24 @@ module RubyCurses
40
54
  _state = :SELECTED if @variable.value == @value
41
55
  case _state
42
56
  when :HIGHLIGHTED
57
+ $log.debug("TabbedBUTTon repaint : HIGHLIGHTED #{bgcolor}, #{color}, v: #{@value}" )
43
58
  bgcolor = @highlight_background
44
59
  color = @highlight_foreground
45
60
  bgcolor = @bgcolor
46
61
  color = @color
47
62
  attribs = Ncurses::A_BOLD
63
+ setrowcol r,c # show cursor on highlighted as we tab through
64
+ ## but when tabbing thru selected one, then selected one doesn't show cursor
48
65
  when :SELECTED
66
+ $log.debug("TabbedBUTTon repaint : SELECTED #{bgcolor}, #{color}")
49
67
  bgcolor = @bgcolor
50
68
  color = @color
51
69
  attribs = Ncurses::A_REVERSE
70
+ if @state == :HIGHLIGHTED
71
+ setrowcol r,c # show cursor on highlighted as we tab through
72
+ end
52
73
  else
74
+ $log.debug("TabbedBUTTon repaint : ELSE #{bgcolor}, #{color}")
53
75
  bgcolor = @bgcolor
54
76
  color = @color
55
77
  end
@@ -59,48 +81,234 @@ module RubyCurses
59
81
  color = ColorMap.get_color(color, bgcolor)
60
82
  end
61
83
  value = getvalue_for_paint
62
- # $log.debug("button repaint : r:#{r} c:#{c} col:#{color} bg #{bgcolor} v: #{value} ")
84
+ $log.debug("button repaint : r:#{r} #{@graphic.top} c:#{c} #{@graphic.left} color:#{color} bg #{bgcolor} v: #{value}, g: #{@graphic} ")
63
85
  len = @display_length || value.length
64
- @form.window.printstring r, c, "%-*s" % [len, value], color, attribs
86
+ # paint the tabs name in approp place with attribs
87
+ #@form.window.printstring r, c, "%-*s" % [len, value], color, attribs
88
+ #@graphic.printstring r+@graphic.top, c+@graphic.left, "%-*s" % [len, value], color, attribs
89
+ #@graphic.printstring r-@graphic.top, c-@graphic.left, "%-*s" % [len, value], color, attribs
90
+ @graphic.printstring r, c, "%-*s" % [len, value], color, attribs
91
+ @graphic.modified = true
65
92
  # @form.window.mvchgat(y=r, x=c, max=len, Ncurses::A_NORMAL, bgcolor, nil)
93
+ # underline for the top tab buttons.
66
94
  if @underline != nil
67
- # changed +1 to +0 on 2008-12-15 21:23 pls check.
68
- @form.window.mvchgat(y=r, x=c+@underline+0, max=1, Ncurses::A_BOLD|Ncurses::A_UNDERLINE, color, nil)
95
+ r -= @graphic.top # because of pad, remove if we go back to windows
96
+ c -= @graphic.left # because of pad, remove if we go back to windows
97
+ @graphic.mvchgat(y=r, x=c+@underline+0, max=1, Ncurses::A_BOLD|Ncurses::A_UNDERLINE, color, nil)
69
98
  end
70
99
  end
100
+ # trying to give the option so as we tab through buttons, the relevant tab opens
101
+ # but this is getting stuck on a tab and not going on
102
+ # fire() is causing the problem
103
+ # fire takes the focus into tab area so the next TAB goes back to first button
104
+ # due to current_tab = tab (so next key stroke goes to tab)
105
+ def on_enter
106
+ $log.debug " overridden on_enter of tabbedbutton #{@name} "
107
+ super
108
+ $log.debug " calling fire overridden on_enter of tabbedbutton"
109
+ fire if @display_tab_on_traversal
110
+ end
111
+ # In order to get tab display as we traverse buttons, we need to tamper with KEY_DOWN
112
+ # since that's the only way of getting down to selected tab in this case.
113
+ def handle_key ch
114
+ case ch
115
+ when KEY_DOWN
116
+ # form will not do a next_field, it will ignore this
117
+ return :NO_NEXT_FIELD
118
+ when KEY_RIGHT
119
+ @form.select_next_field
120
+ when KEY_LEFT
121
+ @form.select_prev_field
122
+ when KEY_ENTER, 10, 13, 32 # added space bar also
123
+ if respond_to? :fire
124
+ fire
125
+ end
126
+ else
127
+ # all thrse will be re-evaluated by form
128
+ return :UNHANDLED
129
+ end
130
+ end
131
+
71
132
  end
72
- class TabbedPane
73
- include DSL
74
- include EventHandler
75
- dsl_accessor :row, :col
76
- dsl_accessor :height, :width
133
+ ##
134
+ # extending Widget from 2009-10-08 18:45
135
+ # It should extend Widget so we can pop it in a form. In fact it should be in a form,
136
+ # we should not have tried to make it standalone like messagebox.
137
+ # This is the main TabbedPane widget that will be slung into a form
138
+ class TabbedPane < Widget
139
+ TAB_ROW_OFFSET = 3 # what row should tab start on (was 4 when printing subheader)
140
+ TAB_COL_OFFSET = 0 # what col should tab start on (to save space, flush on left)
77
141
  dsl_accessor :button_type # ok, ok_cancel, yes_no
78
142
  dsl_accessor :buttons # used if type :custom
79
143
  attr_reader :selected_index
80
- def initialize win, aconfig={}, &block
81
- @parent = win
144
+ attr_reader :current_tab
145
+ attr_reader :window
146
+ def initialize form, aconfig={}, &block
147
+ super
148
+ @parent = form
149
+ @parentwin = form.window
150
+ @visible = true
151
+ @focusable= true
82
152
  @tabs ||= []
83
153
  @forms ||= []
84
- @bgcolor ||= "black" # 0
85
- @color ||= "white" # $datacolor
86
154
  @attr = nil
87
155
  @current_form = nil
88
156
  @current_tab = nil
89
157
  @config = aconfig
90
- @config.each_pair { |k,v| variable_set(k,v) }
91
- instance_eval &block if block_given?
158
+ @col_offset = 2; @row_offset = 1 # added 2010-01-10 22:54
159
+ @recreate_buttons = true
160
+ install_keys
161
+ end
162
+ def install_keys
163
+ @form.bind_key([?d, ?d]) { ix = highlighted_tab_index; repeatm { remove_tab(ix) } }
164
+ @form.bind_key(?u) { undelete_tab; }
165
+ @form.bind_key(?p) { paste_tab 0; } # paste before or at position
166
+ @form.bind_key(?P) { paste_tab 1; } # paste deleted tab after this one
167
+ @form.bind_key([?c, ?w]) { change_label }
168
+ @form.bind_key(?C) { change_label }
92
169
  end
93
170
  ##
171
+ # This is a public, user called method for appending a new tab
172
+ # This will be called several times for one TP.
94
173
  # when adding tabs, you may use ampersand in text to create hotkey
95
- def add_tab text, aconfig={}, &block
96
- #create a button here and block is taken care of in button's instance
97
- #or push this for later creation.
98
- @tabs << Tab.new(text, aconfig, &block)
99
- tab = @tabs.last
100
- @forms << create_tab_form(tab)
101
- tab.form = @forms.last
174
+ # XXX adding a tab later does not influence buttons array,
175
+ def add_tab text, component = nil, aconfig={}, &block
176
+ index = @tabs.size
177
+ tab = insert_tab text, component, index, aconfig, &block
178
+ return tab
179
+ end
180
+ alias :add :add_tab
181
+ ## insert a component at given index
182
+ # index cannnot be greater than size of tab count
183
+ def insert_tab text, component, index, aconfig={}, &block
184
+ $log.debug " TAB insert #{text} at #{index} "
185
+ @tabs[index] = Tab.new(text, self, aconfig, &block)
186
+ tab = @tabs[index]
187
+ tab.component = component unless component.nil?
188
+ tab.index = index # so i can undelete !!!
189
+ fire_event tab, index, :INSERT
190
+ @recreate_buttons = true
102
191
  return tab
103
192
  end
193
+ ## remove given tab based on index
194
+ # This does not unbind the key mapping, FIXME
195
+ # Currently, can be invoked by 'dd' over highlighted button
196
+ # XXX can append to deleted_tabs, then on insert or paste insert with splat.
197
+ def remove_tab index
198
+ @recreate_buttons = true
199
+ $log.debug " inside remove_tab with #{index}, #{@tabs.size} "
200
+ @deleted_tab = @tabs.delete_at(index) unless @tabs.size < index
201
+ # note this is the index it was at.
202
+ fire_event @deleted_tab, index, :DELETE
203
+ end
204
+ ##
205
+ # Move this fun stuff to a util class. TODO
206
+ # If tab deleted accidentally, undelete it
207
+ # Okay, i just can stop myself from having a little fun
208
+ def undelete_tab
209
+ return unless @deleted_tab
210
+ @recreate_buttons = true
211
+ @tabs.insert(@deleted_tab.index, @deleted_tab)
212
+ fire_event @deleted_tab, @deleted_tab.index, :INSERT
213
+ @deleted_tab = nil
214
+ $log.debug " undelete over #{@tabs.size} "
215
+ end
216
+ def paste_tab pos
217
+ return unless @deleted_tab
218
+ ix = highlighted_tab_index
219
+ return if ix == -1
220
+ @recreate_buttons = true
221
+ @deleted_tab.index = ix + pos
222
+ @tabs.insert(@deleted_tab.index, @deleted_tab)
223
+ fire_event @deleted_tab, @deleted_tab.index, :INSERT
224
+ @deleted_tab = nil
225
+ $log.debug " paste over #{@tabs.size} #{ix} + #{pos} "
226
+ end
227
+
228
+ ##
229
+ # prompts for a new label for a tab - taking care of mnemonics if ampersand present
230
+ # Currently, mapped to 'C' and 'cw' when cursor is on a label
231
+ # Perhaps some of this kind of utility stuff needs to go into a util class.
232
+ #
233
+ def change_label
234
+ ix = highlighted_tab_index
235
+ return if ix < 0
236
+ prompt = "Enter new label: "
237
+ label = @buttons[ix].text
238
+ config = {}
239
+ config[:default] = label.dup
240
+ maxlen = 10
241
+ ret, str = rbgetstr(@graphic, $error_message_row, $error_message_col, prompt, maxlen, config)
242
+ if ret == 0 and str != "" and str != label
243
+ @tabs[ix].text = str
244
+ @buttons[ix].text(str)
245
+ @recreate_buttons = true
246
+ end
247
+ end
248
+ ##
249
+ # returns the index of the tab cursor is on (not the one that is selected)
250
+ # @return [0..] index, or -1 if some error
251
+ def highlighted_tab_index
252
+ @form.widgets.each_with_index{ |w, ix|
253
+ return ix if w.state == :HIGHLIGHTED
254
+ }
255
+ return -1
256
+ end
257
+ def selected_tab_index
258
+ @form.widgets.each_with_index{ |w, ix|
259
+ return ix if w.selected?
260
+ }
261
+ return -1
262
+ end
263
+ ## remove all tabs
264
+ def remove_all
265
+ if !@buttons.empty?
266
+ @buttons.each {|e| @form.remove_widget(e) }
267
+ end
268
+ @buttons = []
269
+ @tabs = []
270
+ @recreate_buttons = true
271
+ end
272
+
273
+ ## return a form for use by program - if you want to put multiple items
274
+ # Otherwise just use add_component
275
+ # private - can't use externally
276
+ def configure_component component
277
+ #component.set_form @parent <<--- definitely NOT
278
+ component.form = @parent
279
+ component.rows_panned = component.cols_panned = 0
280
+ component.parent_component = self # added 2010-02-27 so offsets can go down ?
281
+ component.should_create_buffer = true
282
+ component.row = @row + TAB_ROW_OFFSET # 2
283
+ component.col = @col + TAB_COL_OFFSET
284
+
285
+ # current_form likely to be nil XXX
286
+ scr_top = component.row # for Pad, if Pad passed as in SplitPane
287
+ scr_left = component.col # for Pad, if Pad passed as in SplitPane
288
+ ho = TAB_ROW_OFFSET + 2 # 5
289
+ component.set_buffering(:target_window => @target_window || @parentwin, :form => @current_form, :bottom => @height-ho, :right => @width-2, :screen_top => scr_top, :screen_left => scr_left)
290
+ # if left nil, then we expand the comp
291
+ component.height ||= @height - (ho - 1) # 1 keeps lower border inside by 1
292
+ component.width ||= @width - 0 # 0 keeps it flush on right border
293
+
294
+
295
+ end
296
+ ## create a form for tab, if multiple components are to be placed inside tab.
297
+ # Tabbedpane has no control over placement and width etc of what's inside a form
298
+ def form tab
299
+ if tab.form.nil?
300
+ @forms << create_tab_form(tab)
301
+ tab.form = @forms.last
302
+ end
303
+ return tab.form
304
+ end
305
+
306
+ ## returns the index of the current / selected tab
307
+ ## @returns 0.. index of selected tab
308
+ def selected_tab_index
309
+ @tabs.index(@current_tab)
310
+ end
311
+
104
312
  # private
105
313
  def variable_set var, val
106
314
  var = "@#{var}"
@@ -117,128 +325,240 @@ module RubyCurses
117
325
  end
118
326
  instance_eval &block if block_given?
119
327
  end
328
+ ## this is a really wierd repaint method.
329
+ # First time it creates the TP window/form which contains the buttons.
330
+ # In future calls it really doesn't do anything.
331
+ # Deos it have nothing to paint, no borders to redraw, no repaint_required ???
120
332
  def repaint
333
+ $log.debug " tabbedpane repaint "
121
334
  @window || create_window
335
+ _recreate_buttons if @recreate_buttons
336
+ $log.debug " tabbedpane repaint #{@window.name} "
122
337
  @window.show
338
+ #x set_buffer_modified()
123
339
  end
124
340
  def show
125
341
  repaint
126
342
  end
127
- def create_window
128
- # first create the main top window with the tab buttons on it.
129
- @layout = { :height => @height, :width => @width, :top => @row, :left => @col }
130
- @window = VER::Window.new(@layout)
131
- @form = RubyCurses::Form.new @window
132
- @form.navigation_policy = :NON_CYCLICAL
133
- @current_form = @form
134
- @window.bkgd(Ncurses.COLOR_PAIR($datacolor));
135
- @window.box( 0, 0);
136
- @window.wrefresh
137
- Ncurses::Panel.update_panels
138
- col = 1
343
+ ## recreate all buttons
344
+ # We call this if even one is added : adv is we can space out accordinagly if the numbers increase
345
+ # We could also expand the pad here.
346
+ # Test it out with removing tabs to.
347
+ # XXX have to remove buttons from the form
348
+ def _recreate_buttons
349
+ $log.debug " inside recreate_buttons: #{@tabs.size} "
350
+ r = @row
351
+ col = @col + 1
352
+ @buttons ||= []
353
+ if !@buttons.empty?
354
+ @buttons.each {|e| @form.remove_widget(e) }
355
+ end
139
356
  @buttons = []
357
+ button_gap = 4
358
+ # the next line necessitates a clear on the pad
359
+ # button_gap = 1 if @tabs.size > 6 # quick dirty fix, we need something that checks fit
360
+ # we may also need to truncate text to fit
361
+
362
+ @buttonpad.wclear
140
363
  ## create a button for each tab
141
- $tabradio = Variable.new
364
+ $tabradio = Variable.new # so we know which is highlighted
142
365
  @tabs.each do |tab|
143
366
  text = tab.text
367
+ $log.debug " TABS EACH #{text} "
144
368
  @buttons << RubyCurses::TabbedButton.new(@form) do
145
369
  variable $tabradio
146
370
  text text
147
371
  name text
148
372
  value text
149
- row 1
373
+ row r + 1
150
374
  col col
151
375
  end
152
- col += text.length+4
153
- # @forms << create_tab_form(tab)
154
- # form = @forms.last
376
+ col += text.length + button_gap
377
+ # if col exceeds pad_w then we need to expand pad
378
+ # but here we don't know that a pad is being used
379
+ $log.debug " button col #{col} "
155
380
  form = tab.form
156
- form.window = @window if form.window.nil? ## XXX
157
- panel = form.window.panel
158
- @buttons.last.command { Ncurses::Panel.top_panel(panel)
159
- Ncurses::Panel.update_panels();
160
- Ncurses.doupdate();
161
- form.repaint
162
- @current_form = form
163
- @current_tab = form
381
+ form.set_parent_buffer(@window) if form
382
+
383
+ b = @buttons.last
384
+ b.command(b) {
385
+ $log.debug " calling display form from button press #{b.name} #{b.state} "
386
+ # form.rep essentially sees that buttons get correct attributes
387
+ # when triggering M-<char>. This button should get highlighted.
388
+ tab.repaint
389
+ button_form_repaint #( b.state == :HIGHLIGHTED )
390
+ if @display_tab_on_traversal
391
+ # set as old tab so ONLY on going down this becomes current_tab
392
+ @old_tab = tab
393
+ else
394
+ # next line means next key is IMMED taken by the tab not main form
395
+ @current_tab = tab
396
+ end
397
+ fire_event tab, tab.index, :OPEN
164
398
  }
165
-
166
399
  end
167
- create_buttons
400
+ @recreate_buttons = false
401
+ # make the buttons visible now, not after next handle_key
168
402
  @form.repaint
169
403
  end
170
- def display_form form
171
- panel = form.window.panel
172
- Ncurses::Panel.top_panel(panel)
173
- Ncurses::Panel.update_panels();
174
- Ncurses.doupdate();
175
- form.repaint
404
+ ## This form is for the tabbed buttons on top
405
+ def create_window
406
+ set_buffer_modified() # required still ??
407
+ # first create the main top window with the tab buttons on it.
408
+ $log.debug " TPane create_buff Top #{@row}, Left #{@col} H #{@height} W #{@width} "
409
+ #$log.debug " parentwin #{@parentwin.left} #{@parentwin.top} "
410
+
411
+ r = @row
412
+ c = @col
413
+ @form = ScrollForm.new(@parentwin)
414
+ @form.set_layout(1, @width, @row+1, @col+1)
415
+ @form.display_h = 1
416
+ @form.display_w = @width-3
417
+ @buttonpad = @form.create_pad
418
+
419
+
420
+ ## We will use the parent window, and not a pad. We will write absolute coordinates.
421
+ @window = @parentwin
422
+ color = $datacolor
423
+ # border around button bar. should this not be in scrollform as a border ? XXX
424
+ @window.print_border @row, @col, 2, @width, color #, Ncurses::A_REVERSE
425
+ @buttonpad.name = "Window::TPTOPPAD" # 2010-02-02 20:01
426
+ @form.name = "Form::TPTOPFORM"
427
+ $log.debug("TP WINDOW TOP ? PAD MAIN FORM W:#{@window.name}, F:#{@form.name} ")
428
+ @form.parent_form = @parent ## 2010-01-21 15:55 TRYING OUT BUFFERED
429
+ @form.navigation_policy = :NON_CYCLICAL
430
+ #xx @current_form = @form
431
+ #xx color = $datacolor
432
+ #xx @window.print_border @row, @col, @height-1, @width, color #, Ncurses::A_REVERSE
433
+
434
+ Ncurses::Panel.update_panels
435
+ _recreate_buttons
436
+
437
+ button_form_repaint true
438
+ @window.wrefresh ## ADDED 2009-11-02 23:29
439
+ @old_tab = @tabs.first
440
+ @buttons.first().fire unless @buttons.empty? # make the first form active to start with.
441
+ end
442
+ def button_form_repaint flag = true
443
+ $log.debug " INSIDE button_form_repaint #{flag} "
444
+ #if flag
445
+ #@form.repaint if flag # This paints the outer form not inner
446
+ #@buttonpad.mvwaddch(2, 0, Ncurses::ACS_LTEE) # beautify the corner 2010-02-06 19:35
447
+ #@buttonpad.mvwaddch(2, @width-1, Ncurses::ACS_RTEE)
448
+ #end
449
+ #ret = @buttonpad.prefresh(0,0, @row+0, @col+0, @row+@height, @col+@width)
450
+ #$log.debug " prefresh error buttonpad 2 " if ret < 0
451
+ #@buttonpad.modified = false
452
+ if flag
453
+ # repaint form and refresh pad
454
+ @form.repaint
455
+ else
456
+ # only refresh pad
457
+ @form.prefresh
458
+ end
176
459
  end
460
+ ##
461
+ # This creates a form for the tab, in case we wish to put many components in it.
462
+ # Else just pass single components in add_tab.
463
+ # @params tab tab just created for which a form is required
464
+ # @return form - a pad based form
177
465
  def create_tab_form tab
178
- layout = { :height => @height-2, :width => @width, :top => @row+2, :left => @col }
179
- window = VER::Window.new(layout)
466
+ mtop = 0
467
+ mleft = 0
468
+ bottom_offset = 2 # 0 will overwrite bottom line, 1 will make another line for inner form
469
+ layout = { :height => @height-(mtop+bottom_offset), :width => @width, :top => mtop, :left => mleft }
470
+ # create a pad but it must behave like a window at all times 2009-10-25 12:25
471
+ window = VER::Pad.create_with_layout(layout)
472
+
180
473
  form = RubyCurses::Form.new window
474
+ $log.debug " pad created in TP create_tab_form: #{window.name} , form #{form.name} "
475
+ $log.debug " hwtl: #{layout[:height]} #{layout[:width]} #{layout[:top]} #{layout[:left]} "
476
+ ## added 2010-01-21 15:46 to pass cursor up
477
+ form.parent_form = @parent
181
478
  form.navigation_policy = :NON_CYCLICAL
182
479
  window.bkgd(Ncurses.COLOR_PAIR($datacolor));
183
480
  window.box( 0, 0);
184
- window.mvprintw(1,1, tab.text.tr('&', ''))
185
- ##window.wrefresh
186
- ##Ncurses::Panel.update_panels
481
+ window.mvwaddch(0, 0, Ncurses::ACS_LTEE) # beautify the corner 2010-02-06 19:35
482
+ window.mvwaddch(0, @width-1, Ncurses::ACS_RTEE)
483
+
484
+ # XXX TODO this wastes space we should ditch it.
485
+ ## this prints the tab name on top left
486
+ window.mvprintw(1,1, tab.text.tr('&', '')) if @print_subheader
487
+ window.name = "Tab::TAB-#{tab.text}" # 2010-02-02 19:59
488
+ form.name = "Form::TAB-#{tab.text}" # 2010-02-02 19:59
489
+ form.add_cols=@col
490
+ form.add_rows=@row
187
491
  return form
188
492
  end
189
- def handle_keys
190
- begin
191
- while (( ch=@window.getchar()) != 999)
192
- if ch == ?\C-q
193
- @selected_index = -1 # this signifies cancel by ?C-q
194
- @stop = true
195
- return
196
- end
197
- return if @stop
198
- @current_form ||= @form
199
- ret = @current_form.handle_key(ch)
493
+ ##
494
+ # added 2009-10-08 19:39 so it can be placed in a form
495
+ # @form is the top button form
496
+ # XXX stop this nonsense about current_form and current_tab
497
+ # TP should only be concerned with tabs. what happens inside is none of its business
498
+ def handle_key(ch)
499
+ @current_tab ||= @form # first we cycle buttons
500
+ $log.debug " handle_key in tabbed pane got : #{ch}"
501
+ # needs to go to component
502
+ ret = @current_tab.handle_key(ch)
503
+ $log.debug " -- form.handle_key in tabbed pane got ret : #{ret} , #{@current_tab} , #{ch} "
504
+
505
+ # components will usually return UNHANDLED for a tab or btab
506
+ # We need to convert it so the main form can use it
507
+ if @current_tab != @form
508
+ if ret == :UNHANDLED
509
+ if ch == 9 #or ch == KEY_DOWN
510
+ ret = :NO_NEXT_FIELD
511
+ elsif ch == 353 #or ch == KEY_UP # btab
512
+ ret = :NO_PREV_FIELD
513
+ end
514
+ end
515
+ else
516
+ # key down pressed in top form, go to tab
517
+ if ch == KEY_DOWN
518
+ ret = :NO_NEXT_FIELD
519
+ end
520
+ end
521
+
200
522
  case ret
201
523
  when :NO_NEXT_FIELD
202
- if @current_form != @form
203
- @current_form = @form
204
- #@current_form.select_field -1
205
- @current_form.req_first_field
206
- #ret = @current_form.handle_key(ch)
524
+ if @current_tab != @form
525
+ ## if no next field on a subform go to first button of main form
526
+ @old_tab = @current_tab
527
+ @current_tab = @form
528
+ @form.req_first_field
529
+ #@form.select_next_field
207
530
  else
208
- if !@current_tab.nil?
209
- @current_form = @current_tab
210
- display_form @current_form
211
- @current_form.req_first_field
212
- #@current_form.select_field -1
213
- #ret = @current_form.handle_key(ch)
531
+ # on top button panel - no more buttons, go to tabs first field
532
+ if @old_tab # in case of empty tabbed pane old_tab was nil
533
+ @current_tab = @old_tab
534
+ @current_tab.set_focus :FIRST
214
535
  end
215
536
  end
216
537
  when :NO_PREV_FIELD
217
- if @current_form != @form
218
- $log.debug " 1 no prev field - going to button "
219
- @current_form = @form
220
- @current_form.req_last_field
538
+ if @current_tab != @form
539
+ $log.debug "TP 1 no prev field - going to last button "
540
+ @old_tab = @current_tab
541
+ @current_tab = @form
542
+ @form.req_last_field
221
543
  else
222
- if !@current_tab.nil?
223
- @current_form = @current_tab
224
- display_form @current_form
225
- @current_form.req_last_field
544
+ # on top button panel - no prev buttons, go to tabs last field
545
+ if @old_tab # in case of one tab
546
+ @current_tab = @old_tab
547
+ @current_tab.set_focus :LAST
226
548
  end
227
549
  end
228
550
  when :UNHANDLED
229
551
  $log.debug " unhandled in tabbed pane #{ch}"
230
552
  ret = @form.process_key ch, self # field
231
- @form.repaint
232
- #return :UNHANDLED if ret == :UNHANDLED
553
+
554
+ return ret if ret == :UNHANDLED
555
+ end
556
+ if @buttonpad.modified
557
+ button_form_repaint
233
558
  end
234
- return if @stop
235
- @current_form.window.wrefresh
236
- @window.refresh
237
- end
238
- ensure
239
- destroy
240
- end
241
559
  end
560
+ ##
561
+ # ensure that the pads are being destroyed, although we've not found a way.
242
562
  def destroy
243
563
  @window.destroy
244
564
  @forms.each { |f| w = f.window; w.destroy unless w.nil? }
@@ -291,31 +611,339 @@ module RubyCurses
291
611
  width = @layout[:width]
292
612
  return (width-textlen)/2
293
613
  end
614
+ def fire_event tab, index, event
615
+ # experimenting with structs, earlier we've used classes
616
+ if @tabbedpane_event.nil?
617
+ @tabbedpane_event = Event.new
618
+ end
619
+ @tabbedpane_event.tab = tab
620
+ @tabbedpane_event.index = index
621
+ @tabbedpane_event.event = event
622
+ fire_handler event, @tabbedpane_event
623
+ end
294
624
 
295
625
  ##
296
626
  # nested class tab
627
+ # A user created tab, with its own form
297
628
  class Tab
298
- attr_reader :text
629
+ attr_accessor :text
299
630
  attr_reader :config
631
+ attr_reader :component
300
632
  attr_accessor :form
301
- def initialize text, aconfig={}, &block
633
+ attr_accessor :parent_component
634
+ attr_accessor :index
635
+ def initialize text, parent_component, aconfig={}, &block
302
636
  @text = text
303
637
  @config = aconfig
638
+ @parent_component = parent_component
304
639
  @config.each_pair { |k,v| variable_set(k,v) }
305
640
  instance_eval &block if block_given?
306
641
  end
642
+ ## add a single component to the tab
643
+ # Calling this a second time will overwrite the existing component
644
+ def component=(component)
645
+ raise "Component cannot be null" unless component
646
+ raise "Component already associated with a form. Do not pass form in constructor." unless component.form.nil?
647
+ $log.debug " calling configure component "
648
+ @parent_component.configure_component component
649
+ @component = component
650
+ end
651
+ def remove_component
652
+ @component = nil
653
+ end
307
654
  # private
308
655
  def variable_set var, val
309
656
  var = "@#{var}"
310
657
  instance_variable_set(var, val)
311
658
  end
312
- def repaint
313
-
314
-
659
+ # tab should handle key instead of TP.
660
+ # Pass to component or form
661
+ def handle_key ch
662
+ kh = @component || @form
663
+ ret = kh.handle_key(ch)
664
+ # forms seem to returning a nil when the pad has been updated. We need to copy it
665
+ ret ||= 0
666
+ if ret == 0
667
+ @component.repaint if @component
668
+ display_form false if @form
669
+ end
670
+ # XXX i need to call repaint of compoent if updated !!
671
+ return ret
672
+ end
673
+ def repaint # Tab
674
+ if @form
675
+ display_form
676
+ elsif @component
677
+ # we ask the component to paint its buffer only, no actual repainting
678
+ redraw
679
+ else
680
+ # pls use tp.form(tab) to get form explicity.
681
+ # It could come here if tab precreated and user is yet to assign a component.
682
+ # or has removed component and not yet set a new one.
683
+ $log.error "Got neither component nor form."
684
+ $log.error "Programmer error. A change in Tabbedpane requires you to create form explicitly using form = tpane.form(tab) syntax"
685
+ end
686
+ end
687
+ # force a redraw of a component when tabs changed
688
+ def redraw
689
+ # this kinda stuff should be inside widget or some util class
690
+ c = @component
691
+ if c.is_double_buffered?
692
+ c.set_buffer_modified
693
+ c.buffer_to_window
694
+ else
695
+ # force a repaint, if not buffered object e.g scrollpane.
696
+ $log.debug " TP: forcing repaint of non-buffered object #{c} "
697
+ c.repaint_all
698
+ c.repaint
699
+ end
315
700
  end
701
+ ## Set focus on a component or form field when a user has tabbed off the last or first button
702
+ def set_focus first_last
703
+ if !@form.nil?
704
+ # move to first field of existing form
705
+ #@current_form = @current_tab.form # 2010-02-27 20:22
706
+ $log.debug " calling display form from handle_key NO_NEXT_FIELD: #{first_last} "
707
+ first_last == :FIRST ? @form.req_first_field : @form.req_last_field
708
+ display_form
709
+ else
710
+ # move to component
711
+ #@current_form = @current_tab.component # temp HACK 2010-02-27 23:24
712
+ @component.set_form_row
713
+ @component.set_form_col
714
+ end
715
+ end
716
+ # On a tabbed button press, this will display the relevant form
717
+ # On why I am directyl calling copywin and not using copy_pad_to_win etc
718
+ #+ those require setting top and left. However, while printing a pad, top and left are reduced and so
719
+ #+ must be absolute r and c. But inside TP, objects have a relative coord. So the print functions
720
+ #+ were failing silently, and i was wondering why nothing was printing.
721
+ # XXX move this tab in tab.repaint and let tab decide based on component or form
722
+ # if component then pad = component.get_buffer
723
+ def display_form flag = true
724
+ return if @form.nil?
725
+ form = @form
726
+ if form.is_a? RubyCurses::Form # tempo XXX since there are components
727
+ pad = form.window
728
+ else
729
+ return
730
+ pad = form.get_buffer() # component
731
+ end
732
+ pc = @parent_component
733
+ form.repaint if flag # added 2009-11-03 23:27 paint widgets in inside form
734
+ $log.debug " TP display form before pad copy: #{pad.name}, set_backing: #{form}: #{form.name} parent: #{@parent_component} : #{pc.row} , #{pc.col}. #{pc.height} , #{pc.width}: repaint flag #{flag} "
735
+ ret = -1
736
+ pminr = pminc = 0
737
+ r = pc.row + 2
738
+ c = pc.col + 0
739
+ border_width = 0
740
+ maxr = pc.height() - 3
741
+ maxc = pc.width() - 1
742
+ $log.debug " ret = pad.copywin(pc.window.get_window, #{pminr}, #{pminc}, #{r}, #{c}, r+ #{maxr} - border_width, c+ #{maxc} -border_width,0). W:#{pc.window}, #{pc.window.get_window} "
743
+ ret = pad.copywin(pc.window.get_window, pminr, pminc, r, c, r+maxr-border_width, c+maxc-border_width,0)
744
+ $log.debug " display form after pad copy #{ret}. #{form.name} "
316
745
  end
317
746
 
747
+ end # class Tab
748
+
318
749
  end # class Tabbedpane
319
750
 
751
+ ## An extension of Form that only displays and focuses on visible widgets
752
+ # This is minimal, and is being expanded upon as a separate class in rscrollform.rb
753
+ #
754
+ class ScrollForm < RubyCurses::Form
755
+ attr_accessor :pmincol # advance / scroll columns
756
+ attr_accessor :pminrow # advance / scroll rows (vertically)
757
+ attr_accessor :display_w
758
+ attr_accessor :display_h
759
+ attr_accessor :scroll_ctr
760
+ attr_reader :orig_top, :orig_left
761
+ attr_reader :window
762
+ attr_accessor :name
763
+ attr_reader :cols_panned, :rows_panned
764
+ def initialize win, &block
765
+ @target_window = win
766
+ super
767
+ @pminrow = @pmincol = 0
768
+ @scroll_ctr = 2
769
+ @cols_panned = @rows_panned = 0
770
+ end
771
+ def set_layout(h, w, t, l)
772
+ @pad_h = h
773
+ @pad_w = w
774
+ @top = @orig_top = t
775
+ @left = @orig_left = l
776
+ end
777
+ def create_pad
778
+ r = @top
779
+ c = @left
780
+ layout = { :height => @pad_h, :width => @pad_w, :top => r, :left => c }
781
+ @window = VER::Pad.create_with_layout(layout)
782
+ #@window = @parentwin
783
+ #@form = RubyCurses::Form.new @buttonpad
784
+ @window.name = "Pad::ScrollPad" # 2010-02-02 20:01
785
+ @name = "Form::ScrollForm"
786
+ return @window
787
+ end
788
+ ## ScrollForm handle key, scrolling
789
+ def handle_key ch
790
+ $log.debug " inside ScrollForm handlekey #{ch} "
791
+ # do the scrolling thing here top left prow and pcol of pad to be done
792
+ # # XXX TODO check whether we can scroll before incrementing esp cols_panned etc
793
+ case ch
794
+ when ?\M-l.getbyte(0)
795
+ return false if !validate_scroll_col(@pmincol + @scroll_ctr)
796
+ @pmincol += @scroll_ctr # some check is required or we'll crash
797
+ @cols_panned -= @scroll_ctr
798
+ $log.debug " handled ch M-l in ScrollForm"
799
+ @window.modified = true
800
+ return 0
801
+ when ?\M-h.getbyte(0)
802
+ return false if !validate_scroll_col(@pmincol - @scroll_ctr)
803
+ @pmincol -= @scroll_ctr # some check is required or we'll crash
804
+ @cols_panned += @scroll_ctr
805
+ $log.debug " handled ch M-h in ScrollForm"
806
+ @window.modified = true
807
+ return 0
808
+ when ?\M-n.getbyte(0)
809
+ return false if !validate_scroll_row(@pminrow + @scroll_ctr)
810
+ @pminrow += @scroll_ctr # some check is required or we'll crash
811
+ @rows_panned -= @scroll_ctr
812
+ @window.modified = true
813
+ return 0
814
+ when ?\M-p.getbyte(0)
815
+ return false if !validate_scroll_row(@pminrow - @scroll_ctr)
816
+ @pminrow -= @scroll_ctr # some check is required or we'll crash
817
+ @rows_panned += @scroll_ctr
818
+ @window.modified = true
819
+ return 0
820
+ end
821
+
822
+ super
823
+ end
824
+ def repaint
825
+ $log.debug " scrollForm repaint calling parent"
826
+ super
827
+ prefresh
828
+ @window.modified = false
829
+ end
830
+ def prefresh
831
+ ## reduce so we don't go off in top+h and top+w
832
+ $log.debug " start ret = @buttonpad.prefresh( #{@pminrow} , #{@pmincol} , #{@top} , #{@left} , top + #{@display_h} left + #{@display_w} ) "
833
+ if @pminrow + @display_h > @orig_top + @pad_h
834
+ $log.debug " if #{@pminrow} + #{@display_h} > #{@orig_top} +#{@pad_h} "
835
+ $log.debug " ERROR 1 "
836
+ #return -1
837
+ end
838
+ if @pmincol + @display_w > @orig_left + @pad_w
839
+ $log.debug " if #{@pmincol} + #{@display_w} > #{@orig_left} +#{@pad_w} "
840
+ $log.debug " ERROR 2 "
841
+ return -1
842
+ end
843
+ # actually if there is a change in the screen, we may still need to allow update
844
+ # but ensure that size does not exceed
845
+ if @top + @display_h > @orig_top + @pad_h
846
+ $log.debug " if #{@top} + #{@display_h} > #{@orig_top} +#{@pad_h} "
847
+ $log.debug " ERROR 3 "
848
+ return -1
849
+ end
850
+ if @left + @display_w > @orig_left + @pad_w
851
+ $log.debug " if #{@left} + #{@display_w} > #{@orig_left} +#{@pad_w} "
852
+ $log.debug " ERROR 4 "
853
+ return -1
854
+ end
855
+ # maybe we should use copywin to copy onto @target_window
856
+ $log.debug " ret = @buttonpad.prefresh( #{@pminrow} , #{@pmincol} , #{@top} , #{@left} , #{@top} + #{@display_h}, #{@left} + #{@display_w} ) "
857
+ omit = 0
858
+ # this works but if want to avoid copying border
859
+ ret = @window.prefresh(@pminrow, @pmincol, @top, @left, @top + @display_h , @left + @display_w)
860
+
861
+ $log.debug " ret = #{ret} "
862
+ # need to refresh the form after repaint over
863
+ end
864
+ def validate_scroll_row minrow
865
+ return false if minrow < 0
866
+ if minrow + @display_h > @orig_top + @pad_h
867
+ $log.debug " if #{minrow} + #{@display_h} > #{@orig_top} +#{@pad_h} "
868
+ $log.debug " ERROR 1 "
869
+ return false
870
+ end
871
+ return true
872
+ end
873
+ def validate_scroll_col mincol
874
+ return false if mincol < 0
875
+ if mincol + @display_w > @orig_left + @pad_w
876
+ $log.debug " if #{mincol} + #{@display_w} > #{@orig_left} +#{@pad_w} "
877
+ $log.debug " ERROR 2 "
878
+ return false
879
+ end
880
+ return true
881
+ end
882
+ # when tabbing through buttons, we need to account for all that panning/scrolling goin' on
883
+ def setrowcol r, c
884
+ # aha ! here's where i can check whether the cursor is falling off the viewable area
885
+ if c+@cols_panned < @orig_left
886
+ # this essentially means this widget (button) is not in view, its off to the left
887
+ $log.debug " setrowcol OVERRIDE #{c} #{@cols_panned} < #{@orig_left} "
888
+ $log.debug " aborting settrow col for now"
889
+ return
890
+ end
891
+ if c+@cols_panned > @orig_left + @display_w
892
+ # this essentially means this button is not in view, its off to the right
893
+ $log.debug " setrowcol OVERRIDE #{c} #{@cols_panned} > #{@orig_left} + #{@display_w} "
894
+ $log.debug " aborting settrow col for now"
895
+ return
896
+ end
897
+ super r+@rows_panned, c+@cols_panned
898
+ end
899
+ def add_widget w
900
+ super
901
+ #$log.debug " inside add_widget #{w.name} pad w #{@pad_w} #{w.col} "
902
+ if w.col >= @pad_w
903
+ @pad_w += 10 # XXX currently just a guess value, we need length and maybe some extra
904
+ @window.wresize(@pad_h, @pad_w)
905
+ end
906
+ end
907
+ ## Is a component visible, typically used to prevent traversal into the field
908
+ # @returns [true, false] false if components has scrolled off
909
+ def visible? component
910
+ r, c = component.rowcol
911
+ return false if c+@cols_panned < @orig_left
912
+ return false if c+@cols_panned > @orig_left + @display_w
913
+ # XXX TODO for rows UNTESTED for rows
914
+ return false if r + @rows_panned < @orig_top
915
+ return false if r + @rows_panned > @orig_top + @display_h
916
+
917
+ return true
918
+ end
919
+ # returns index of first visible component. Currently using column index
920
+ # I am doing this for horizontal scrolling presently
921
+ # @return [index, -1] -1 if none visible, else index/offset
922
+ def first_visible_component_index
923
+ @widgets.each_with_index do |w, ix|
924
+ return ix if visible?(w)
925
+ end
926
+ return -1
927
+ end
928
+ def last_visible_component_index
929
+ ret = -1
930
+ @widgets.each_with_index do |w, ix|
931
+ $log.debug " reverse last vis #{ix} , #{w} : #{visible?(w)} "
932
+ ret = ix if visible?(w)
933
+ end
934
+ return ret
935
+ end
936
+ def req_first_field
937
+ select_field(first_visible_component_index)
938
+ end
939
+ def req_last_field
940
+ select_field(last_visible_component_index)
941
+ end
942
+ def focusable?(w)
943
+ w.focusable and visible?(w)
944
+ end
945
+
946
+ end # class ScrollF
947
+
320
948
 
321
949
  end # module