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.
- 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
|