rbcurse-extras 0.0.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (38) hide show
  1. data/README.md +75 -0
  2. data/VERSION +1 -0
  3. data/examples/data/list.txt +300 -0
  4. data/examples/data/lotr.txt +12 -0
  5. data/examples/data/table.txt +36 -0
  6. data/examples/data/tasks.txt +27 -0
  7. data/examples/data/unix1.txt +21 -0
  8. data/examples/inc/qdfilechooser.rb +70 -0
  9. data/examples/inc/rfe_renderer.rb +121 -0
  10. data/examples/newtabbedwindow.rb +100 -0
  11. data/examples/rfe.rb +1236 -0
  12. data/examples/test2.rb +670 -0
  13. data/examples/testeditlist.rb +78 -0
  14. data/examples/testtable.rb +270 -0
  15. data/examples/testvimsplit.rb +141 -0
  16. data/lib/rbcurse/extras/include/celleditor.rb +112 -0
  17. data/lib/rbcurse/extras/include/checkboxcellrenderer.rb +57 -0
  18. data/lib/rbcurse/extras/include/comboboxcellrenderer.rb +30 -0
  19. data/lib/rbcurse/extras/include/defaultlistselectionmodel.rb +79 -0
  20. data/lib/rbcurse/extras/include/listkeys.rb +37 -0
  21. data/lib/rbcurse/extras/include/listselectable.rb +144 -0
  22. data/lib/rbcurse/extras/include/tableextended.rb +40 -0
  23. data/lib/rbcurse/extras/widgets/horizlist.rb +203 -0
  24. data/lib/rbcurse/extras/widgets/menutree.rb +63 -0
  25. data/lib/rbcurse/extras/widgets/multilinelabel.rb +142 -0
  26. data/lib/rbcurse/extras/widgets/rcomboedit.rb +256 -0
  27. data/lib/rbcurse/extras/widgets/rlink.rb.moved +27 -0
  28. data/lib/rbcurse/extras/widgets/rlistbox.rb +1247 -0
  29. data/lib/rbcurse/extras/widgets/rmenulink.rb.moved +21 -0
  30. data/lib/rbcurse/extras/widgets/rmulticontainer.rb +304 -0
  31. data/lib/rbcurse/extras/widgets/rmultisplit.rb +722 -0
  32. data/lib/rbcurse/extras/widgets/rmultitextview.rb +306 -0
  33. data/lib/rbcurse/extras/widgets/rpopupmenu.rb +755 -0
  34. data/lib/rbcurse/extras/widgets/rtable.rb +1758 -0
  35. data/lib/rbcurse/extras/widgets/rvimsplit.rb +800 -0
  36. data/lib/rbcurse/extras/widgets/table/tablecellrenderer.rb +86 -0
  37. data/lib/rbcurse/extras/widgets/table/tabledatecellrenderer.rb +98 -0
  38. metadata +94 -0
@@ -0,0 +1,256 @@
1
+ =begin
2
+ * Name: combo box
3
+ * Description:
4
+ * Author: rkumar
5
+
6
+ --------
7
+ * Date: 2008-12-16 22:03
8
+ * License:
9
+ Same as Ruby's License (http://www.ruby-lang.org/LICENSE.txt)
10
+
11
+ TODO:
12
+ Simplify completely. we don't need to use popup list, use something simpler do
13
+ we can control keys.
14
+ Keys: ignore down arrow in field. Use space to popup and space to select from popup.
15
+ Or keep that as default.
16
+ That v character does not position correctly if label used.
17
+ =end
18
+ require 'rbcurse'
19
+ require 'rbcurse/extras/widgets/rlistbox'
20
+
21
+ include RubyCurses
22
+ module RubyCurses
23
+ extend self
24
+
25
+ # TODO :
26
+ # i no longer use values, i now use "list" or better "list_data_model"
27
+ # try to make it so values gets converted to list.
28
+ # NOTE: 2010-10-01 13:25 spacebar and enter will popup in addition to Alt-Down
29
+ class ComboBoxEdit < Field
30
+ include RubyCurses::EventHandler
31
+ dsl_accessor :list_config
32
+ dsl_accessor :insert_policy # NO_INSERT, INSERT_AT_TOP, INSERT_AT_BOTTOM, INSERT_AT_CURRENT
33
+ # INSERT_AFTER_CURRENT, INSERT_BEFORE_CURRENT,INSERT_ALPHABETICALLY
34
+
35
+ attr_accessor :current_index
36
+ # the symbol you want to use for combos
37
+ attr_accessor :COMBO_SYMBOL
38
+ attr_accessor :show_symbol # show that funny symbol after a combo to signify its a combo
39
+ dsl_accessor :arrow_key_policy # :IGNORE :NEXT_ROW :POPUP
40
+
41
+ def initialize form, config={}, &block
42
+ @arrow_key_policy = :ignore
43
+ @show_symbol = true
44
+ @COMBO_SYMBOL = "v".ord # trying this out
45
+ super
46
+ @current_index ||= 0
47
+ # added if check since it was overriding set_buffer in creation. 2009-01-18 00:03
48
+ set_buffer @list[@current_index].dup if @buffer.nil? or @buffer.empty?
49
+ init_vars
50
+ @_events.push(*[:CHANGE, :ENTER_ROW, :LEAVE_ROW])
51
+ end
52
+ def init_vars
53
+ super
54
+ #@show_symbol = false if @label # commented out 2011-11-13 maybe it doesn't place properly if label
55
+ @COMBO_SYMBOL ||= FFI::NCurses::ACS_DARROW #GEQUAL # now this won't work since i've set it above
56
+ bind_key(KEY_UP) { previous_row }
57
+ bind_key(KEY_DOWN) { next_row }
58
+ end
59
+ def selected_item
60
+ @list[@current_index]
61
+ end
62
+ def selected_index
63
+ @current_index
64
+ end
65
+
66
+ ##
67
+ # convert given list to datamodel
68
+ def list alist=nil
69
+ return @list if alist.nil?
70
+ @list = RubyCurses::ListDataModel.new(alist)
71
+ end
72
+ ##
73
+ # set given datamodel
74
+ def list_data_model ldm
75
+ raise "Expecting list_data_model" unless ldm.is_a? RubyCurses::ListDataModel
76
+ @list = ldm
77
+ end
78
+ ##
79
+ # combo edit box key handling
80
+ # removed UP and DOWN and bound it, so it can be unbound
81
+ def handle_key(ch)
82
+ @current_index ||= 0
83
+ # added 2009-01-18 22:44 no point moving horiz or passing up to Field if not edit
84
+ if !@editable
85
+ if ch == KEY_LEFT or ch == KEY_RIGHT
86
+ return :UNHANDLED
87
+ end
88
+ end
89
+ case @arrow_key_policy
90
+ when :ignore
91
+ if ch == KEY_DOWN or ch == KEY_UP
92
+ return :UNHANDLED
93
+ end
94
+ when :popup
95
+ if ch == KEY_DOWN or ch == KEY_UP
96
+ popup
97
+ end
98
+ end
99
+ case ch
100
+ #when KEY_UP # show previous value
101
+ # previous_row
102
+ #when KEY_DOWN # show previous value
103
+ # next_row
104
+ # adding spacebar to popup combo, as in microemacs 2010-10-01 13:21
105
+ when 32, KEY_DOWN+ META_KEY # alt down
106
+ popup # pop up the popup
107
+ else
108
+ super
109
+ end
110
+ end
111
+ def previous_row
112
+ @current_index -= 1 if @current_index > 0
113
+ set_buffer @list[@current_index].dup
114
+ set_modified(true) ## ??? not required
115
+ fire_handler :ENTER_ROW, self
116
+ @list.on_enter_row self
117
+ end
118
+ def next_row
119
+ @current_index += 1 if @current_index < @list.length()-1
120
+ set_buffer @list[@current_index].dup
121
+ set_modified(true) ## ??? not required
122
+ fire_handler :ENTER_ROW, self
123
+ @list.on_enter_row self
124
+ end
125
+ ##
126
+ # calls a popup list
127
+ # TODO: should not be positioned so that it goes off edge
128
+ # user's customizations of list should be passed in
129
+ # The dup of listconfig is due to a tricky feature/bug.
130
+ # I try to keep the config hash and instance variables in synch. So
131
+ # this config hash is sent to popuplist which updates its row col and
132
+ # next time we pop up the popup row and col are zero.
133
+ #
134
+ #
135
+ # added dup in PRESS since editing edit field mods this
136
+ # on pressing ENTER, value set back and current_index updated
137
+ def popup
138
+ listconfig = (@list_config && @list_config.dup) || {}
139
+ dm = @list
140
+ # current item in edit box will be focussed when list pops up
141
+ #$log.debug "XXX POPUP: #{dm.selected_index} = #{@current_index}, value #{@buffer}"
142
+ # we are having some problms when using this in a list. it retains earlier value
143
+ _index = dm.index @buffer
144
+ dm.selected_index = _index # @current_index
145
+ poprow = @row+0 # one row below the edit box
146
+ popcol = @col
147
+ dlength = @display_length
148
+ f = self
149
+ @popup = RubyCurses::PopupList.new do
150
+ row poprow
151
+ col popcol
152
+ width dlength
153
+ list_data_model dm
154
+ list_selection_mode 'single'
155
+ relative_to f
156
+ list_config listconfig
157
+ bind(:PRESS) do |index|
158
+ f.set_buffer dm[index].dup
159
+ f.set_modified(true) if f.current_index != index
160
+ f.current_index = index
161
+ end
162
+ end
163
+ end
164
+
165
+ # Field putc advances cursor when it gives a char so we override this
166
+ def putc c
167
+ if c >= 0 and c <= 127
168
+ ret = putch c.chr
169
+ if ret == 0
170
+ addcol 1 if @editable
171
+ set_modified
172
+ end
173
+ end
174
+ return -1 # always ??? XXX
175
+ end
176
+ ##
177
+ # field does not give char to non-editable fields so we override
178
+ def putch char
179
+ @current_index ||= 0
180
+ if @editable
181
+ super
182
+ return 0
183
+ else
184
+ match = next_match(char)
185
+ set_buffer match unless match.nil?
186
+ fire_handler :ENTER_ROW, self
187
+ end
188
+ @modified = true
189
+ fire_handler :CHANGE, self # 2008-12-09 14:51 ???
190
+ 0
191
+ end
192
+ ##
193
+ # the sets the next match in the edit field
194
+ def next_match char
195
+ start = @current_index
196
+ start.upto(@list.length-1) do |ix|
197
+ if @list[ix][0,1].casecmp(char) == 0
198
+ return @list[ix] unless @list[ix] == @buffer
199
+ end
200
+ @current_index += 1
201
+ end
202
+ ## could not find, start from zero
203
+ @current_index = 0
204
+ start = [@list.length()-1, start].min
205
+ 0.upto(start) do |ix|
206
+ if @list[ix][0,1].casecmp(char) == 0
207
+ return @list[ix] unless @list[ix] == @buffer
208
+ end
209
+ @current_index += 1
210
+ end
211
+ @current_index = [@list.length()-1, @current_index].min
212
+ return nil
213
+ end
214
+ ##
215
+ # on leaving the listbox, update the combo/datamodel.
216
+ # we are using methods of the datamodel. Updating our list will have
217
+ # no effect on the list, and wont trigger events.
218
+ # Do not override.
219
+ def on_leave
220
+ if !@list.include? @buffer and !@buffer.strip.empty?
221
+ _insert_policy = @insert_policy || :INSERT_AT_BOTTOM
222
+ case _insert_policy
223
+ when :INSERT_AT_BOTTOM, :INSERT_AT_END
224
+ @list.append @buffer
225
+ when :INSERT_AT_TOP
226
+ @list.insert(0, @buffer)
227
+ when :INSERT_AFTER_CURRENT
228
+ @current_index += 1
229
+ @list.insert(@current_index, @buffer)
230
+
231
+ when :INSERT_BEFORE_CURRENT
232
+ #_index = @current_index-1 if @current_index>0
233
+ _index = @current_index
234
+ @list.insert(_index, @buffer)
235
+ when :INSERT_AT_CURRENT
236
+ @list[@current_index]=@buffer
237
+ when :NO_INSERT
238
+ ; # take a break
239
+ end
240
+ end
241
+ fire_handler :LEAVE, self
242
+ end
243
+
244
+ def repaint
245
+ super
246
+ c = @col + @display_length
247
+ if @show_symbol # 2009-01-11 18:47
248
+ # i have changed c +1 to c, since we have no right to print beyond display_length
249
+ @form.window.mvwaddch @row, c, @COMBO_SYMBOL # Ncurses::ACS_GEQUAL
250
+ @form.window.mvchgat(y=@row, x=c, max=1, Ncurses::A_REVERSE|Ncurses::A_UNDERLINE, $datacolor, nil)
251
+ end
252
+ end
253
+
254
+ end # class ComboBox
255
+
256
+ end # module
@@ -0,0 +1,27 @@
1
+ require 'rbcurse'
2
+ ##
3
+ module RubyCurses
4
+ class Link < Button
5
+ dsl_property :description
6
+
7
+
8
+ def initialize form, config={}, &block
9
+ super
10
+ @text_offset = 0
11
+ # haha we've never done this, pin the cursor up on 0,0
12
+ @col_offset = -1
13
+ if @mnemonic
14
+ form.bind_key(@mnemonic.downcase, self){ self.fire }
15
+ end
16
+ @display_length = config[:width]
17
+ end
18
+ def fire
19
+ super
20
+ self.focus
21
+ end
22
+ def getvalue_for_paint
23
+ getvalue()
24
+ end
25
+ ##
26
+ end # class
27
+ end # module
@@ -0,0 +1,1247 @@
1
+ =begin
2
+ * Name: rlistbox: editable scrollable lists
3
+ * Description
4
+ * Author: rkumar (arunachalesha)
5
+ * Date: 2008-11-19 12:49
6
+ * License: Same as Ruby's License (http://www.ruby-lang.org/LICENSE.txt)
7
+ * This file started on 2009-01-13 22:18 (broken off rwidgets.rb)
8
+ NOTE: listbox now traps RETURN/ENTER/13 so if you are trapping it, please use bind :PRESS
9
+ # Changes:
10
+ # Added edit_toggle_mode and edit_toggle_key so on can use vim keys for navigating
11
+ # and go into edit mode (currently using C-e). This way ENTER can still be used
12
+ # for PRESS. Esc and esc-esc closes edit mode whereas C-c or C-g reverts edit.
13
+
14
+ TODO
15
+ =end
16
+ require 'rbcurse'
17
+ require 'rbcurse/core/include/listcellrenderer'
18
+ require 'rbcurse/extras/include/listkeys'
19
+ require 'forwardable'
20
+
21
+
22
+ #include Ncurses # FFI 2011-09-8
23
+ module RubyCurses
24
+ extend self
25
+ ##
26
+ # When an event is fired by Listbox, contents are changed, then this object will be passed
27
+ # to trigger
28
+ # shamelessly plugged from a legacy language best unnamed
29
+ # type is CONTENTS_CHANGED, INTERVAL_ADDED, INTERVAL_REMOVED
30
+ class ListDataEvent
31
+ attr_accessor :index0, :index1, :source, :type
32
+ def initialize index0, index1, source, type
33
+ @index0 = index0
34
+ @index1 = index1
35
+ @source = source
36
+ @type = type
37
+ end
38
+ def to_s
39
+ "#{@type.to_s}, #{@source}, #{@index0}, #{@index1}"
40
+ end
41
+ def inspect
42
+ "#{@type.to_s}, #{@source}, #{@index0}, #{@index1}"
43
+ end
44
+ end
45
+ # http://www.java2s.com/Code/JavaAPI/javax.swing.event/ListDataEventCONTENTSCHANGED.htm
46
+ # should we extend array of will that open us to misuse
47
+ class ListDataModel
48
+ include Enumerable
49
+ include RubyCurses::EventHandler
50
+ attr_accessor :selected_index
51
+ attr_reader :last_regex # should i really keep here as public or maintain in listbox
52
+
53
+ def initialize anarray=[]
54
+ @list = anarray.dup
55
+ @_events = [:LIST_DATA_EVENT, :ENTER_ROW]
56
+ end
57
+ # changd on 2009-01-14 12:28 based on ..
58
+ # http://www.ruby-forum.com/topic/175637#769030
59
+ def each(&blk)
60
+ @list.each(&blk)
61
+ end
62
+ # not sure how to do this XXX removed on 2009-01-14 12:28
63
+ #def <=>(other)
64
+ # @list <=> other
65
+ #end
66
+ def index obj
67
+ @list.index(obj)
68
+ end
69
+ def length ; @list.length; end
70
+ alias :size :length
71
+
72
+ def insert off0, *data
73
+ @list.insert off0, *data
74
+ lde = ListDataEvent.new(off0, off0+data.length-1, self, :INTERVAL_ADDED)
75
+ fire_handler :LIST_DATA_EVENT, lde
76
+ end
77
+ def append data
78
+ @list << data
79
+ lde = ListDataEvent.new(@list.length-1, @list.length-1, self, :INTERVAL_ADDED)
80
+ fire_handler :LIST_DATA_EVENT, lde
81
+ end
82
+ def update off0, data
83
+ @list[off0] = data
84
+ lde = ListDataEvent.new(off0, off0, self, :CONTENTS_CHANGED)
85
+ fire_handler :LIST_DATA_EVENT, lde
86
+ end
87
+ def []=(off0, data)
88
+ update off0, data
89
+ end
90
+ def [](off0)
91
+ @list[off0]
92
+ end
93
+ def delete_at off0
94
+ ret=@list.delete_at off0
95
+ lde = ListDataEvent.new(off0, off0, self, :INTERVAL_REMOVED)
96
+ fire_handler :LIST_DATA_EVENT, lde
97
+ return ret
98
+ end
99
+ def remove_all
100
+ return if @list.nil? || @list.empty? # 2010-09-21 13:25
101
+ lde = ListDataEvent.new(0, @list.size, self, :INTERVAL_REMOVED)
102
+ @list = []
103
+ @current_index = 0
104
+ fire_handler :LIST_DATA_EVENT, lde
105
+ end
106
+ def delete obj
107
+ off0 = @list.index obj
108
+ return nil if off0.nil?
109
+ ret=@list.delete_at off0
110
+ lde = ListDataEvent.new(off0, off0, self, :INTERVAL_REMOVED)
111
+ fire_handler :LIST_DATA_EVENT, lde
112
+ return ret
113
+ end
114
+ def include?(obj)
115
+ return @list.include?(obj)
116
+ end
117
+ # returns a `dup()` of the list
118
+ def values
119
+ @list.dup
120
+ end
121
+ # why do we have this here in data, we should remove this
122
+ # @deprecated this was just eye candy for some demo
123
+ def on_enter_row object
124
+ $log.debug " XXX on_enter_row of list_data"
125
+ fire_handler :ENTER_ROW, object
126
+ end
127
+ # ##
128
+ # added 2009-01-14 01:00
129
+ # searches between given range of rows (def 0 and end)
130
+ # returns row index of first match of given regex (or nil if not found)
131
+ def find_match regex, ix0=0, ix1=length()
132
+ $log.debug " find_match got #{regex} #{ix0} #{ix1}"
133
+ @last_regex = regex
134
+ @search_start_ix = ix0
135
+ @search_end_ix = ix1
136
+ #@search_found_ix = nil
137
+ @list.each_with_index do |row, ix|
138
+ next if ix < ix0
139
+ break if ix > ix1
140
+ if !row.match(regex).nil?
141
+ @search_found_ix = ix
142
+ return ix
143
+ end
144
+ end
145
+ return nil
146
+ end
147
+ ##
148
+ # continues previous search
149
+ def find_next
150
+ raise "No previous search" if @last_regex.nil?
151
+ start = @search_found_ix && @search_found_ix+1 || 0
152
+ return find_match @last_regex, start, @search_end_ix
153
+ end
154
+ ##
155
+ # find backwards, list_data_model
156
+ # Using this to start a search or continue search
157
+ def find_prev regex=@last_regex, start = @search_found_ix
158
+ raise "No previous search" if regex.nil? # @last_regex.nil?
159
+ $log.debug " find_prev #{@search_found_ix} : #{@current_index}"
160
+ start -= 1 unless start == 0
161
+ @last_regex = regex
162
+ @search_start_ix = start
163
+ start.downto(0) do |ix|
164
+ row = @list[ix]
165
+ if !row.match(regex).nil?
166
+ @search_found_ix = ix
167
+ return ix
168
+ end
169
+ end
170
+ return nil
171
+ #return find_match @last_regex, start, @search_end_ix
172
+ end
173
+ ##
174
+ # added 2010-05-23 12:10 for listeditable
175
+ def slice!(line, howmany)
176
+ ret = @list.slice!(line, howmany)
177
+ lde = ListDataEvent.new(line, line+howmany-1, self, :INTERVAL_REMOVED)
178
+ fire_handler :LIST_DATA_EVENT, lde
179
+ return ret
180
+ end
181
+
182
+ alias :to_array :values
183
+ end # class ListDataModel
184
+ ##
185
+ # scrollable, selectable list of items
186
+ # TODO Add events for item add/remove and selection change
187
+ # added event LIST_COMBO_SELECT fired whenever a select/deselect is done.
188
+ # - I do not know how this works in Tk so only the name is copied..
189
+ # - @selected contains indices of selected objects.
190
+ # - currently the first argument of event is row (the row selected/deselected). Should it
191
+ # be the object.
192
+ # - this event could change when range selection is allowed.
193
+ #
194
+
195
+ ##
196
+ # TODO CAN WE MOVE THIS OUT TO ANOTHER FILE as confusing me
197
+ # pops up a list of values for selection
198
+ # 2008-12-10
199
+ class PopupList
200
+ include RubyCurses::EventHandler
201
+ dsl_accessor :title
202
+ dsl_accessor :row, :col, :height, :width
203
+ dsl_accessor :layout
204
+ attr_reader :config
205
+ attr_reader :selected_index # button index selected by user
206
+ attr_reader :window # required for keyboard
207
+ dsl_accessor :list_selection_mode # true or false allow multiple selection
208
+ dsl_accessor :relative_to # a widget, if given row and col are relative to widgets windows
209
+ # layout
210
+ dsl_accessor :max_visible_items # how many to display
211
+ dsl_accessor :list_config # hash with values for the list to use
212
+ dsl_accessor :valign
213
+ attr_reader :listbox
214
+
215
+ def initialize aconfig={}, &block
216
+ @config = aconfig
217
+ @selected_index = -1
218
+ @list_config ||= {}
219
+ @config.each_pair { |k,v| instance_variable_set("@#{k}",v) }
220
+ instance_eval &block if block_given?
221
+ @list_config.each_pair { |k,v| instance_variable_set("@#{k}",v) }
222
+ @height ||= [@max_visible_items || 10, @list.length].min
223
+ $log.debug " POPUP XXX #{@max_visible_items} ll:#{@list.length} h:#{@height}"
224
+ # get widgets absolute coords
225
+ if @relative_to
226
+ layout = @relative_to.form.window.layout
227
+ @row = @row + layout[:top]
228
+ @col = @col + layout[:left]
229
+ end
230
+ if !@valign.nil?
231
+ case @valign.to_sym
232
+ when :BELOW
233
+ @row += 1
234
+ when :ABOVE
235
+ @row -= @height+1
236
+ @row = 0 if @row < 0
237
+ when :CENTER
238
+ @row -= @height/2
239
+ @row = 0 if @row < 0
240
+ else
241
+ end
242
+ end
243
+
244
+ layout(1+height, @width+4, @row, @col) # changed 2 to 1, 2008-12-17 13:48
245
+ @window = VER::Window.new(@layout)
246
+ @form = RubyCurses::Form.new @window
247
+ @window.bkgd(Ncurses.COLOR_PAIR($reversecolor));
248
+ @window.wrefresh
249
+ #@panel = @window.panel # useless line ?
250
+ Ncurses::Panel.update_panels
251
+ print_input # creates the listbox
252
+ @form.repaint
253
+ @window.wrefresh
254
+ handle_keys
255
+ end
256
+ # class popup
257
+ def list alist=nil
258
+ return @list if alist.nil?
259
+ @list = ListDataModel.new(alist)
260
+ @repaint_required = true
261
+ # will we need this ? listbox made each time so data should be fresh
262
+ #@list.bind(:LIST_DATA_EVENT) { |e| list_data_changed() }
263
+ end
264
+ # class popup
265
+ def list_data_model ldm
266
+ raise "Expecting list_data_model" unless ldm.is_a? RubyCurses::ListDataModel
267
+ @list = ldm
268
+ # will we need this ? listbox made each time so data should be fresh
269
+ #@list.bind(:LIST_DATA_EVENT) { |e| list_data_changed() }
270
+ end
271
+ ##
272
+ def input_value
273
+ #return @listbox.getvalue if !@listbox.nil?
274
+ return @listbox.focussed_index if !@listbox.nil?
275
+ end
276
+ ## popuplist
277
+ def stopping?
278
+ @stop
279
+ end
280
+ ## popuplist
281
+ def handle_keys
282
+ begin
283
+ while((ch = @window.getchar()) != 999 )
284
+ case ch
285
+ when -1
286
+ next
287
+ else
288
+ press ch
289
+ break if @stop
290
+ end
291
+ end
292
+ ensure
293
+ destroy
294
+ end
295
+ return 0 #@selected_index
296
+ end
297
+ ##
298
+ # TODO get next match for key
299
+ def press ch
300
+ $log.debug "popup handle_keys : #{ch}" if ch != -1
301
+ case ch
302
+ when -1
303
+ return
304
+ when KEY_F1, 27, ?\C-q # 27/ESC does not come here since gobbled by keyboard.rb
305
+ @stop = true
306
+ return
307
+ when KEY_ENTER, 10, 13
308
+ # if you press ENTER without selecting, it won't come here
309
+ # it will fire button OK's fire, if that's the default button
310
+
311
+ # returns an array of indices if multiple selection
312
+ if @listbox.selection_mode == :multiple
313
+ fire_handler :PRESS, @listbox
314
+ else
315
+ fire_handler :PRESS, @listbox.focussed_index
316
+ end
317
+ # since Listbox is handling enter, COMBO_SELECT will not be fired
318
+ # $log.debug "popup ENTER : #{field.name}" if !field.nil?
319
+ @stop = true
320
+ return
321
+ when KEY_TAB
322
+ @form.select_next_field
323
+ else
324
+ # fields must return unhandled else we will miss hotkeys.
325
+ # On messageboxes, often if no edit field, then O and C are hot.
326
+ field = @form.get_current_field
327
+ handled = field.handle_key ch
328
+
329
+ if handled == :UNHANDLED
330
+ @stop = true
331
+ return
332
+ end
333
+ end
334
+ @form.repaint
335
+ Ncurses::Panel.update_panels();
336
+ Ncurses.doupdate();
337
+ @window.wrefresh
338
+ end
339
+ def print_input
340
+ r = c = 0
341
+ width = @layout[:width]
342
+ #$log.debug " print_input POPUP ht:#{@height} lh:#{@layout[:height]} "
343
+ height = @layout[:height]
344
+ #height = @height # 2010-01-06 12:52 why was this overriding previous line. its one less than layout
345
+ # i am now using layout height since it gives a closer size to whats asked for.
346
+ parent = @relative_to
347
+ defaultvalue = @default_value || ""
348
+ list = @list
349
+ selection_mode = @list_selection_mode
350
+ default_values = @default_values
351
+ @list_config['color'] ||= 'black'
352
+ @list_config['bgcolor'] ||= 'yellow'
353
+ @listbox = RubyCurses::Listbox.new @form, @list_config do
354
+ name "input"
355
+ row r
356
+ col c
357
+ # attr 'reverse'
358
+ width width
359
+ height height
360
+ list_data_model list
361
+ # ?? XXX display_length 30
362
+ # set_buffer defaultvalue
363
+ selection_mode selection_mode
364
+ default_values default_values
365
+ is_popup true
366
+ #add_observer parent
367
+ end
368
+ end
369
+ # may need to be upgraded to new one XXX FIXME
370
+ def configure(*val , &block)
371
+ case val.size
372
+ when 1
373
+ return @config[val[0]]
374
+ when 2
375
+ @config[val[0]] = val[1]
376
+ instance_variable_set("@#{val[0]}", val[1])
377
+ end
378
+ instance_eval &block if block_given?
379
+ end
380
+ def cget param
381
+ @config[param]
382
+ end
383
+
384
+ def layout(height=0, width=0, top=0, left=0)
385
+ @layout = { :height => height, :width => width, :top => top, :left => left }
386
+ end
387
+ def destroy
388
+ @window.destroy if !@window.nil?
389
+ end
390
+ end # class PopupList
391
+ ##
392
+ # A control for displaying a list of data or values.
393
+ # The list will be editable if @cell_editing_allowed
394
+ # is set to true when creating. By default, multiple selection is allowed, but may be set to :single.
395
+ # TODO: were we not going to force creation of datamodel and listener on startup by putting a blank
396
+ # :list in config, if no list or list_variable or model is there ?
397
+ # Or at end of constructor check, if no listdatamodel then create default.
398
+ # TODO : perhaps when datamodel created, attach listener to it, so we can fire to callers when
399
+ # they want to be informed of changes. As we did with selection listeners.
400
+ #
401
+ class Listbox < Widget
402
+
403
+ require 'rbcurse/core/include/listscrollable'
404
+ require 'rbcurse/extras/include/listselectable'
405
+ require 'rbcurse/extras/include/defaultlistselectionmodel'
406
+ require 'rbcurse/extras/include/celleditor'
407
+ include ListScrollable
408
+ include ListSelectable
409
+ include RubyCurses::ListKeys
410
+ extend Forwardable
411
+ dsl_accessor :height
412
+ dsl_accessor :title
413
+ dsl_property :title_attrib # bold, reverse, normal
414
+ # dsl_accessor :list # the array of data to be sent by user
415
+ attr_reader :toprow
416
+ # attr_reader :prow
417
+ # attr_reader :winrow
418
+ # dsl_accessor :selection_mode # allow multiple select or not
419
+ # dsl_accessor :list_variable # a variable values are shown from this
420
+ dsl_accessor :default_values # array of default values
421
+ dsl_accessor :is_popup # if it is in a popup and single select, selection closes
422
+ attr_accessor :current_index
423
+ #dsl_accessor :cell_renderer
424
+ dsl_accessor :selected_color, :selected_bgcolor, :selected_attr
425
+ dsl_accessor :max_visible_items # how many to display 2009-01-11 16:15
426
+ dsl_accessor :cell_editing_allowed
427
+ dsl_property :show_selector
428
+ dsl_property :row_selected_symbol # 2009-01-12 12:01 changed from selector to selected
429
+ dsl_property :row_unselected_symbol # added 2009-01-12 12:00
430
+ dsl_property :left_margin
431
+ # please set these in he constructor block. Settin them later will have no effect
432
+ # since i would have bound them to actions
433
+ dsl_accessor :KEY_ROW_SELECTOR # editable lists may want to use 0 or some other key
434
+ dsl_accessor :KEY_GOTO_TOP # this is going to go
435
+ dsl_accessor :KEY_GOTO_BOTTOM # this is going to go
436
+ dsl_accessor :KEY_CLEAR_SELECTION # this is going to go
437
+ dsl_accessor :KEY_NEXT_SELECTION # this is going to go
438
+ dsl_accessor :KEY_PREV_SELECTION # this is going to go
439
+ dsl_accessor :valign # 2009-01-17 18:32 vertical alignment used in combos
440
+ dsl_accessor :justify # 2010-09-27 12:41 used by renderer
441
+ attr_accessor :one_key_selection # will pressing a single key select or not
442
+ dsl_accessor :border_attrib, :border_color #
443
+ dsl_accessor :sanitization_required
444
+ dsl_accessor :suppress_borders #to_print_borders
445
+
446
+
447
+ def initialize form, config={}, &block
448
+ @focusable = true
449
+ @editable = false
450
+ @sanitization_required = true
451
+ @one_key_selection = false # allow vim like keys
452
+ @row = 0
453
+ @col = 0
454
+ # data of listbox this is not an array, its a pointer to the listdatamodel
455
+ @list = nil
456
+ # any special attribs such as status to be printed in col1, or color (selection)
457
+ @list_attribs = {}
458
+ @suppress_borders = false
459
+ @row_offset = @col_offset = 1 # for borders
460
+ super
461
+ @_events.push(*[:ENTER_ROW, :LEAVE_ROW, :LIST_SELECTION_EVENT, :PRESS])
462
+ @current_index ||= 0
463
+ @selection_mode ||= :multiple # default is multiple, anything else given becomes single
464
+ @win = @graphic # 2010-01-04 12:36 BUFFERED replace form.window with graphic
465
+ # moving down to repaint so that scrollpane can set should_buffered
466
+ # added 2010-02-17 23:05 RFED16 so we don't need a form.
467
+
468
+ # next 2 lines carry a redundancy
469
+ select_default_values
470
+ # when the combo box has a certain row in focus, the popup should have the same row in focus
471
+
472
+ install_keys
473
+ install_list_keys
474
+ init_vars
475
+ init_actions
476
+ # OMG What about people whove installed custom renders such as rfe.rb 2011-10-15
477
+ #bind(:PROPERTY_CHANGE){|e| @cell_renderer = nil } # will be recreated if anything changes 2011-09-28 V1.3.1
478
+ bind(:PROPERTY_CHANGE){|e|
479
+ # I can't delete the cell renderer, but this may not have full effect if one color is passed.
480
+ if @cell_renderer.respond_to? e.property_name
481
+ @cell_renderer.send(e.property_name.to_sym, e.newvalue)
482
+ end
483
+ } # will be recreated if anything changes 2011-09-28 V1.3.1
484
+
485
+ if @list && !@list.selected_index.nil?
486
+ set_focus_on @list.selected_index # the new version
487
+ end
488
+ end
489
+ # this is called several times, from constructor
490
+ # and when list data changed, so only put relevant resets here.
491
+ # why can't current_index be set to 0 here
492
+ def init_vars
493
+ @repaint_required = true
494
+ @toprow = @pcol = 0
495
+
496
+ @row_offset = @col_offset = 0 if @suppress_borders
497
+ if @show_selector
498
+ @row_selected_symbol ||= '>'
499
+ @row_unselected_symbol ||= ' '
500
+ @left_margin ||= @row_selected_symbol.length
501
+ end
502
+ @left_margin ||= 0
503
+ # we reduce internal_width from width while printing
504
+ @internal_width = 2 # taking into account borders accounting for 2 cols
505
+ @internal_width = 0 if @suppress_borders # should it be 0 ???
506
+
507
+ # toggle editable state using ENTER key
508
+ @edit_toggle = false
509
+ @edit_toggle_key = ?\C-e.getbyte(0)
510
+ # has editing started using edit_toggle
511
+ @is_editing = !@edit_toggle
512
+ map_keys unless @keys_mapped # moved here so users can remap
513
+
514
+ end
515
+ def map_keys
516
+ return if @keys_mapped
517
+ bind_key(?f, 'next row starting with ...'){ ask_selection_for_char() }
518
+ bind_key(?\M-v, 'one key toggle'){ @one_key_selection = !@one_key_selection }
519
+ bind_key(?j, 'next row'){ next_row() }
520
+ bind_key(?k, 'prev row'){ previous_row() }
521
+ bind_key(?\C-n, 'next row'){ next_row() }
522
+ bind_key(?\C-p, 'prev row'){ previous_row() }
523
+ bind_key(?\C-d, :scroll_forward)
524
+ bind_key(?\C-b, :scroll_backward)
525
+ bind_key(?G, 'goto bottom'){ goto_bottom() }
526
+ bind_key([?g,?g], 'goto top'){ goto_top() }
527
+ bind_key(?\M-<, :goto_top )
528
+ bind_key(?\M->, :goto_bottom )
529
+ bind_key(?/, 'find'){ ask_search() }
530
+ bind_key(?n, 'find next'){ find_more() }
531
+ #bind_key(32){ toggle_row_selection() } # some guys may want another selector
532
+ if @cell_editing_allowed && @KEY_ROW_SELECTOR == 32
533
+ @KEY_ROW_SELECTOR = 0 # Ctrl-Space
534
+ end
535
+ bind_key(@KEY_ROW_SELECTOR, 'select a row'){ toggle_row_selection() }
536
+ bind_key(10, 'fire action'){ fire_action_event }
537
+ # I had commented this but we need ENTER for cases like a directory browser where ENTER opens dir
538
+ bind_key(KEY_ENTER, 'fire action'){ fire_action_event }
539
+ #bind_key(13){ @is_editing = !@is_editing }
540
+ #bind_key(?\M-: , :_show_menu)
541
+ @keys_mapped = true
542
+ end
543
+
544
+ ##
545
+ # getter and setter for selection_mode
546
+ # Must be called after creating model, so no duplicate. Since one may set in model directly.
547
+ def selection_mode(*val)
548
+ #raise "ListSelectionModel not yet created!" if @list_selection_model.nil?
549
+
550
+ if val.empty?
551
+ if @list_selection_model
552
+ return @list_selection_model.selection_mode
553
+ else
554
+ return @tmp_selection_mode
555
+ end
556
+ else
557
+ if @list_selection_model.nil?
558
+ @tmp_selection_mode = val[0]
559
+ else
560
+ @list_selection_model.selection_mode = val[0].to_sym
561
+ end
562
+ end
563
+ end
564
+ def row_count
565
+ return 0 if @list.nil?
566
+ @list.length
567
+ end
568
+ # added 2009-01-07 13:05 so new scrollable can use
569
+ def scrollatrow
570
+ if @suppress_borders
571
+ return @height - 1
572
+ else
573
+ return @height - 3
574
+ end
575
+ end
576
+ # provide data to List in the form of an Array or Variable or
577
+ # ListDataModel. This will create a default ListSelectionModel.
578
+ #
579
+ # CHANGE as on 2010-09-21 12:53:
580
+ # If explicit nil passed then dummy datamodel and selection model created
581
+ # From now on, constructor will call this, so this can always
582
+ # happen.
583
+ #
584
+ # NOTE: sometimes this can be added much after its painted.
585
+ # Do not expect this to be called from constructor, although that
586
+ # is the usual case. it can be dependent on some other list or tree.
587
+ # @param [Array, Variable, ListDataModel] data to populate list with
588
+ # @return [ListDataModel] just created or assigned
589
+
590
+ def list *val
591
+ return @list if val.empty?
592
+ clear_selection if @list # clear previous selections if any
593
+ @default_values = nil if @list # clear previous selections if any
594
+ alist = val[0]
595
+ case alist
596
+ when Array
597
+ if @list
598
+ @list.remove_all
599
+ @list.insert 0, *alist
600
+ @current_index = 0
601
+ else
602
+ @list = RubyCurses::ListDataModel.new(alist)
603
+ end
604
+ when NilClass
605
+ if @list
606
+ @list.remove_all
607
+ else
608
+ @list = RubyCurses::ListDataModel.new(alist)
609
+ end
610
+ when Variable
611
+ @list = RubyCurses::ListDataModel.new(alist.value)
612
+ when RubyCurses::ListDataModel
613
+ @list = alist
614
+ else
615
+ raise ArgumentError, "Listbox list(): do not know how to handle #{alist.class} "
616
+ end
617
+ # added on 2009-01-13 23:19 since updates are not automatic now
618
+ @list.bind(:LIST_DATA_EVENT) { |e| list_data_changed() }
619
+ create_default_list_selection_model
620
+ @list_selection_model.selection_mode = @tmp_selection_mode if @tmp_selection_mode
621
+ @repaint_required = true
622
+ @list
623
+ end
624
+ # populate using a Variable which should contain a list
625
+ # NOTE: This explicilty overwrites any existing datamodel such as the
626
+ # default one. You may lose any events you have bound to the listbox
627
+ # prior to this call.
628
+ def list_variable alist=nil
629
+ return @list if alist.nil?
630
+ @list = RubyCurses::ListDataModel.new(alist.value)
631
+ # added on 2009-01-13 23:19 since updates are not automatic now
632
+ @list.bind(:LIST_DATA_EVENT) { |e| list_data_changed() }
633
+ create_default_list_selection_model
634
+ end
635
+ # populate using a custom data model
636
+ # NOTE: This explicilty overwrites any existing datamodel such as the
637
+ # default one. You may lose any events you have bound to the listbox
638
+ # prior to this call.
639
+
640
+ def list_data_model ldm=nil
641
+ return @list if ldm.nil?
642
+ raise "Expecting list_data_model" unless ldm.is_a? RubyCurses::ListDataModel
643
+ @list = ldm
644
+ # added on 2009-01-13 23:19 since updates are not automatic now
645
+ @list.bind(:LIST_DATA_EVENT) { |e| list_data_changed() }
646
+ create_default_list_selection_model
647
+ end
648
+ # create a default list selection model and set it
649
+ # NOTE: I am now checking if one is not already created, since
650
+ # a second creation would wipe out any listeners on it.
651
+ # @see ListSelectable
652
+ # @see DefaultListSelectionModel
653
+ def create_default_list_selection_model
654
+ if @list_selection_model.nil?
655
+ list_selection_model DefaultListSelectionModel.new(self)
656
+ end
657
+ end
658
+ # added 2010-09-15 00:11 to make life easier
659
+ def_delegators :@list, :insert, :remove_all, :delete_at, :include?, :each, :values, :size
660
+ # get element at
661
+ # @param [Fixnum] index for element
662
+ # @return [Object] element
663
+ # @since 1.2.0 2010-09-06 14:33 making life easier for others.
664
+ def [](off0)
665
+ @list[off0]
666
+ end
667
+ # return object under cursor
668
+ # Note: this should not be confused with selected row/s. User may not have selected this.
669
+ # This is only useful since in some demos we like to change a status bar as a user scrolls down
670
+ # @since 1.2.0 2010-09-06 14:33 making life easier for others.
671
+ def current_value
672
+ @list[@current_index]
673
+ end
674
+ # avoid using "row", i'd rather stick with "index" and "value".
675
+ alias :current_row :current_value
676
+ alias :text :current_value # thanks to shoes, not sure how this will impact since widget has text.
677
+
678
+ # XXX can this not be done at repaint
679
+ def select_default_values
680
+ return if @default_values.nil?
681
+ @default_values.each do |val|
682
+ row = @list.index val
683
+ add_row_selection_interval row, row unless row.nil?
684
+ end
685
+ end
686
+ def print_borders
687
+ raise "Width not supplied" unless @width
688
+ raise "Height not supplied" unless @height
689
+ width = @width
690
+ height = @height-1 # 2010-01-04 15:30 BUFFERED HEIGHT
691
+ window = @graphic # 2010-01-04 12:37 BUFFERED
692
+ startcol = @col
693
+ startrow = @row
694
+ @color_pair = get_color($datacolor)
695
+ # bordercolor = @border_color || $datacolor # changed 2011 dts
696
+ bordercolor = @border_color || @color_pair
697
+ borderatt = @border_attrib || Ncurses::A_NORMAL
698
+
699
+ #$log.debug "rlistb #{name}: window.print_border #{startrow}, #{startcol} , h:#{height}, w:#{width} , @color_pair, @attr "
700
+ window.print_border startrow, startcol, height, width, bordercolor, borderatt
701
+ print_title
702
+ end
703
+ def print_title
704
+ @color_pair ||= get_color($datacolor)
705
+ # check title.length and truncate if exceeds width
706
+ return unless @title
707
+ _title = @title
708
+ if @title.length > @width - 2
709
+ _title = @title[0..@width-2]
710
+ end
711
+ @graphic.printstring( @row, @col+(@width-_title.length)/2, _title, @color_pair, @title_attrib) unless @title.nil?
712
+ end
713
+ ### START FOR scrollable ###
714
+ def get_content
715
+ #@list 2008-12-01 23:13
716
+ # NOTE: we never stored the listvariable, so its redundant, we used its value to set list
717
+ @list_variable && @list_variable.value || @list
718
+ end
719
+ def get_window
720
+ @graphic # 2010-01-04 12:37 BUFFERED
721
+ end
722
+ ### END FOR scrollable ###
723
+ # override widgets text
724
+ # returns indices of selected rows
725
+ def getvalue
726
+ selected_rows
727
+ end
728
+ # Listbox
729
+ def handle_key(ch)
730
+ @current_index ||= 0
731
+ @toprow ||= 0
732
+ h = scrollatrow()
733
+ rc = row_count
734
+ $log.debug " listbox got ch #{ch}"
735
+ # not sure we should do something like this
736
+ # If editing is happening don't use space for selection, let it be using in editing
737
+ # and make 0 (ctrl-space) the selector. If no editing, let space be selector
738
+ if ch == 32
739
+ if @KEY_ROW_SELECTOR == 32
740
+ if @is_editing
741
+ key_row_selector = 0 # Ctrl-Space
742
+ else
743
+ key_row_selector = 32
744
+ end
745
+ end
746
+ end
747
+ case ch
748
+ when KEY_UP # show previous value
749
+ return previous_row
750
+ when KEY_DOWN # show previous value
751
+ return next_row
752
+ when key_row_selector # 32
753
+ return if is_popup && @selection_mode != :multiple # not allowing select this way since there will be a difference
754
+ toggle_row_selection @current_index #, @current_index
755
+ @repaint_required = true
756
+ when @KEY_GOTO_TOP # 48, ?\C-[
757
+ # please note that C-[ gives 27, same as esc so will respond after ages
758
+ goto_top
759
+ when @KEY_GOTO_BOTTOM # ?\C-]
760
+ goto_bottom
761
+ when @KEY_NEXT_SELECTION # ?'
762
+ $log.debug "insdie next selection"
763
+ @oldrow = @current_index
764
+ do_next_selection
765
+ bounds_check
766
+ when @KEY_PREV_SELECTION # ?"
767
+ @oldrow = @current_index
768
+ $log.debug "insdie prev selection"
769
+ do_prev_selection
770
+ bounds_check
771
+ when @KEY_CLEAR_SELECTION
772
+ clear_selection
773
+ @repaint_required = true
774
+ when 3, ?\C-c.getbyte(0), ?\C-g.getbyte(0)
775
+ editing_canceled @current_index if @cell_editing_allowed
776
+ cancel_block # block
777
+ @repaint_required = true
778
+ $multiplier = 0
779
+ when 27, 2727
780
+ # ESC or ESC ESC completes edit along with toggle key
781
+ if @is_editing
782
+ editing_completed @current_index
783
+ end
784
+ @repaint_required = true
785
+ when @edit_toggle_key
786
+ # I am currently allowing this to work even if edit_toggle is false
787
+ # You can check for edit_toggle here, if you don't like this behaviour
788
+ @is_editing = !@is_editing
789
+ if !@is_editing
790
+ editing_completed @current_index
791
+ end
792
+ @repaint_required = true
793
+ when @KEY_ASK_FIND_FORWARD
794
+ # ask_search_forward
795
+ when @KEY_ASK_FIND_BACKWARD
796
+ # ask_search_backward
797
+ when @KEY_FIND_NEXT
798
+ # find_next
799
+ when @KEY_FIND_PREV
800
+ # find_prev
801
+ when @KEY_ASK_FIND
802
+ ask_search
803
+ when @KEY_FIND_MORE
804
+ find_more
805
+ when @KEY_BLOCK_SELECTOR
806
+ mark_block #selection
807
+ #when ?\C-u.getbyte(0)
808
+ # multiplier. Series is 4 16 64
809
+ # TESTING @multiplier = (@multiplier == 0 ? 4 : @multiplier *= 4)
810
+ # return 0
811
+ when ?\C-c.getbyte(0)
812
+ @multiplier = 0
813
+ return 0
814
+ else
815
+ # this has to be fixed, if compo does not handle key it has to continue into next part FIXME
816
+ ret = :UNHANDLED # changed on 2009-01-27 13:14 not going into unhandled, tab not released
817
+ if @cell_editing_allowed
818
+ #if !@edit_toggle || (@edit_toggle && @is_editing)
819
+ if @is_editing
820
+ @repaint_required = true
821
+ # hack - on_enter_row should fire when this widget gets focus. first row that is DONE
822
+ begin
823
+ ret = @cell_editor.component.handle_key(ch)
824
+ rescue
825
+ on_enter_row @current_index
826
+ ret = @cell_editor.component.handle_key(ch)
827
+ end
828
+ end
829
+ end
830
+ if ret == :UNHANDLED
831
+ # beware one-key eats up numbers. we'll be wondering why
832
+ if @one_key_selection
833
+ case ch
834
+ #when ?A.getbyte(0)..?Z.getbyte(0), ?a.getbyte(0)..?z.getbyte(0), ?0.getbyte(0)..?9.getbyte(0)
835
+ when ?A.getbyte(0)..?Z.getbyte(0), ?a.getbyte(0)..?z.getbyte(0)
836
+ # simple motion, key press defines motion
837
+ ret = set_selection_for_char ch.chr
838
+ else
839
+ ret = process_key ch, self
840
+ $log.debug "111 listbox #{@current_index} "
841
+ @multiplier = 0
842
+ return :UNHANDLED if ret == :UNHANDLED
843
+ end
844
+ else
845
+ # no motion on single key, we can freak out like in vim, pref f <char> for set_selection
846
+ case ch
847
+ when ?0.getbyte(0)..?9.getbyte(0)
848
+ $multiplier *= 10 ; $multiplier += (ch-48)
849
+ #$log.debug " setting mult to #{$multiplier} in list "
850
+ return 0
851
+ end
852
+ ret = process_key ch, self
853
+ return :UNHANDLED if ret == :UNHANDLED
854
+ end
855
+ end
856
+ end
857
+ $multiplier = 0
858
+ end
859
+ # fire handler when user presses ENTER/RETURN
860
+ # @since 1.2.0
861
+ # listbox now traps ENTER key and fires action event
862
+ # to trap please bind :PRESS
863
+ #
864
+ def fire_action_event
865
+ # this does not select the row ???? FIXME ??
866
+ require 'rbcurse/core/include/ractionevent'
867
+ # should have been callled :ACTION_EVENT !!!
868
+ fire_handler :PRESS, ActionEvent.new(self, :PRESS, text)
869
+ end
870
+ # get a keystroke from user and go to first item starting with that key
871
+ def ask_selection_for_char
872
+ ch = @graphic.getch
873
+ if ch < 0 || ch > 255
874
+ return :UNHANDLED
875
+ end
876
+ ret = set_selection_for_char ch.chr
877
+ end
878
+ def ask_search_forward
879
+ regex = get_string("Enter regex to search")
880
+ ix = @list.find_match regex
881
+ if ix.nil?
882
+ alert("No matching data for: #{regex}")
883
+ else
884
+ set_focus_on(ix)
885
+ end
886
+ end
887
+ # gets string to search and calls data models find prev
888
+ def ask_search_backward
889
+ regex = get_string("Enter regex to search (backward)")
890
+ @last_regex = regex
891
+ ix = @list.find_prev regex, @current_index
892
+ if ix.nil?
893
+ alert("No matching data for: #{regex}")
894
+ else
895
+ set_focus_on(ix)
896
+ end
897
+ end
898
+ # please check for error before proceeding
899
+ # @return [Boolean] false if no data
900
+ def on_enter
901
+ if @list.nil? || @list.size == 0
902
+ #Ncurses.beep
903
+ get_window.ungetch($current_key) # 2011-10-4 push key back so form can go next
904
+ return :UNHANDLED
905
+ end
906
+ on_enter_row @current_index
907
+ set_form_row # added 2009-01-11 23:41
908
+ #$log.debug " ONE ENTER LIST #{@current_index}, #{@form.row}"
909
+ @repaint_required = true
910
+ super
911
+ #fire_handler :ENTER, self
912
+ true
913
+ end
914
+ def on_enter_row arow
915
+ #$log.debug " Listbox #{self} ENTER_ROW with curr #{@current_index}. row: #{arow} H: #{@handler.keys}"
916
+ #fire_handler :ENTER_ROW, arow
917
+ fire_handler :ENTER_ROW, self
918
+ @list.on_enter_row self ## XXX WHY THIS ???
919
+ edit_row_at arow
920
+ @repaint_required = true
921
+ end
922
+ def edit_row_at arow
923
+ if @cell_editing_allowed
924
+ #$log.debug " cell editor on enter #{arow} val of list[row]: #{@list[arow]}"
925
+ editor = cell_editor
926
+ prepare_editor editor, arow
927
+ end
928
+ end
929
+ ##
930
+ # private
931
+ def prepare_editor editor, row
932
+ r,c = rowcol
933
+ value = @list[row] # .chomp
934
+ value = value.dup rescue value # so we can cancel
935
+ row = r + (row - @toprow) # @form.row
936
+ col = c+@left_margin # @form.col
937
+ # unfortunately 2009-01-11 19:47 combo boxes editable allows changing value
938
+ editor.prepare_editor self, row, col, value
939
+ editor.component.curpos = 0 # reset it after search, if user scrols down
940
+ #editor.component.graphic = @graphic # 2010-01-05 00:36 TRYING OUT BUFFERED
941
+ ## override is required if the listbox uses a buffer
942
+ #if @should_create_buffer # removed on 2011-09-29
943
+ #$log.debug " overriding editors comp with GRAPHIC #{@graphic} "
944
+ #editor.component.override_graphic(@graphic) # 2010-01-05 00:36 TRYING OUT BUFFERED
945
+ #end
946
+ set_form_col 0 #@left_margin
947
+
948
+ # set original value so we can cancel
949
+ # set row and col,
950
+ # set value and other things, color and bgcolor
951
+ end
952
+ def on_leave_row arow
953
+ #$log.debug " Listbox #{self} leave with (cr: #{@current_index}) #{arow}: list[row]:#{@list[arow]}"
954
+ #$log.debug " Listbox #{self} leave with (cr: #{@current_index}) #{arow}: "
955
+ #fire_handler :LEAVE_ROW, arow
956
+ fire_handler :LEAVE_ROW, self
957
+ editing_completed arow
958
+ end
959
+ def editing_completed arow
960
+ if @cell_editing_allowed
961
+ if !@cell_editor.nil?
962
+ # $log.debug " cell editor (leave) setting value row: #{arow} val: #{@cell_editor.getvalue}"
963
+ $log.debug " cell editor #{@cell_editor.component.form.window} (leave) setting value row: #{arow} val: #{@cell_editor.getvalue}"
964
+ @list[arow] = @cell_editor.getvalue #.dup 2009-01-10 21:42 boolean can't duplicate
965
+ else
966
+ $log.debug "CELL EDITOR WAS NIL, #{arow} "
967
+ end
968
+ if @edit_toggle
969
+ @is_editing = false
970
+ end
971
+ end
972
+ @repaint_required = true
973
+ end
974
+ def editing_canceled arow=@current_index
975
+ return unless @cell_editing_allowed
976
+ prepare_editor @cell_editor, arow
977
+ @repaint_required = true
978
+ end
979
+
980
+ ##
981
+ # getter and setter for cell_editor
982
+ def cell_editor(*val)
983
+ if val.empty?
984
+ @cell_editor ||= create_default_cell_editor
985
+ else
986
+ @cell_editor = val[0]
987
+ end
988
+ end
989
+ def create_default_cell_editor
990
+ return RubyCurses::CellEditor.new RubyCurses::Field.new nil, {"focusable"=>false, "visible"=>false, "display_length"=> @width-@internal_width-@left_margin}
991
+ end
992
+ ##
993
+ # getter and setter for cell_renderer
994
+ def cell_renderer(*val)
995
+ if val.empty?
996
+ @cell_renderer ||= create_default_cell_renderer
997
+ else
998
+ @cell_renderer = val[0]
999
+ end
1000
+ end
1001
+ def create_default_cell_renderer
1002
+ return RubyCurses::ListCellRenderer.new "", {"color"=>@color, "bgcolor"=>@bgcolor, "parent" => self, "display_length"=> @width-@internal_width-@left_margin}
1003
+ end
1004
+ ##
1005
+ # this method chops the data to length before giving it to the
1006
+ # renderer, this can cause problems if the renderer does some
1007
+ # processing. also, it pans the data horizontally giving the renderer
1008
+ # a section of it.
1009
+ def repaint
1010
+ return unless @repaint_required
1011
+ return if (@height || 0) < 3 || @width == 0
1012
+ # not sure where to put this, once for all or repeat 2010-02-17 23:07 RFED16
1013
+ my_win = @form ? @form.window : @target_window
1014
+ @graphic = my_win unless @graphic
1015
+ raise " #{@name} neither form, nor target window given LB paint " unless my_win
1016
+ raise " #{@name} NO GRAPHIC set as yet LB paint " unless @graphic
1017
+
1018
+ #$log.debug "rlistbox repaint #{@name} r,c, #{@row} #{@col} , width: #{@width} "
1019
+ print_borders unless @suppress_borders # do this once only, unless everything changes
1020
+ #maxlen = @maxlen || @width-@internal_width
1021
+ renderer = cell_renderer()
1022
+ renderer.display_length(@width-@internal_width-@left_margin) # just in case resizing of listbox
1023
+ tm = list()
1024
+ rc = row_count
1025
+ @longest_line = @width
1026
+ $log.debug " rlistbox repaint #{row_count} #{name} "
1027
+ if rc > 0 # just added in case no data passed
1028
+ tr = @toprow
1029
+ acolor = get_color $datacolor # should be set once, if color or bgcolor changs TODO FIXME
1030
+ h = scrollatrow()
1031
+ r,c = rowcol
1032
+ 0.upto(h) do |hh|
1033
+ crow = tr+hh
1034
+ if crow < rc
1035
+ _focussed = @current_index == crow ? true : false # row focussed ?
1036
+ focus_type = _focussed
1037
+ # added 2010-09-02 14:39 so inactive fields don't show a bright focussed line
1038
+ #focussed = false if focussed && !@focussed
1039
+ focus_type = :SOFT_FOCUS if _focussed && !@focussed
1040
+ selected = is_row_selected crow
1041
+ content = tm[crow] # 2009-01-17 18:37 chomp giving error in some cases says frozen
1042
+ content = convert_value_to_text content, crow # 2010-09-23 20:12
1043
+ # by now it has to be a String
1044
+ if content.is_a? String
1045
+ content = content.dup
1046
+ sanitize content if @sanitization_required
1047
+ truncate content
1048
+ end
1049
+ ## set the selector symbol if requested
1050
+ selection_symbol = ''
1051
+ if @show_selector
1052
+ if selected
1053
+ selection_symbol = @row_selected_symbol
1054
+ else
1055
+ selection_symbol = @row_unselected_symbol
1056
+ end
1057
+ @graphic.printstring r+hh, c, selection_symbol, acolor,@attr
1058
+ end
1059
+ #renderer = get_default_cell_renderer_for_class content.class.to_s
1060
+ renderer.repaint @graphic, r+hh, c+@left_margin, crow, content, focus_type, selected
1061
+ else
1062
+ # clear rows
1063
+ @graphic.printstring r+hh, c, " " * (@width-@internal_width), acolor,@attr
1064
+ end
1065
+ end
1066
+ if @cell_editing_allowed && @is_editing
1067
+ @cell_editor.component.repaint unless @cell_editor.nil? or @cell_editor.component.form.nil?
1068
+ end
1069
+ end # rc == 0
1070
+ #@table_changed = false
1071
+ @repaint_required = false
1072
+ end
1073
+ # the idea here is to allow users who subclass Listbox to easily override parts of the cumbersome repaint
1074
+ # method. This assumes your List has some data, but you print a lot more. Now you don't need to
1075
+ # change the data in the renderer, or keep formatted data in the list itself.
1076
+ # e.g. @list contains file names, or File objects, and this converts to a long listing.
1077
+ # If the renderer did that, the truncation would be on wrong data.
1078
+ # @since 1.2.0
1079
+ def convert_value_to_text value, crow
1080
+ case value
1081
+ when TrueClass, FalseClass
1082
+ value
1083
+ else
1084
+ value.to_s if value
1085
+ end
1086
+ end
1087
+ # takes a block, this way anyone extending this class can just pass a block to do his job
1088
+ # This modifies the string
1089
+ def sanitize content
1090
+ if content.is_a? String
1091
+ content.chomp!
1092
+ content.gsub!(/[\t\n\r]/, ' ') # don't display tab
1093
+ content.gsub!(/[^[:print:]]/, '') # don't display non print characters
1094
+ else
1095
+ content
1096
+ end
1097
+ end
1098
+ # returns only the visible portion of string taking into account display length
1099
+ # and horizontal scrolling. MODIFIES STRING
1100
+ # if you;ve truncated the data, it could stay truncated even if lb is increased. be careful
1101
+ # FIXME if _maxlen becomes 0 then maxlen -1 will print whole string again
1102
+ def truncate content
1103
+ _maxlen = @maxlen || @width-@internal_width
1104
+ _maxlen = @width-@internal_width if _maxlen > @width-@internal_width
1105
+ #$log.debug "TRUNCATE: listbox maxlen #{@maxlen}, #{_maxlen} width #{@width}: #{content} "
1106
+ if !content.nil?
1107
+ if content.length > _maxlen # only show maxlen
1108
+ @longest_line = content.length if content.length > @longest_line
1109
+ #content = content[@pcol..@pcol+maxlen-1]
1110
+ content.replace content[@pcol..@pcol+_maxlen-1]
1111
+ else
1112
+ # can this be avoided if pcol is 0 XXX
1113
+ content.replace content[@pcol..-1] if @pcol > 0
1114
+ end
1115
+ end
1116
+ #$log.debug " content: #{content}"
1117
+ content
1118
+ end
1119
+
1120
+ def list_data_changed
1121
+ if row_count == 0 # added on 2009-02-02 17:13 so cursor not hanging on last row which could be empty
1122
+ init_vars
1123
+ @current_index = 0
1124
+ # I had placed this at some time to get cursor correct. But if this listbox is updated
1125
+ # during entry in another field, then this steals the row. e.g. test1.rb 5
1126
+ #set_form_row
1127
+ end
1128
+ @repaint_required = true
1129
+ end
1130
+ # set cursor column position
1131
+ # if i set col1 to @curpos, i can move around left right if key mapped
1132
+ def set_form_col col1=0
1133
+ # TODO BUFFERED use setrowcol @form.row, col
1134
+ # TODO BUFFERED use cols_panned
1135
+ #col1 ||= 0
1136
+ @cols_panned ||= 0 # RFED16 2010-02-17 23:40
1137
+ # editable listboxes will involve changing cursor and the form issue
1138
+ ## added win_col on 2010-01-04 23:28 for embedded forms BUFFERED TRYING OUT
1139
+ #win_col=@form.window.left
1140
+ win_col = 0 # 2010-02-17 23:19 RFED16
1141
+ #col = win_col + @orig_col + @col_offset + @curpos + @form.cols_panned
1142
+ col2 = win_col + @col + @col_offset + col1 + @cols_panned + @left_margin
1143
+ $log.debug " set_form_col in rlistbox #{@col}+ left_margin #{@left_margin} ( #{col2} ) "
1144
+ #super col+@left_margin
1145
+ #@form.setrowcol @form.row, col2 # added 2009-12-29 18:50 BUFFERED
1146
+ setrowcol nil, col2 # 2010-02-17 23:19 RFED16
1147
+ end
1148
+ #def rowcol
1149
+ ## $log.debug "rlistbox rowcol : #{@row+@row_offset+@winrow}, #{@col+@col_offset}"
1150
+ #win_col=@form.window.left
1151
+ #col2 = win_col + @col + @col_offset + @form.cols_panned + @left_margin
1152
+ #return @row+@row_offset, col2
1153
+ #end
1154
+ # experimental selection of multiple rows via block
1155
+ # specify a block start and then a block end
1156
+ # usage: bind mark_selection to a key. It works as a toggle.
1157
+ # C-c will cancel any selection that has begun.
1158
+ # added on 2009-02-19 22:37
1159
+ def mark_block #selection
1160
+ if @inside_block
1161
+ @inside_block = false
1162
+ end_block #selection
1163
+ else
1164
+ @inside_block = true
1165
+ start_block #selection
1166
+ end
1167
+ end
1168
+ # added on 2009-02-19 22:37
1169
+ def cancel_block
1170
+ @first_index = @last_index = nil
1171
+ @inside_block = false
1172
+ end
1173
+ # sets marker for start of block
1174
+ # added on 2009-02-19 22:37
1175
+ def start_block #selection
1176
+ @first_index = @current_index
1177
+ @last_index = nil
1178
+ end
1179
+ # sets marker for end of block
1180
+ # added on 2009-02-19 22:37
1181
+ def end_block #selection
1182
+ @last_index = current_index
1183
+ lower = [@first_index, @last_index].min
1184
+ higher = [@first_index, @last_index].max
1185
+ #lower.upto(higher) do |i| @list.toggle_row_selection i; end
1186
+ add_row_selection_interval(lower, higher)
1187
+ @repaint_required = true
1188
+ end
1189
+ # 2010-02-18 11:40
1190
+ # TRYING OUT - canceling editing if resized otherwise drawing errors can occur
1191
+ # the earlier painted edited comp in yellow keeps showing until a key is pressed
1192
+
1193
+ def set_buffering params
1194
+ $log.warn "CALLED set_buffering in LISTBOX listbox " if $log.debug?
1195
+ super # removed from widget 2011-09-29
1196
+ ## Ensuring that changes to top get reflect in editing comp
1197
+ #+ otherwise it raises an exception. Still the earlier cell_edit is being
1198
+ #+ printed where it was , until a key is moved
1199
+ # FIXME - do same for col
1200
+ if @cell_editor
1201
+ r,c = rowcol
1202
+ if @cell_editor.component.row < @row_offset + @buffer_params[:screen_top]
1203
+ @cell_editor.component.row = @row_offset + @buffer_params[:screen_top]
1204
+ end
1205
+ # TODO next block to be tested by placing a listbox in right split of vertical
1206
+ if @cell_editor.component.col < @col_offset + @buffer_params[:screen_left]
1207
+ @cell_editor.component.col = @col_offset + @buffer_params[:screen_left]
1208
+ end
1209
+ #editing_canceled @current_index if @cell_editing_allowed and @cell_editor
1210
+ end
1211
+ #set_form_row
1212
+ @repaint_required = true
1213
+ end
1214
+
1215
+ # trying to simplify usage. The Java way has made listboxes very difficult to use
1216
+ # Returns selected indices
1217
+ # Indices are often required since the renderer may modify the values displayed
1218
+ #
1219
+ def get_selected_indices
1220
+ @list_selection_model.get_selected_indices
1221
+ end
1222
+
1223
+ # Returns selected values
1224
+ #
1225
+ def get_selected_values
1226
+ selected = []
1227
+ @list_selection_model.get_selected_indices.each { |i| selected << list_data_model[i] }
1228
+ return selected
1229
+ end
1230
+ alias :selected_values :get_selected_values
1231
+ alias :selected_indices :get_selected_indices
1232
+
1233
+ def init_actions
1234
+ return if @actions_added
1235
+ @actions_added = true
1236
+ am = action_manager()
1237
+ am.add_action(Action.new("&One Key Selection toggle ") { @one_key_selection = !@one_key_selection} )
1238
+ am.add_action(Action.new("&Edit Toggle") { @edit_toggle = !@edit_toggle; $status_message.value = "Edit toggle is #{@edit_toggle}" })
1239
+
1240
+ # this is a little more involved due to list selection model
1241
+ am.add_action(Action.new("&Disable selection") { @selection_mode = :none; unbind_key(32); bind_key(32, :scroll_forward); })
1242
+ end
1243
+ # ADD HERE
1244
+ end # class listb
1245
+
1246
+
1247
+ end # module