rbcurse-experimental 0.0.1 → 0.0.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,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