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.
- data/CHANGELOG +3 -0
- data/VERSION +1 -1
- data/lib/rbcurse/experimental/widgets/tablewidget.rb +843 -0
- data/rbcurse-experimental.gemspec +56 -0
- metadata +8 -4
data/CHANGELOG
ADDED
data/VERSION
CHANGED
@@ -1 +1 @@
|
|
1
|
-
0.0.
|
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.
|
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:
|
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.
|
89
|
+
rubygems_version: 1.8.25
|
86
90
|
signing_key:
|
87
91
|
specification_version: 3
|
88
92
|
summary: Ruby Ncurses Toolkit experimental widgets
|