rbcurse-experimental 0.0.1 → 0.0.2

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,3 @@
1
+ 2013-04-12 - 13:06
2
+ Added tablewidget which will very soon move to core, as soon as I figure
3
+ out how to go about this.
data/VERSION CHANGED
@@ -1 +1 @@
1
- 0.0.1
1
+ 0.0.2
@@ -0,0 +1,843 @@
1
+ #!/usr/bin/env ruby
2
+ # ----------------------------------------------------------------------------- #
3
+ # File: tablewidget.rb
4
+ # Description: A tabular widget based on textpad
5
+ # Author: rkumar http://github.com/rkumar/rbcurse/
6
+ # Date: 2013-03-29 - 20:07
7
+ # License: Same as Ruby's License (http://www.ruby-lang.org/LICENSE.txt)
8
+ # Last update: 2013-04-12 13:04
9
+ # ----------------------------------------------------------------------------- #
10
+ # tablewidget.rb Copyright (C) 2012-2013 rahul kumar
11
+
12
+ require 'logger'
13
+ require 'rbcurse'
14
+ require 'rbcurse/core/widgets/textpad'
15
+
16
+ ##
17
+ # The motivation to create yet another table widget is because tabular_widget
18
+ # is based on textview etc which have a lot of complex processing and rendering
19
+ # whereas textpad is quite simple. It is easy to just add one's own renderer
20
+ # making the code base simpler to understand and maintain.
21
+ # TODO
22
+ # _ compare to tabular_widget and see what's missing
23
+ # _ filtering rows without losing data
24
+ # . selection stuff
25
+ # x test with resultset from sqlite to see if we can use Array or need to make model
26
+ # should we use a datamodel so resultsets can be sent in, what about tabular
27
+ # _ header to handle events ?
28
+ #
29
+ #
30
+ module RubyCurses
31
+ # column data, one instance for each column
32
+ # index is the index in the data of this column. This index will not change.
33
+ # Order of printing columns is determined by the ordering of the objects.
34
+ class ColumnInfo < Struct.new(:name, :index, :offset, :width, :align, :hidden, :attrib, :color, :bgcolor)
35
+ end
36
+ # a structure that maintains position and gives
37
+ # next and previous taking max index into account.
38
+ # it also circles. Can be used for traversing next component
39
+ # in a form, or container, or columns in a table.
40
+ class Circular < Struct.new(:max_index, :current_index)
41
+ attr_reader :last_index
42
+ attr_reader :current_index
43
+ def initialize m, c=0
44
+ raise "max index cannot be nil" unless m
45
+ @max_index = m
46
+ @current_index = c
47
+ @last_index = c
48
+ end
49
+ def next
50
+ @last_index = @current_index
51
+ if @current_index + 1 > @max_index
52
+ @current_index = 0
53
+ else
54
+ @current_index += 1
55
+ end
56
+ end
57
+ def previous
58
+ @last_index = @current_index
59
+ if @current_index - 1 < 0
60
+ @current_index = @max_index
61
+ else
62
+ @current_index -= 1
63
+ end
64
+ end
65
+ def is_last?
66
+ @current_index == @max_index
67
+ end
68
+ end
69
+
70
+ # This is our default table row sorter.
71
+ # It does a multiple sort and allows for reverse sort also.
72
+ # It's a pretty simple sorter and uses sort, not sort_by.
73
+ # Improvements welcome.
74
+ # Usage: provide model in constructor or using model method
75
+ # Call toggle_sort_order(column_index)
76
+ # Call sort.
77
+ # Currently, this sorts the provided model in-place. Future versions
78
+ # may maintain a copy, or use a table that provides a mapping of model to result.
79
+ # # TODO check if column_sortable
80
+ class DefaultTableRowSorter
81
+ attr_reader :sort_keys
82
+ # model is array of data
83
+ def initialize data_model=nil
84
+ self.model = data_model
85
+ @columns_sort = []
86
+ @sort_keys = nil
87
+ end
88
+ def model=(model)
89
+ @model = model
90
+ @sort_keys = nil
91
+ end
92
+ def sortable colindex, tf
93
+ @columns_sort[colindex] = tf
94
+ end
95
+ def sortable? colindex
96
+ return false if @columns_sort[colindex]==false
97
+ return true
98
+ end
99
+ # should to_s be used for this column
100
+ def use_to_s colindex
101
+ return true # TODO
102
+ end
103
+ # sorts the model based on sort keys and reverse flags
104
+ # @sort_keys contains indices to sort on
105
+ # @reverse_flags is an array of booleans, true for reverse, nil or false for ascending
106
+ def sort
107
+ return unless @model
108
+ return if @sort_keys.empty?
109
+ $log.debug "TABULAR SORT KEYS #{sort_keys} "
110
+ # first row is the header which should remain in place
111
+ # We could have kept column headers separate, but then too much of mucking around
112
+ # with textpad, this way we avoid touching it
113
+ header = @model.delete_at 0
114
+ begin
115
+ # next line often can give error "array within array" - i think on date fields that
116
+ # contain nils
117
+ @model.sort!{|x,y|
118
+ res = 0
119
+ @sort_keys.each { |ee|
120
+ e = ee.abs-1 # since we had offsetted by 1 earlier
121
+ abse = e.abs
122
+ if ee < 0
123
+ res = y[abse] <=> x[abse]
124
+ else
125
+ res = x[e] <=> y[e]
126
+ end
127
+ break if res != 0
128
+ }
129
+ res
130
+ }
131
+ ensure
132
+ @model.insert 0, header if header
133
+ end
134
+ end
135
+ # toggle the sort order if given column offset is primary sort key
136
+ # Otherwise, insert as primary sort key, ascending.
137
+ def toggle_sort_order index
138
+ index += 1 # increase by 1, since 0 won't multiple by -1
139
+ # internally, reverse sort is maintained by multiplying number by -1
140
+ @sort_keys ||= []
141
+ if @sort_keys.first && index == @sort_keys.first.abs
142
+ @sort_keys[0] *= -1
143
+ else
144
+ @sort_keys.delete index # in case its already there
145
+ @sort_keys.delete(index*-1) # in case its already there
146
+ @sort_keys.unshift index
147
+ # don't let it go on increasing
148
+ if @sort_keys.size > 3
149
+ @sort_keys.pop
150
+ end
151
+ end
152
+ end
153
+ def set_sort_keys list
154
+ @sort_keys = list
155
+ end
156
+ end #class
157
+ #
158
+ # TODO see how jtable does the renderers and columns stuff.
159
+ #
160
+ # perhaps we can combine the two but have different methods or some flag
161
+ # that way oter methods can be shared
162
+ class DefaultTableRenderer
163
+
164
+ # source is the textpad or extending widget needed so we can call show_colored_chunks
165
+ # if the user specifies column wise colors
166
+ def initialize source
167
+ @source = source
168
+ @y = '|'
169
+ @x = '+'
170
+ @coffsets = []
171
+ @header_color = :red
172
+ @header_bgcolor = :white
173
+ @header_attrib = NORMAL
174
+ @color = :white
175
+ @bgcolor = :black
176
+ @color_pair = $datacolor
177
+ @attrib = NORMAL
178
+ @_check_coloring = nil
179
+ end
180
+ def header_colors fg, bg
181
+ @header_color = fg
182
+ @header_bgcolor = bg
183
+ end
184
+ def header_attrib att
185
+ @header_attrib = att
186
+ end
187
+ # set fg and bg color of content rows, default is $datacolor (white on black).
188
+ def content_colors fg, bg
189
+ @color = fg
190
+ @bgcolor = bg
191
+ @color_pair = get_color($datacolor, fg, bg)
192
+ end
193
+ def content_attrib att
194
+ @attrib = att
195
+ end
196
+ def column_model c
197
+ @chash = c
198
+ end
199
+ ##
200
+ # Takes the array of row data and formats it using column widths
201
+ # and returns a string which is used for printing
202
+ #
203
+ # TODO return an array so caller can color columns if need be
204
+ def convert_value_to_text r
205
+ str = []
206
+ fmt = nil
207
+ field = nil
208
+ # we need to loop through chash and get index from it and get that row from r
209
+ #r.each_with_index { |e, i|
210
+ #c = @chash[i]
211
+ #@chash.each_with_index { |c, i|
212
+ #next if c.hidden
213
+ each_column {|c,i|
214
+ e = r[c.index]
215
+ w = c.width
216
+ l = e.to_s.length
217
+ # if value is longer than width, then truncate it
218
+ if l > w
219
+ fmt = "%.#{w}s "
220
+ else
221
+ case c.align
222
+ when :right
223
+ fmt = "%#{w}s "
224
+ else
225
+ fmt = "%-#{w}s "
226
+ end
227
+ end
228
+ field = fmt % e
229
+ # if we really want to print a single column with color, we need to print here itself
230
+ # each cell. If we want the user to use tmux formatting in the column itself ...
231
+ # FIXME - this must not be done for headers.
232
+ #if c.color
233
+ #field = "#[fg=#{c.color}]#{field}#[/end]"
234
+ #end
235
+ str << field
236
+ }
237
+ return str
238
+ end
239
+ #
240
+ # @param pad for calling print methods on
241
+ # @param lineno the line number on the pad to print on
242
+ # @param text data to print
243
+ def render pad, lineno, str
244
+ #lineno += 1 # header_adjustment
245
+ return render_header pad, lineno, 0, str if lineno == 0
246
+ #text = str.join " | "
247
+ #text = @fmstr % str
248
+ text = convert_value_to_text str
249
+ if @_check_coloring
250
+ $log.debug "XXX: INSIDE COLORIIN"
251
+ text = colorize pad, lineno, text
252
+ return
253
+ end
254
+ # check if any specific colors , if so then print colors in a loop with no dependence on colored chunks
255
+ # then we don't need source pointer
256
+ text = text.join
257
+ $log.debug "XXX: NOTINSIDE COLORIIN"
258
+ #if text.index "#["
259
+ #require 'rbcurse/core/include/chunk'
260
+ #@parser ||= Chunks::ColorParser.new :tmux
261
+ #text = @parser.convert_to_chunk text
262
+ #FFI::NCurses.wmove pad, lineno, 0
263
+ #@source.show_colored_chunks text, nil, nil
264
+ #return
265
+ #end
266
+ # FIXME why repeatedly getting this colorpair
267
+ cp = @color_pair
268
+ att = @attrib
269
+ FFI::NCurses.wattron(pad,FFI::NCurses.COLOR_PAIR(cp) | att)
270
+ FFI::NCurses.mvwaddstr(pad, lineno, 0, text)
271
+ FFI::NCurses.wattroff(pad,FFI::NCurses.COLOR_PAIR(cp) | att)
272
+
273
+ end
274
+ def render_header pad, lineno, col, columns
275
+ # I could do it once only but if user sets colors midway we can check once whenvever
276
+ # repainting
277
+ check_colors #if @_check_coloring.nil?
278
+ #text = columns.join " | "
279
+ #text = @fmstr % columns
280
+ text = convert_value_to_text columns
281
+ text = text.join
282
+ bg = @header_bgcolor
283
+ fg = @header_color
284
+ att = @header_attrib
285
+ #cp = $datacolor
286
+ cp = get_color($datacolor, fg, bg)
287
+ FFI::NCurses.wattron(pad,FFI::NCurses.COLOR_PAIR(cp) | att)
288
+ FFI::NCurses.mvwaddstr(pad, lineno, col, text)
289
+ FFI::NCurses.wattroff(pad,FFI::NCurses.COLOR_PAIR(cp) | att)
290
+ end
291
+ # check if we need to individually color columns or we can do the entire
292
+ # row in one shot
293
+ def check_colors
294
+ each_column {|c,i|
295
+ #@chash.each_with_index { |c, i|
296
+ #next if c.hidden
297
+ if c.color || c.bgcolor || c.attrib
298
+ @_check_coloring = true
299
+ return
300
+ end
301
+ @_check_coloring = false
302
+ }
303
+ end
304
+ def each_column
305
+ @chash.each_with_index { |c, i|
306
+ next if c.hidden
307
+ yield c,i if block_given?
308
+ }
309
+ end
310
+ def colorize pad, lineno, r
311
+ # the incoming data is already in the order of display based on chash,
312
+ # so we cannot run chash on it again, so how do we get the color info
313
+ _offset = 0
314
+ # we need to get coffsets here FIXME
315
+ #@chash.each_with_index { |c, i|
316
+ #next if c.hidden
317
+ each_column {|c,i|
318
+ text = r[i]
319
+ color = c.color
320
+ bg = c.bgcolor
321
+ if color || bg
322
+ cp = get_color(@color_pair, color || @color, bg || @bgcolor)
323
+ else
324
+ cp = @color_pair
325
+ end
326
+ att = c.attrib || @attrib
327
+ FFI::NCurses.wattron(pad,FFI::NCurses.COLOR_PAIR(cp) | att)
328
+ FFI::NCurses.mvwaddstr(pad, lineno, _offset, text)
329
+ FFI::NCurses.wattroff(pad,FFI::NCurses.COLOR_PAIR(cp) | att)
330
+ _offset += text.length
331
+ }
332
+ end
333
+ end
334
+
335
+ # If we make a pad of the whole thing then the columns will also go out when scrolling
336
+ # So then there's no point storing columns separately. Might as well keep in content
337
+ # so scrolling works fine, otherwise textpad will have issues scrolling.
338
+ # Making a pad of the content but not column header complicates stuff,
339
+ # do we make a pad of that, or print it like the old thing.
340
+ class TableWidget < TextPad
341
+
342
+ dsl_accessor :print_footer
343
+ attr_reader :columns
344
+ attr_accessor :table_row_sorter
345
+
346
+ def initialize form = nil, config={}, &block
347
+
348
+ # hash of column info objects, for some reason a hash and not an array
349
+ @chash = []
350
+ # chash should be an array which is basically the order of rows to be printed
351
+ # it contains index, which is the offset of the row in the data @content
352
+ # When printing we should loop through chash and get the index in data
353
+ #
354
+ # should be zero here, but then we won't get textpad correct
355
+ @_header_adjustment = 0 #1
356
+ @col_min_width = 3
357
+
358
+ super
359
+ bind_key(?w, "next column") { self.next_column }
360
+ bind_key(?b, "prev column") { self.prev_column }
361
+ bind_key(?-, "contract column") { self.contract_column }
362
+ bind_key(?+, "expand column") { self.expand_column }
363
+ bind_key(?=, "expand column to width") { self.expand_column_to_width }
364
+ bind_key(?\M-=, "expand column to width") { self.expand_column_to_max_width }
365
+ end
366
+
367
+ # retrieve the column info structure for the given offset. The offset
368
+ # pertains to the visible offset not actual offset in data model.
369
+ # These two differ when we move a column.
370
+ # @return ColumnInfo object containing width align color bgcolor attrib hidden
371
+ def get_column index
372
+ return @chash[index] if @chash[index]
373
+ # create a new entry since none present
374
+ c = ColumnInfo.new
375
+ c.index = index
376
+ @chash[index] = c
377
+ return c
378
+ end
379
+ ##
380
+ # returns collection of ColumnInfo objects
381
+ def column_model
382
+ @chash
383
+ end
384
+
385
+ # calculate pad width based on widths of columns
386
+ def content_cols
387
+ total = 0
388
+ #@chash.each_pair { |i, c|
389
+ #@chash.each_with_index { |c, i|
390
+ #next if c.hidden
391
+ each_column {|c,i|
392
+ w = c.width
393
+ # if you use prepare_format then use w+2 due to separator symbol
394
+ total += w + 1
395
+ }
396
+ return total
397
+ end
398
+
399
+ #
400
+ # This calculates and stores the offset at which each column starts.
401
+ # Used when going to next column or doing a find for a string in the table.
402
+ # TODO store this inside the hash so it's not calculated again in renderer
403
+ #
404
+ def _calculate_column_offsets
405
+ @coffsets = []
406
+ total = 0
407
+
408
+ #@chash.each_pair { |i, c|
409
+ #@chash.each_with_index { |c, i|
410
+ #next if c.hidden
411
+ each_column {|c,i|
412
+ w = c.width
413
+ @coffsets[i] = total
414
+ c.offset = total
415
+ # if you use prepare_format then use w+2 due to separator symbol
416
+ total += w + 1
417
+ }
418
+ end
419
+ # Convert current cursor position to a table column
420
+ # calculate column based on curpos since user may not have
421
+ # user w and b keys (:next_column)
422
+ # @return [Fixnum] column index base 0
423
+ def _convert_curpos_to_column #:nodoc:
424
+ _calculate_column_offsets unless @coffsets
425
+ x = 0
426
+ @coffsets.each_with_index { |i, ix|
427
+ if @curpos < i
428
+ break
429
+ else
430
+ x += 1
431
+ end
432
+ }
433
+ x -= 1 # since we start offsets with 0, so first auto becoming 1
434
+ return x
435
+ end
436
+ # jump cursor to next column
437
+ # TODO : if cursor goes out of view, then pad should scroll right or left and down
438
+ def next_column
439
+ # TODO take care of multipliers
440
+ _calculate_column_offsets unless @coffsets
441
+ c = @column_pointer.next
442
+ cp = @coffsets[c]
443
+ #$log.debug " next_column #{c} , #{cp} "
444
+ @curpos = cp if cp
445
+ down() if c < @column_pointer.last_index
446
+ end
447
+ # jump cursor to previous column
448
+ # TODO : if cursor goes out of view, then pad should scroll right or left and down
449
+ def prev_column
450
+ # TODO take care of multipliers
451
+ _calculate_column_offsets unless @coffsets
452
+ c = @column_pointer.previous
453
+ cp = @coffsets[c]
454
+ #$log.debug " prev #{c} , #{cp} "
455
+ @curpos = cp if cp
456
+ up() if c > @column_pointer.last_index
457
+ end
458
+ def expand_column
459
+ x = _convert_curpos_to_column
460
+ w = get_column(x).width
461
+ column_width x, w+1 if w
462
+ @coffsets = nil
463
+ fire_dimension_changed
464
+ end
465
+ def expand_column_to_width w=nil
466
+ x = _convert_curpos_to_column
467
+ unless w
468
+ # expand to width of current cell
469
+ s = @content[@current_index][x]
470
+ w = s.to_s.length + 1
471
+ end
472
+ column_width x, w
473
+ @coffsets = nil
474
+ fire_dimension_changed
475
+ end
476
+ # find the width of the longest item in the current columns and expand the width
477
+ # to that.
478
+ def expand_column_to_max_width
479
+ x = _convert_curpos_to_column
480
+ w = calculate_column_width x
481
+ expand_column_to_width w
482
+ end
483
+ def contract_column
484
+ x = _convert_curpos_to_column
485
+ w = get_column(x).width
486
+ return if w <= @col_min_width
487
+ column_width x, w-1 if w
488
+ @coffsets = nil
489
+ fire_dimension_changed
490
+ end
491
+
492
+ #def method_missing(name, *args)
493
+ #@tp.send(name, *args)
494
+ #end
495
+ #
496
+ # supply a custom renderer that implements +render()+
497
+ # @see render
498
+ def renderer r
499
+ @renderer = r
500
+ end
501
+
502
+ ##
503
+ # Set column titles with given array of strings.
504
+ # NOTE: This is only required to be called if first row of file or content does not contain
505
+ # titles. In that case, this should be called before setting the data as the array passed
506
+ # is appended into the content array.
507
+ #
508
+ def columns=(array)
509
+ @_header_adjustment = 1
510
+ # I am eschewing using a separate field for columns. This is simpler for textpad.
511
+ # We always assume first row is columns.
512
+ #@columns = array
513
+ # should we just clear column, otherwise there's no way to set the whole thing with new data
514
+ # but then if we need to change columns what do it do, on moving or hiding a column ?
515
+ # Maybe we need a separate clear method or remove_all TODO
516
+ @content ||= []
517
+ @content << array
518
+ # This needs to go elsewhere since this method will not be called if file contains
519
+ # column titles as first row.
520
+ _init_model array
521
+ end
522
+ alias :headings= :columns=
523
+
524
+ def _init_model array
525
+ array.each_with_index { |c,i|
526
+ # if columns added later we could be overwriting the width
527
+ c = get_column(i)
528
+ c.width ||= 10
529
+ }
530
+ # maintains index in current pointer and gives next or prev
531
+ @column_pointer = Circular.new array.size()-1
532
+ end
533
+ def model_row index
534
+ array = @content[index]
535
+ array.each_with_index { |c,i|
536
+ # if columns added later we could be overwriting the width
537
+ ch = get_column(i)
538
+ ch.width = c.to_s.length + 2
539
+ }
540
+ # maintains index in current pointer and gives next or prev
541
+ @column_pointer = Circular.new array.size()-1
542
+ end
543
+
544
+ ##
545
+ # insert entire database in one shot
546
+ # WARNING: overwrites columns if put there, should contain columns already as in CSV data
547
+ # @param lines is an array or arrays
548
+ def text lines, fmt=:none
549
+ _init_model lines[0]
550
+ fire_dimension_changed
551
+ super
552
+ end
553
+
554
+ ##
555
+ # set column array and data array in one shot
556
+ # Erases any existing content
557
+ def resultset columns, data
558
+ @content = []
559
+ _init_model columns
560
+ @content << columns
561
+ @_header_adjustment = 1
562
+
563
+ @content.concat( data)
564
+ fire_dimension_changed
565
+ end
566
+
567
+
568
+ ## add a row to the table
569
+ def add array
570
+ unless @content
571
+ # columns were not added, this most likely is the title
572
+ @content ||= []
573
+ _init_model array
574
+ end
575
+ @content << array
576
+ fire_dimension_changed
577
+ self
578
+ end
579
+ def delete_at ix
580
+ return unless @content
581
+ fire_dimension_changed
582
+ @content.delete_at ix
583
+ end
584
+ alias :<< :add
585
+ # convenience method to set width of a column
586
+ # @param index of column
587
+ # @param width
588
+ # For setting other attributes, use get_column(index)
589
+ def column_width colindex, width
590
+ get_column(colindex).width = width
591
+ _invalidate_width_cache
592
+ end
593
+ # convenience method to set alignment of a column
594
+ # @param index of column
595
+ # @param align - :right (any other value is taken to be left)
596
+ def column_align colindex, align
597
+ get_column(colindex).align = align
598
+ end
599
+ # convenience method to hide or unhide a column
600
+ # Provided since column offsets need to be recalculated in the case of a width
601
+ # change or visibility change
602
+ def column_hidden colindex, hidden
603
+ get_column(colindex).hidden = hidden
604
+ _invalidate_width_cache
605
+ end
606
+ # http://www.opensource.apple.com/source/gcc/gcc-5483/libjava/javax/swing/table/DefaultTableColumnModel.java
607
+ def _invalidate_width_cache #:nodoc:
608
+ @coffsets = nil
609
+ end
610
+ ##
611
+ # should all this move into table column model or somepn
612
+ # move a column from offset ix to offset newix
613
+ def move_column ix, newix
614
+ acol = @chash.delete_at ix
615
+ @chash.insert newix, acol
616
+ _invalidate_width_cache
617
+ #tmce = TableColumnModelEvent.new(ix, newix, self, :MOVE)
618
+ #fire_handler :TABLE_COLUMN_MODEL_EVENT, tmce
619
+ end
620
+ def add_column tc
621
+ raise "to figure out add_column"
622
+ _invalidate_width_cache
623
+ end
624
+ def remove_column tc
625
+ raise "to figure out add_column"
626
+ _invalidate_width_cache
627
+ end
628
+ def calculate_column_width col, maxrows=99
629
+ ret = 3
630
+ ctr = 0
631
+ @content.each_with_index { |r, i|
632
+ #next if i < @toprow # this is also a possibility, it checks visible rows
633
+ break if ctr > maxrows
634
+ ctr += 1
635
+ #next if r == :separator
636
+ c = r[col]
637
+ x = c.to_s.length
638
+ ret = x if x > ret
639
+ }
640
+ ret
641
+ end
642
+ ##
643
+ # refresh pad onto window
644
+ # overrides super
645
+ def padrefresh
646
+ top = @window.top
647
+ left = @window.left
648
+ sr = @startrow + top
649
+ sc = @startcol + left
650
+ # first do header always in first row
651
+ retval = FFI::NCurses.prefresh(@pad,0,@pcol, sr , sc , 2 , @cols+ sc );
652
+ # now print rest of data
653
+ # h is header_adjustment
654
+ h = 1
655
+ retval = FFI::NCurses.prefresh(@pad,@prow + h,@pcol, sr + h , sc , @rows + sr , @cols+ sc );
656
+ $log.warn "XXX: PADREFRESH #{retval}, #{@prow}, #{@pcol}, #{sr}, #{sc}, #{@rows+sr}, #{@cols+sc}." if retval == -1
657
+ # padrefresh can fail if width is greater than NCurses.COLS
658
+ end
659
+
660
+ def create_default_sorter
661
+ raise "Data not sent in." unless @content
662
+ @table_row_sorter = DefaultTableRowSorter.new @content
663
+ end
664
+ def header_row?
665
+ @prow == 0
666
+ end
667
+
668
+ def fire_action_event
669
+ if header_row?
670
+ if @table_row_sorter
671
+ x = _convert_curpos_to_column
672
+ c = @chash[x]
673
+ # convert to index in data model since sorter only has data_model
674
+ index = c.index
675
+ @table_row_sorter.toggle_sort_order index
676
+ @table_row_sorter.sort
677
+ fire_dimension_changed
678
+ end
679
+ end
680
+ super
681
+ end
682
+ ##
683
+ # Find the next row that contains given string
684
+ # Overrides textpad since each line is an array
685
+ # NOTE does not go to next match within row
686
+ # NOTE: FIXME ensure_visible puts prow = current_index so in this case, the header
687
+ # overwrites the matched row.
688
+ # @return row and col offset of match, or nil
689
+ # @param String to find
690
+ def next_match str
691
+ _calculate_column_offsets unless @coffsets
692
+ first = nil
693
+ ## content can be string or Chunkline, so we had to write <tt>index</tt> for this.
694
+ @content.each_with_index do |fields, ix|
695
+ #col = line.index str
696
+ #fields.each_with_index do |f, jx|
697
+ #@chash.each_with_index do |c, jx|
698
+ #next if c.hidden
699
+ each_column do |c,jx|
700
+ f = fields[c.index]
701
+ # value can be numeric
702
+ col = f.to_s.index str
703
+ if col
704
+ col += @coffsets[jx]
705
+ first ||= [ ix, col ]
706
+ if ix > @current_index
707
+ return [ix, col]
708
+ end
709
+ end
710
+ end
711
+ end
712
+ return first
713
+ end
714
+ # yields each column to caller method
715
+ # for true returned, collects index of row into array and returns the array
716
+ # @returns array of indices which can be empty
717
+ # Value yielded can be fixnum or date etc
718
+ def matching_indices
719
+ raise "block required for matching_indices" unless block_given?
720
+ @indices = []
721
+ ## content can be string or Chunkline, so we had to write <tt>index</tt> for this.
722
+ @content.each_with_index do |fields, ix|
723
+ flag = yield ix, fields
724
+ if flag
725
+ @indices << ix
726
+ end
727
+ end
728
+ $log.debug "XXX: INDICES found #{@indices}"
729
+ if @indices.count > 0
730
+ fire_dimension_changed
731
+ init_vars
732
+ else
733
+ @indices = nil
734
+ end
735
+ #return @indices
736
+ end
737
+ def clear_matches
738
+ # clear previous match so all data can show again
739
+ if @indices && @indices.count > 0
740
+ fire_dimension_changed
741
+ init_vars
742
+ end
743
+ @indices = nil
744
+ end
745
+ ##
746
+ # Ensure current row is visible, if not make it first row
747
+ # This overrides textpad due to header_adjustment, otherwise
748
+ # during next_match, the header overrides the found row.
749
+ # @param current_index (default if not given)
750
+ #
751
+ def ensure_visible row = @current_index
752
+ unless is_visible? row
753
+ @prow = @current_index - @_header_adjustment
754
+ end
755
+ end
756
+ #
757
+ # yields non-hidden columns (ColumnInfo) and the offset/index
758
+ # This is the order in which columns are to be printed
759
+ def each_column
760
+ @chash.each_with_index { |c, i|
761
+ next if c.hidden
762
+ yield c,i if block_given?
763
+ }
764
+ end
765
+ def render_all
766
+ if @indices && @indices.count > 0
767
+ @indices.each_with_index do |ix, jx|
768
+ render @pad, jx, @content[ix]
769
+ end
770
+ else
771
+ @content.each_with_index { |line, ix|
772
+ #FFI::NCurses.mvwaddstr(@pad,ix, 0, @content[ix])
773
+ render @pad, ix, line
774
+ }
775
+ end
776
+ end
777
+
778
+ end # class TableWidget
779
+
780
+ ##
781
+ # Handles selection of items in a list or table or tree that uses stable indices.
782
+ # Indexes are in the order they were places, not sorted.
783
+ # This is just a wrapper over an array, except that it fires an event so users can bind
784
+ # to row selection and deselection
785
+ # TODO - fire events to listeners
786
+ #
787
+ class ListSelectionModel
788
+ ##
789
+ # obj is the source object, I am wondering whether i need it or not
790
+ def initialize component
791
+ @obj = component
792
+ @selected_indices = []
793
+ end
794
+ def toggle_row_selection crow
795
+ if is_row_selected? crow
796
+ unselect crow
797
+ else
798
+ select crow
799
+ end
800
+ end
801
+ def select ix
802
+ @selected_indices << ix
803
+ _fire_event ix, ix, :INSERT
804
+ end
805
+ def unselect ix
806
+ @selected_indices.delete ix
807
+ _fire_event ix, ix, :DELETE
808
+ end
809
+ alias :add_to_selection :select
810
+ alias :remove_from_selection :unselect
811
+ def clear_selection
812
+ @selected_indices = []
813
+ _fire_event 0, 0, :CLEAR
814
+ end
815
+ def is_row_selected? crow
816
+ @selected_indices.include? crow
817
+ end
818
+ def is_selection_empty?
819
+ return @selected_indices.empty?
820
+ end
821
+ # if row deleted in list, then synch with list
822
+ # (No listeners are informed)
823
+ def remove_index crow
824
+ @selected_indices.delete crow
825
+ end
826
+ def _fire_event firsti, lasti, event
827
+ lse = ListSelectionEvent.new(firsti, lasti, self, event)
828
+ fire_handler :LIST_SELECTION_EVENT, lse
829
+ end
830
+
831
+ def select_all
832
+ # how do we do this since we don't know what the indices are.
833
+ # What is the user using as identifier?
834
+ end
835
+
836
+ # returns a list of selected indices in the same order as added
837
+ def selected_rows
838
+ @selected_indices
839
+ end
840
+ end # class
841
+ class ListSelectionEvent < Struct.new(:firstrow, :lastrow, :source, :type)
842
+ end
843
+ end # module
@@ -0,0 +1,56 @@
1
+ # Generated by jeweler
2
+ # DO NOT EDIT THIS FILE DIRECTLY
3
+ # Instead, edit Jeweler::Tasks in Rakefile, and run 'rake gemspec'
4
+ # -*- encoding: utf-8 -*-
5
+
6
+ Gem::Specification.new do |s|
7
+ s.name = "rbcurse-experimental"
8
+ s.version = "0.0.2"
9
+
10
+ s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
11
+ s.authors = ["Rahul Kumar"]
12
+ s.date = "2013-04-12"
13
+ s.description = "Ruby curses/ncurses widgets, experimental and minimally tested. Copy into your project, do not depend on this gem."
14
+ s.email = "sentinel1879@gmail.com"
15
+ s.extra_rdoc_files = [
16
+ "README.markdown"
17
+ ]
18
+ s.files = [
19
+ "CHANGELOG",
20
+ "README.markdown",
21
+ "VERSION",
22
+ "examples/teststackflow.rb",
23
+ "lib/rbcurse/experimental/widgets/directorylist.rb",
24
+ "lib/rbcurse/experimental/widgets/directorytree.rb",
25
+ "lib/rbcurse/experimental/widgets/masterdetail.rb",
26
+ "lib/rbcurse/experimental/widgets/multiform.rb",
27
+ "lib/rbcurse/experimental/widgets/resultsetbrowser.rb",
28
+ "lib/rbcurse/experimental/widgets/resultsettextview.rb",
29
+ "lib/rbcurse/experimental/widgets/rscrollform.rb",
30
+ "lib/rbcurse/experimental/widgets/stackflow.rb",
31
+ "lib/rbcurse/experimental/widgets/tablewidget.rb",
32
+ "lib/rbcurse/experimental/widgets/undomanager.rb",
33
+ "rbcurse-experimental.gemspec"
34
+ ]
35
+ s.homepage = "http://github.com/rkumar/rbcurse-experimental"
36
+ s.require_paths = ["lib"]
37
+ s.rubyforge_project = "rbcurse"
38
+ s.rubygems_version = "1.8.25"
39
+ s.summary = "Ruby Ncurses Toolkit experimental widgets"
40
+
41
+ if s.respond_to? :specification_version then
42
+ s.specification_version = 3
43
+
44
+ if Gem::Version.new(Gem::VERSION) >= Gem::Version.new('1.2.0') then
45
+ s.add_runtime_dependency(%q<rbcurse-core>, [">= 0.0.3"])
46
+ s.add_runtime_dependency(%q<rbcurse-extras>, [">= 0.0"])
47
+ else
48
+ s.add_dependency(%q<rbcurse-core>, [">= 0.0.3"])
49
+ s.add_dependency(%q<rbcurse-extras>, [">= 0.0"])
50
+ end
51
+ else
52
+ s.add_dependency(%q<rbcurse-core>, [">= 0.0.3"])
53
+ s.add_dependency(%q<rbcurse-extras>, [">= 0.0"])
54
+ end
55
+ end
56
+
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: rbcurse-experimental
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.1
4
+ version: 0.0.2
5
5
  prerelease:
6
6
  platform: ruby
7
7
  authors:
@@ -9,7 +9,7 @@ authors:
9
9
  autorequire:
10
10
  bindir: bin
11
11
  cert_chain: []
12
- date: 2012-12-14 00:00:00.000000000 Z
12
+ date: 2013-04-12 00:00:00.000000000 Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
15
  name: rbcurse-core
@@ -43,13 +43,15 @@ dependencies:
43
43
  - - ! '>='
44
44
  - !ruby/object:Gem::Version
45
45
  version: '0.0'
46
- description: Ruby curses/ncurses widgets, experimental and minimally tested
46
+ description: Ruby curses/ncurses widgets, experimental and minimally tested. Copy
47
+ into your project, do not depend on this gem.
47
48
  email: sentinel1879@gmail.com
48
49
  executables: []
49
50
  extensions: []
50
51
  extra_rdoc_files:
51
52
  - README.markdown
52
53
  files:
54
+ - CHANGELOG
53
55
  - README.markdown
54
56
  - VERSION
55
57
  - examples/teststackflow.rb
@@ -61,7 +63,9 @@ files:
61
63
  - lib/rbcurse/experimental/widgets/resultsettextview.rb
62
64
  - lib/rbcurse/experimental/widgets/rscrollform.rb
63
65
  - lib/rbcurse/experimental/widgets/stackflow.rb
66
+ - lib/rbcurse/experimental/widgets/tablewidget.rb
64
67
  - lib/rbcurse/experimental/widgets/undomanager.rb
68
+ - rbcurse-experimental.gemspec
65
69
  homepage: http://github.com/rkumar/rbcurse-experimental
66
70
  licenses: []
67
71
  post_install_message:
@@ -82,7 +86,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
82
86
  version: '0'
83
87
  requirements: []
84
88
  rubyforge_project: rbcurse
85
- rubygems_version: 1.8.23
89
+ rubygems_version: 1.8.25
86
90
  signing_key:
87
91
  specification_version: 3
88
92
  summary: Ruby Ncurses Toolkit experimental widgets