ncumbra 0.1.0 → 0.1.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,260 @@
1
+ # ----------------------------------------------------------------------------- #
2
+ # File: table.rb
3
+ # Description: widget for tabular data
4
+ # Author: j kepler http://github.com/mare-imbrium/umbra/
5
+ # Date: 2018-05-06 - 09:56
6
+ # License: MIT
7
+ # Last update: 2018-05-22 14:46
8
+ # ----------------------------------------------------------------------------- #
9
+ # table.rb Copyright (C) 2018 j kepler
10
+
11
+ ##--------- Todo section ---------------
12
+ ## DONE w - next column, b - previous column
13
+ ## TODO paint lines as column separators. issue is panning.
14
+ ## TODO starting visual column (required when scrolling)
15
+ ## DONE change a value value_at(x,y, value) ; << ; delete_at
16
+ ## TODO change column width interactively, hide column , move column
17
+ ## TODO maybe even column_color(n, color_pair, attr)
18
+ ## TODO sort on column/s.
19
+ ## TODO selection will have to be added. maybe we should have extended listbox after all. Or made multiline selectable.
20
+ ## DONE how to format the header
21
+ ## DONE formatting rows
22
+ ## DONE if we want to color specific columns based on values then I think we have to format (render) the row at the last
23
+ ## moment in print_row and not in advance
24
+ ## NOTE: we are setting the data in tabular, not list. So calling list() will give nil until a render has happened.
25
+ ## callers will have to use data() instead of list() which is not consistent.
26
+ ## NOTE: current_index in this object refers to index including header and separator. It is not the offset in the data array.
27
+ ## For that we need to adjust with @data_offset.
28
+ #
29
+ require 'forwardable'
30
+ require 'umbra/tabular'
31
+ require 'umbra/multiline'
32
+
33
+ module Umbra
34
+ ##
35
+ ## A table of columnar data.
36
+ ## This uses Tabular as a table model and extends Multiline.
37
+ #
38
+ class Table < Multiline
39
+
40
+ extend Forwardable
41
+
42
+
43
+ ## tabular is the data model for Table.
44
+ ## It may be passed in in the constructor, or else is created when columns and data are passed in.
45
+ attr_accessor :tabular
46
+
47
+ ## color pair and attribute for header row
48
+ attr_accessor :header_color_pair, :header_attr
49
+
50
+ attr_accessor :rendered ## boolean, if data has changed, we need to re-render
51
+
52
+
53
+ ## Create a Table object passing either a Tabular object or columns and list
54
+ ## e.g. Table.new tabular: tabular
55
+ ## Table.new columns: cols, list: mylist
56
+ ##
57
+ def initialize config={}, &block
58
+ if config.key? :tabular
59
+ @tabular = config.delete(:tabular)
60
+ else
61
+ cols = config.delete(:columns)
62
+ data = config.delete(:list)
63
+ @tabular = Tabular.new cols
64
+ if data
65
+ @tabular.data = data
66
+ end
67
+ end
68
+ @rendered = nil
69
+ super
70
+
71
+ bind_key(?w, "next column") { self.next_column }
72
+ bind_key(?b, "prev column") { self.prev_column }
73
+ bind_key(KEY_RETURN, :fire_action_event)
74
+ ## NOTE: a tabular object should be existing at this point.
75
+ end
76
+
77
+ ## returns the raw data as array of arrays in tabular
78
+ def data
79
+ @tabular.list
80
+ end
81
+
82
+
83
+ def data=(list)
84
+ @rendered = false
85
+ @tabular.data = list
86
+ @repaint_required = true
87
+ self.focusable = true
88
+ @pstart = @current_index = 0
89
+ @pcol = 0
90
+ #$log.debug " before table data= CHANGED "
91
+ #fire_handler(:CHANGED, self) ## added 2018-05-08 -
92
+ end
93
+
94
+ ## render the two-dimensional array of data as an array of Strings.
95
+ ## Calculates data_offset which is the row offset from which data starts.
96
+ def render
97
+ @data_offset = 0
98
+ @data_offset +=1 if @tabular.use_separator
99
+ @data_offset +=1 if @tabular.columns
100
+ self.list = @tabular.render
101
+ end
102
+
103
+ ## paint the table
104
+ def repaint
105
+ render if !@rendered
106
+ super
107
+
108
+ @rendered = true
109
+ end
110
+
111
+ ## Specify how to print the header and separator.
112
+ ## index can be 0 or 1
113
+ ## returns an array of color_pair and attribute
114
+ def color_of_header_row index, state
115
+ arr = [ @header_color_pair || CP_MAGENTA, @header_attr || REVERSE ]
116
+ return arr if index == 0
117
+ [ arr[0], NORMAL ]
118
+ end
119
+
120
+ ## Specify how the data rows are to be coloured.
121
+ ## Override this to have customised row coloring.
122
+ ## @return array of color_pair and attrib.
123
+ def color_of_data_row index, state, data_index
124
+ color_of_row(index, state) ## calling superclass here
125
+ end
126
+
127
+ ## Print the row which could be header or data
128
+ ## @param index [Integer] - index of list, starting with header and separator
129
+ def print_row(win, row, col, str, index, state)
130
+ if index <= @data_offset - 1
131
+ _print_headings(win, row, col, str, index, state)
132
+ else
133
+ data_index = index - @data_offset ## index into actual data object
134
+ _print_data(win, row, col, str, index, state, data_index)
135
+ end
136
+ end
137
+
138
+ ## Print the header row
139
+ ## index [Integer] - should be 0 or 1 (1 for optional separator)
140
+ def _print_headings(win, row, col, str, index, state)
141
+ arr = color_of_header_row(index, state)
142
+ win.printstring(row, col, str, arr[0], arr[1])
143
+ end
144
+
145
+
146
+
147
+ ## Print the data.
148
+ ## index is index into visual row, starting 0 for headings, and 1 for separator
149
+ ## data_index is index into actual data object. Use this if checking actual data array
150
+ def _print_data(win, row, col, str, index, state, data_index)
151
+ data_index = index - @data_offset ## index into actual data object
152
+ arr = color_of_data_row(index, state, data_index)
153
+
154
+ win.printstring(row, col, str, arr[0], arr[1])
155
+ end
156
+ def color_of_column ix, value, defaultcolor
157
+ raise "unused yet"
158
+ end
159
+
160
+
161
+
162
+ def row_count
163
+ @tabular.list.size
164
+ end
165
+
166
+
167
+ ## return rowid (assumed to be first column)
168
+ def current_id
169
+ data = current_row_as_array()
170
+ return nil unless data
171
+ data.first
172
+ end
173
+ # How do I deal with separators and headers here - return nil
174
+ ## This returns all columns including hidden so rowid can be accessed
175
+ def current_row_as_array
176
+ data_index = @current_index - @data_offset ## index into actual data object
177
+ return nil if data_index < 0 ## separator and heading
178
+ data()[data_index]
179
+ end
180
+
181
+ ## returns the current row as a hash with column name as key.
182
+ def current_row_as_hash
183
+ data = current_row_as_array
184
+ return nil unless data
185
+ columns = @tabular.columns
186
+ hash = columns.zip(data).to_h
187
+ end
188
+
189
+ ## Move cursor to next column
190
+ def next_column
191
+ @coffsets = @tabular._calculate_column_offsets unless @coffsets
192
+ #c = @column_pointer.next
193
+ current_column = current_column_offset() +1
194
+ if current_column > @tabular.column_count-1
195
+ current_column = 0
196
+ end
197
+ cp = @coffsets[current_column]
198
+ @curpos = cp if cp
199
+ $log.debug " next_column #{@coffsets} :::: #{cp}, curpos=#{@curpos} "
200
+ set_col_offset @curpos
201
+ #down() if c < @column_pointer.last_index
202
+ #fire_column_event :ENTER_COLUMN
203
+ end
204
+
205
+ ## Move cursor to previous column
206
+ def prev_column
207
+ @coffsets = @tabular._calculate_column_offsets unless @coffsets
208
+ #c = @column_pointer.next
209
+ current_column = current_column_offset() -1
210
+ if current_column < 0 #
211
+ current_column = @tabular.column_count-1
212
+ end
213
+ cp = @coffsets[current_column]
214
+ @curpos = cp if cp
215
+ $log.debug " next_column #{@coffsets} :::: #{cp}, curpos=#{@curpos} "
216
+ set_col_offset @curpos
217
+ #down() if c < @column_pointer.last_index
218
+ #fire_column_event :ENTER_COLUMN
219
+ end
220
+
221
+ # Convert current cursor position to a table column
222
+ # calculate column based on curpos since user may not have
223
+ # used w and b keys (:next_column)
224
+ # @return [Integer] column index base 0
225
+ def current_column_offset
226
+ _calculate_column_offsets unless @coffsets
227
+ x = 0
228
+ @coffsets.each_with_index { |i, ix|
229
+ if @curpos < i
230
+ break
231
+ else
232
+ x += 1
233
+ end
234
+ }
235
+ x -= 1 # since we start offsets with 0, so first auto becoming 1
236
+ return x
237
+ end
238
+
239
+ def header_row?
240
+ @current_index == 0 and @data_offset > 0
241
+ end
242
+
243
+ ## Handle case where ENTER/RETURN pressed on header row (so sorting can be done).
244
+ def fire_action_event
245
+ if header_row?
246
+ # TODO sorting here
247
+ $log.debug " PRESSED ENTER on header row, TODO sorting here"
248
+ end
249
+ super
250
+ end
251
+
252
+ ## delegate calls to the tabular object
253
+ def_delegators :@tabular, :headings=, :columns= , :add, :add_row, :<< , :column_width, :column_align, :column_hide, :convert_value_to_text, :separator, :to_string, :x=, :y=, :column_unhide
254
+ def_delegators :@tabular, :columns , :numbering
255
+ def_delegators :@tabular, :column_hidden, :delete_at, :value_at
256
+
257
+ end # class
258
+ end # module
259
+
260
+ # vim: comments=sr\:##,mb\:##,el\:#/,\:## :
@@ -0,0 +1,431 @@
1
+ #!/usr/bin/env ruby -w
2
+ =begin
3
+ * Name : A Quick take on tabular data. Readonly.
4
+ * Description : To show tabular data inside a control, rather than going by the huge
5
+ Table object, I want to create a simple, minimal table data generator.
6
+ This will be thrown into a TextView for the user to navigate, select
7
+ etc.
8
+ I would use this applications where the tabular data is fairly fixed
9
+ not where i want the user to select columns, move them, expand etc.
10
+ * :
11
+ * Author : jkepler
12
+ * Date :
13
+ * Last Update : 2018-05-20 14:24
14
+ * License : MIT
15
+ =end
16
+
17
+ ## Todo Section --------------
18
+ ## What if user wishes to supply formatstring and override ours
19
+ ## ---------------------------
20
+
21
+
22
+ # A simple tabular data generator. Given table data in arrays and a column heading row in arrays, it
23
+ # quickely generates tabular data. It only takes left and right alignment of columns into account.
24
+ # You may specify individual column widths. Else it will take the widths of the column names you supply
25
+ # in the startup array. You are encouraged to supply column widths.
26
+ # If no columns are specified, and no widths are given, it take the widths of the first row
27
+ # as a model to determine column widths.
28
+ #
29
+ module Umbra
30
+
31
+ class Tabular
32
+ GUESSCOLUMNS = 30
33
+
34
+ def yield_or_eval &block
35
+ return unless block
36
+ if block.arity > 0
37
+ yield self
38
+ else
39
+ self.instance_eval(&block)
40
+ end
41
+ end
42
+
43
+
44
+ ## stores column info internally: name, width and alignment
45
+ class ColumnInfo < Struct.new(:name, :width, :index, :offset, :align, :hidden)
46
+ end
47
+
48
+
49
+
50
+
51
+ ## an array of column titles
52
+ attr_reader :columns
53
+
54
+ ## data which is array of arrays: rows and columns
55
+ attr_reader :list
56
+
57
+ # boolean, does user want lines numbered
58
+ attr_accessor :numbering
59
+
60
+ attr_accessor :use_separator ## boolean. Use a separator line after heading or not.
61
+
62
+ # x is the + character used a field delim in separators
63
+ # y is the field delim used in data rows, default is pipe or bar
64
+ attr_accessor :x, :y
65
+
66
+ # takes first optional argument as array of column names
67
+ # second optional argument as array of data arrays
68
+ # @yield self
69
+ #
70
+ def initialize cols=nil, *args, &block
71
+ @chash = [] # hash of column info, not used
72
+ @_skip_columns = {} # internal, which columns not to calc width of since user has specified
73
+ @separ = @columns = @numbering = nil
74
+ @y = '|'
75
+ @x = '+'
76
+ @use_separator = false
77
+ @_hidden_columns_flag = false
78
+ self.columns = cols if cols
79
+ if !args.empty?
80
+ self.data = args
81
+ end
82
+ yield_or_eval(&block) if block_given?
83
+ end
84
+ #
85
+ # set columns names .
86
+ ## NOTE that we are not clearing chash here. In case, someone changes table and columns.
87
+ # @param [Array<String>] column names, preferably padded out to width for column
88
+ def columns=(array)
89
+ #$log.debug "tabular got columns #{array.count} #{array.inspect} " if $log
90
+ @columns = array
91
+ @columns.each_with_index { |e,i|
92
+ #@chash[i] = ColumnInfo.new(c, c.to_s.length)
93
+ c = get_column(i)
94
+ c.name = e
95
+ c.width = e.to_s.length
96
+ #@chash[i] = c
97
+ #@cw[i] ||= c.to_s.length
98
+ #@calign[i] ||= :left # 2011-09-27 prevent setting later on
99
+ }
100
+ end
101
+ alias :headings= :columns=
102
+ #
103
+ # set data as an array of arrays
104
+ # @param [Array<Array>] data as array of arrays
105
+ def data=(list)
106
+ #puts "got data: #{list.size} " if !$log
107
+ #puts list if !$log
108
+ @list = list
109
+ end
110
+
111
+ # add a row of data
112
+ # @param [Array] an array containing entries for each column
113
+ def add array
114
+ #$log.debug "tabular got add #{array.count} #{array.inspect} " if $log
115
+ @list ||= []
116
+ @list << array
117
+ end
118
+ alias :<< :add
119
+ alias :add_row :add
120
+
121
+ # retrieve the column info structure for the given offset. The offset
122
+ # pertains to the visible offset not actual offset in data model.
123
+ # These two differ when we move a column.
124
+ # @return ColumnInfo object containing width align color bgcolor attrib hidden
125
+ def get_column index
126
+ return @chash[index] if @chash[index]
127
+ # create a new entry since none present
128
+ c = ColumnInfo.new
129
+ c.index = index
130
+ @chash[index] = c
131
+ return c
132
+ end
133
+ # set width of a given column, any data beyond this will be truncated at display time.
134
+ # @param [Number] column offset, starting 0
135
+ # @param [Number] width
136
+ def column_width colindex, width=:NONE
137
+ if width == :NONE
138
+ #return @cw[colindex]
139
+ return get_column(colindex).width
140
+ end
141
+ @_skip_columns[colindex] = true ## don't calculate col width for this.
142
+ get_column(colindex).width = width
143
+ self
144
+ end
145
+
146
+ def column_hidden colindex, flag=:NONE
147
+ if flag == :NONE
148
+ return get_column(colindex).hidden
149
+ #return @chide[colindex]
150
+ end
151
+ @_hidden_columns_flag = true if flag
152
+ #@chide[colindex] = flag
153
+ get_column(colindex).hidden = flag
154
+ self
155
+ end
156
+
157
+ # set alignment of given column offset
158
+ # @param [Number] column offset, starting 0
159
+ # @param [Symbol] :left, :right
160
+ def column_align colindex, lrc=:NONE
161
+ if lrc == :NONE
162
+ return get_column(colindex).align
163
+ #return @calign[colindex]
164
+ end
165
+ raise ArgumentError, "wrong alignment value sent" if ![:right, :left, :center].include? lrc
166
+ get_column(colindex).align = lrc
167
+ self
168
+ end
169
+
170
+ ## return an array of visible columns names
171
+ def visible_column_names
172
+ visible = []
173
+ @chash.each_with_index do |c, ix|
174
+ if !c.hidden
175
+ if block_given?
176
+ yield c.name, ix
177
+ else
178
+ visible << c.name
179
+ end
180
+ end
181
+ end
182
+ return visible unless block_given?
183
+ end
184
+
185
+ ## returns the count of visible columns based on column names.
186
+ ## NOTE: what if no column names gives ???
187
+ def column_count
188
+ visible_column_names().count
189
+ end
190
+
191
+ # yields non-hidden columns (ColumnInfo) and the offset/index
192
+ # This is the order in which columns are to be printed
193
+ def each_column
194
+ @chash.each_with_index { |c, i|
195
+ next if c.hidden
196
+ yield c,i if block_given?
197
+ }
198
+ end
199
+
200
+ ## for the given row, return visible columns as an array
201
+ ## @yield column and index
202
+ def visible_columns(row)
203
+ visible = []
204
+ row.each_with_index do |e, ix|
205
+ hid = @chash[ix].hidden
206
+ if !hid
207
+ if block_given?
208
+ yield e, ix
209
+ else
210
+ visible << e
211
+ end
212
+ end
213
+ end
214
+ return visible if !block_given?
215
+ end
216
+
217
+ #
218
+ # Now returns an array with formatted data
219
+ # @return [Array<String>] array of formatted data
220
+ def render
221
+ raise "tabular:: list is nil " unless @list
222
+ $log.debug " render list:: #{@list.size} "
223
+ #$log.debug " render list:1: #{@list} "
224
+ raise "tabular:: columns is nil " unless @columns
225
+ buffer = []
226
+ @separ = nil
227
+ _guess_col_widths
228
+ rows = @list.size.to_s.length
229
+ #@rows = rows
230
+ fmstr = _prepare_format
231
+ $log.debug "tabular: fmstr:: #{fmstr}"
232
+ $log.debug "tabular: cols: #{@columns}"
233
+ #$log.debug "tabular: data: #{@list}"
234
+
235
+ str = ""
236
+ if @numbering
237
+ str = " "*(rows+1)+@y
238
+ end
239
+ #str << fmstr % visible_column_names()
240
+ str << convert_heading_to_text(visible_column_names(), fmstr)
241
+ buffer << str
242
+ #puts "-" * str.length
243
+ buffer << separator if @use_separator
244
+ if @list ## XXX why wasn't this done in _prepare_format ???? FIXME
245
+ if @numbering
246
+ fmstr = "%#{rows}d "+ @y + fmstr
247
+ end
248
+ #@list.each { |e| puts e.join(@y) }
249
+ count = 0
250
+ @list.each_with_index { |r,i|
251
+ if r == :separator
252
+ buffer << separator
253
+ next
254
+ end
255
+ if @_hidden_columns_flag
256
+ r = visible_columns(r)
257
+ end
258
+ if @numbering
259
+ r.insert 0, count+1
260
+ end
261
+ #value = convert_value_to_text r, count
262
+ value = convert_value_to_text r, fmstr, i
263
+ buffer << value
264
+ count += 1
265
+ }
266
+ end
267
+ buffer
268
+ end
269
+
270
+ ## render_row
271
+ def convert_value_to_text r, fmstr, index
272
+ return fmstr % r;
273
+ end
274
+ def convert_heading_to_text r, fmstr
275
+ return fmstr % r;
276
+ end
277
+ # use this for printing out on terminal
278
+ # NOTE: Do not name this to_s as it will print the entire content in many places in debug statements
279
+ # @example
280
+ # puts t.to_s
281
+ def to_string
282
+ render().join "\n"
283
+ end
284
+
285
+ def value_at x,y, value=:NONE
286
+ if value == :NONE
287
+ return @list[x, y]
288
+ end
289
+ @list[x, y] = value
290
+ end
291
+ def delete_at ix
292
+ return unless @list
293
+ raise ArgumentError, "Argument must be within 0 and #{@list.length}" if ix < 0 or ix >= @list.length
294
+ #fire_dimension_changed
295
+ #@list.delete_at(ix + @_header_adjustment)
296
+ @list.delete_at(ix)
297
+ end
298
+
299
+
300
+
301
+
302
+ def add_separator
303
+ @list << :separator
304
+ end
305
+
306
+ ## This refers to a separator line after the heading and not a field separator.
307
+ ## Badly named !
308
+ def separator
309
+ return @separ if @separ
310
+ str = ""
311
+ if @numbering
312
+ str = "-"*(rows+1)+@x
313
+ end
314
+ each_column { | c, ix|
315
+ v = c.width
316
+ next if v == 0 ## hidden column
317
+ str << "-" * (v+1) + @x
318
+ }
319
+ @separ = str.chop
320
+ end
321
+
322
+ ## This calculates and stores the offset at which each column starts.
323
+ ## Used when going to next column or doing a find for a string in the table.
324
+ def _calculate_column_offsets
325
+ total = 0
326
+ coffsets = []
327
+ ctr = 0
328
+ ## ix will have gaps in between for hidden fields
329
+ each_column { | c, ix|
330
+ v = c.width
331
+ coffsets[ctr] = total
332
+ ctr += 1
333
+ total += v + 2 ## blank space plus separator
334
+ }
335
+ return coffsets
336
+ end
337
+
338
+
339
+ private
340
+ def _guess_col_widths #:nodoc:
341
+ @list.each_with_index { |r, i|
342
+ break if i > GUESSCOLUMNS
343
+ next if r == :separator
344
+ r.each_with_index { |c, j|
345
+ ## we need to skip those columns which user has specified
346
+ next if @_skip_columns[j] == true
347
+ #next if @chide[j]
348
+ next if @chash[j].hidden
349
+ x = c.to_s.length
350
+ w = @chash[j].width
351
+ if !w
352
+ @chash[j].width = x
353
+ else
354
+ @chash[j].width = x if x > w
355
+ end
356
+ }
357
+ }
358
+ end
359
+
360
+ ## prepare formatstring.
361
+ ## NOTE: this is not the final value.
362
+ ## render adds numbering to this, if user has set numbering option.!!!!
363
+ def _prepare_format #:nodoc:
364
+ fmstr = nil
365
+ fmt = []
366
+ each_column { |c, i|
367
+ ## trying a zero for hidden columns
368
+ ## worked but an extra space is added below and the sep
369
+ w = c.width
370
+ case c.align
371
+ when :right
372
+ #fmt << "%.#{w}s "
373
+ fmt << "%#{w}.#{w}s "
374
+ else
375
+ fmt << "%-#{w}.#{w}s "
376
+ end
377
+ }
378
+ ## the next line will put a separator after hidden columns also
379
+ fmstr = fmt.join(@y)
380
+ #puts "format: #{fmstr} " # 2011-12-09 23:09:57
381
+ return fmstr
382
+ end
383
+ end
384
+ end
385
+
386
+ if __FILE__ == $PROGRAM_NAME
387
+ include Umbra
388
+ $log = nil
389
+ t = Tabular.new(['a', 'b'], [1, 2], [3, 4])
390
+ puts t.to_string
391
+ puts
392
+ t = Tabular.new([" Name ", " Number ", " Email "])
393
+ t.add %w{ rahul 32 r@ruby.org }
394
+ t << %w{ _why 133 j@gnu.org }
395
+ t << %w{ Jane 1331 jane@gnu.org }
396
+ t.column_width 1, 10
397
+ t.column_align 1, :right
398
+ puts t.to_string
399
+ puts
400
+
401
+ s = Tabular.new do |b|
402
+ b.columns = %w{ country continent text }
403
+ b << ["india","asia","a warm country" ]
404
+ b << ["japan","asia","a cool country" ]
405
+ b << ["russia","europe","a hot country" ]
406
+ b.column_width 2, 30
407
+ end
408
+ puts s.to_string
409
+ puts
410
+ puts "::::"
411
+ puts
412
+ s = Tabular.new do |b|
413
+ b.columns = %w{ place continent text }
414
+ b << ["india","asia","a warm country" ]
415
+ b << ["japan","asia","a cool country" ]
416
+ b << ["russia","europe","a hot country" ]
417
+ b << ["sydney","australia","a dry country" ]
418
+ b << ["canberra","australia","a dry country" ]
419
+ b << ["ross island","antarctica","a dry country" ]
420
+ b << ["mount terror","antarctica","a windy country" ]
421
+ b << ["mt erebus","antarctica","a cold place" ]
422
+ b << ["siberia","russia","an icy city" ]
423
+ b << ["new york","USA","a fun place" ]
424
+ b.column_width 0, 12
425
+ b.column_width 1, 12
426
+ b.numbering = true
427
+ end
428
+ puts s.to_string
429
+ end
430
+
431
+ # vim: comments=sr\:##,mb\:##,el\:#/,\:## :