#!/usr/bin/env ruby
# header {{{
# vim: set foldlevel=0 foldmethod=marker :
# ----------------------------------------------------------------------------- #
# File: table.rb
# Description: A tabular widget based on textpad
# Author: jkepler http://github.com/mare-imbrium/canis/
# Date: 2013-03-29 - 20:07
# License: Same as Ruby's License (http://www.ruby-lang.org/LICENSE.txt)
# Last update: 2017-03-09 23:14
# ----------------------------------------------------------------------------- #
# table.rb Copyright (C) 2012-2014 kepler
# == CHANGES:
# - changed @content to @list since all multirow wids and utils expect @list
# - changed name from tablewidget to table
#
# == TODO
# [ ] if no columns, then init_model is called so chash is not cleared.
# _ compare to tabular_widget and see what's missing
# _ filtering rows without losing data
# . selection stuff
# x test with resultset from sqlite to see if we can use Array or need to make model
# should we use a datamodel so resultsets can be sent in, what about tabular
# _ header to handle events ?
# header }}}
require 'logger'
require 'canis'
require 'canis/core/widgets/textpad'
##
# The motivation to create yet another table widget is because tabular_widget
# is based on textview etc which have a lot of complex processing and rendering
# whereas textpad is quite simple. It is easy to just add one's own renderer
# making the code base simpler to understand and maintain.
#
#
module Canis
# structures {{{
# column data, one instance for each column
# index is the index in the data of this column. This index will not change.
# Order of printing columns is determined by the ordering of the objects.
class ColumnInfo < Struct.new(:name, :index, :offset, :width, :align, :hidden, :attrib, :color, :bgcolor)
end
# a structure that maintains position and gives
# next and previous taking max index into account.
# it also circles. Can be used for traversing next component
# in a form, or container, or columns in a table.
class Circular < Struct.new(:max_index, :current_index)
attr_reader :last_index
attr_reader :current_index
def initialize m, c=0
raise "max index cannot be nil" unless m
@max_index = m
@current_index = c
@last_index = c
end
def next
@last_index = @current_index
if @current_index + 1 > @max_index
@current_index = 0
else
@current_index += 1
end
end
def previous
@last_index = @current_index
if @current_index - 1 < 0
@current_index = @max_index
else
@current_index -= 1
end
end
def is_last?
@current_index == @max_index
end
end
# structures }}}
# sorter {{{
# This is our default table row sorter.
# It does a multiple sort and allows for reverse sort also.
# It's a pretty simple sorter and uses sort, not sort_by.
# Improvements welcome.
# Usage: provide model in constructor or using model method
# Call toggle_sort_order(column_index)
# Call sort.
# Currently, this sorts the provided model in-place. Future versions
# may maintain a copy, or use a table that provides a mapping of model to result.
# # TODO check if column_sortable
class DefaultTableRowSorter
attr_reader :sort_keys
# model is array of data
def initialize data_model=nil
self.model = data_model
@columns_sort = []
@sort_keys = nil
end
def model=(model)
@model = model
@sort_keys = nil
end
def sortable colindex, tf
@columns_sort[colindex] = tf
end
def sortable? colindex
return false if @columns_sort[colindex]==false
return true
end
# should to_s be used for this column
def use_to_s colindex
return true # TODO
end
# sorts the model based on sort keys and reverse flags
# @sort_keys contains indices to sort on
# @reverse_flags is an array of booleans, true for reverse, nil or false for ascending
def sort
return unless @model
return if @sort_keys.empty?
$log.debug "TABULAR SORT KEYS #{sort_keys} "
# first row is the header which should remain in place
# We could have kept column headers separate, but then too much of mucking around
# with textpad, this way we avoid touching it
header = @model.delete_at 0
begin
# next line often can give error "array within array" - i think on date fields that
# contain nils
@model.sort!{|x,y|
res = 0
@sort_keys.each { |ee|
e = ee.abs-1 # since we had offsetted by 1 earlier
abse = e.abs
if ee < 0
xx = x[abse]
yy = y[abse]
# the following checks are since nil values cause an error to be raised
if xx.nil? && yy.nil?
res = 0
elsif xx.nil?
res = 1
elsif yy.nil?
res = -1
else
res = y[abse] <=> x[abse]
end
else
xx = x[e]
yy = y[e]
# the following checks are since nil values cause an error to be raised
# whereas we want a nil to be wither treated as a zero or a blank
if xx.nil? && yy.nil?
res = 0
elsif xx.nil?
res = -1
elsif yy.nil?
res = 1
else
res = x[e] <=> y[e]
end
end
break if res != 0
}
res
}
ensure
@model.insert 0, header if header
end
end
# toggle the sort order if given column offset is primary sort key
# Otherwise, insert as primary sort key, ascending.
def toggle_sort_order index
index += 1 # increase by 1, since 0 won't multiple by -1
# internally, reverse sort is maintained by multiplying number by -1
@sort_keys ||= []
if @sort_keys.first && index == @sort_keys.first.abs
@sort_keys[0] *= -1
else
@sort_keys.delete index # in case its already there
@sort_keys.delete(index*-1) # in case its already there
@sort_keys.unshift index
# don't let it go on increasing
if @sort_keys.size > 3
@sort_keys.pop
end
end
end
def set_sort_keys list
@sort_keys = list
end
end #class
# sorter }}}
# renderer {{{
#
# TODO see how jtable does the renderers and columns stuff.
#
# perhaps we can combine the two but have different methods or some flag
# that way oter methods can be shared
class DefaultTableRenderer
# source is the textpad or extending widget needed so we can call show_colored_chunks
# if the user specifies column wise colors
def initialize source
@source = source
@y = '|'
@x = '+'
@coffsets = []
@header_color = :white
@header_bgcolor = :red
@header_attrib = NORMAL
@color = :white
@bgcolor = :black
@color_pair = $datacolor
@attrib = NORMAL
@_check_coloring = nil
# adding setting column_model auto on 2014-04-10 - 10:53 why wasn;t this here already
column_model(source.column_model)
end
def header_colors fg, bg
@header_color = fg
@header_bgcolor = bg
end
def header_attrib att
@header_attrib = att
end
# set fg and bg color of content rows, default is $datacolor (white on black).
def content_colors fg, bg
@color = fg
@bgcolor = bg
@color_pair = get_color($datacolor, fg, bg)
end
def content_attrib att
@attrib = att
end
# set column model (Table Renderer)
def column_model c
@chash = c
end
##
# Takes the array of row data and formats it using column widths
# and returns an array which is used for printing
#
# return an array so caller can color columns if need be
def convert_value_to_text r
str = []
fmt = nil
field = nil
# we need to loop through chash and get index from it and get that row from r
each_column {|c,i|
e = r[c.index]
w = c.width
l = e.to_s.length
# if value is longer than width, then truncate it
if l > w
fmt = "%.#{w}s "
else
case c.align
when :right
fmt = "%#{w}s "
else
fmt = "%-#{w}s "
end
end
field = fmt % e
# if we really want to print a single column with color, we need to print here itself
# each cell. If we want the user to use tmux formatting in the column itself ...
# FIXME - this must not be done for headers.
#if c.color
#field = "#[fg=#{c.color}]#{field}#[/end]"
#end
str << field
}
return str
end
# return a string representation of the row so that +index+ can be applied to it.
# This must take into account columns widths and offsets. This is used by textpad's
# next_match method
def to_searchable arr
convert_value_to_text(arr).join
end
#
# @param pad for calling print methods on
# @param lineno the line number on the pad to print on
# @param [String] data to print which will be an array (@list[index])
def render pad, lineno, str
#lineno += 1 # header_adjustment
# header_adjustment means columns have been set
return render_header pad, lineno, 0, str if lineno == 0 && @source.header_adjustment > 0
#text = str.join " | "
#text = @fmstr % str
text = convert_value_to_text str
if @_check_coloring
#$log.debug "XXX: INSIDE COLORIIN"
text = colorize pad, lineno, text
return
end
# check if any specific colors , if so then print colors in a loop with no dependence on colored chunks
# then we don't need source pointer
render_data pad, lineno, text
end
# passes padded data for final printing or data row
# this allows user to do row related coloring without having to tamper
# with the headers or other internal workings. This will not be called
# if column specific colorign is in effect.
# @param text is an array of strings, in the order of actual printing with hidden cols removed
def render_data pad, lineno, text
text = text.join
# FIXME why repeatedly getting this colorpair
cp = @color_pair
att = @attrib
# added for selection, but will crash if selection is not extended !!! XXX
if @source.is_row_selected? lineno
att = REVERSE
# FIXME currentl this overflows into next row
end
FFI::NCurses.wattron(pad,FFI::NCurses.COLOR_PAIR(cp) | att)
FFI::NCurses.mvwaddstr(pad, lineno, 0, text)
FFI::NCurses.wattroff(pad,FFI::NCurses.COLOR_PAIR(cp) | att)
end
def render_header pad, lineno, col, columns
# I could do it once only but if user sets colors midway we can check once whenvever
# repainting
check_colors #if @_check_coloring.nil?
#text = columns.join " | "
#text = @fmstr % columns
text = convert_value_to_text columns
text = text.join
bg = @header_bgcolor
fg = @header_color
att = @header_attrib
#cp = $datacolor
cp = get_color($datacolor, fg, bg)
FFI::NCurses.wattron(pad,FFI::NCurses.COLOR_PAIR(cp) | att)
FFI::NCurses.mvwaddstr(pad, lineno, col, text)
FFI::NCurses.wattroff(pad,FFI::NCurses.COLOR_PAIR(cp) | att)
end
# check if we need to individually color columns or we can do the entire
# row in one shot
def check_colors
each_column {|c,i|
if c.color || c.bgcolor || c.attrib
@_check_coloring = true
return
end
@_check_coloring = false
}
end
def each_column
@chash.each_with_index { |c, i|
next if c.hidden
yield c,i if block_given?
}
end
def colorize pad, lineno, r
# the incoming data is already in the order of display based on chash,
# so we cannot run chash on it again, so how do we get the color info
_offset = 0
each_column {|c,i|
text = r[i]
color = c.color
bg = c.bgcolor
if color || bg
cp = get_color(@color_pair, color || @color, bg || @bgcolor)
else
cp = @color_pair
end
att = c.attrib || @attrib
FFI::NCurses.wattron(pad,FFI::NCurses.COLOR_PAIR(cp) | att)
FFI::NCurses.mvwaddstr(pad, lineno, _offset, text)
FFI::NCurses.wattroff(pad,FFI::NCurses.COLOR_PAIR(cp) | att)
_offset += text.length
}
end
end
# renderer }}}
#--
# If we make a pad of the whole thing then the columns will also go out when scrolling
# So then there's no point storing columns separately. Might as well keep in content
# so scrolling works fine, otherwise textpad will have issues scrolling.
# Making a pad of the content but not column header complicates stuff,
# do we make a pad of that, or print it like the old thing.
#++
# A table widget containing rows and columns and the ability to resize and hide or align
# columns. Also may have first row as column names.
#
# == NOTE
# The most important methods to use probably are `text()` or `resultset` or `filename` to load
# data. With `text` you will want to first specify column names with `columns()`.
#
# +@current_index+ inherited from +Textpad+ continues to be the index of the list that has user's
# focus, and should be used for row operations.
#
# In order to use Textpad easily, the first row of the table model is the column names. Data is maintained
# in an Array. Several operations are delegated to Array, or have the same name. You can get the list
# using `list()` to run other Array operations on it.
#
# If you modify the Array directly, you may have to use `fire_row_changed(index)` to reflect the update to
# a single row. If you delete or add a row, you will have to use `fire_dimension_changed()`. However,
# internal functions do this automatically.
#
require 'canis/core/include/listselectionmodel'
class Table < TextPad
dsl_accessor :print_footer
#attr_reader :columns
attr_accessor :table_row_sorter
def initialize form = nil, config={}, &block
# array of column info objects
@chash = []
# chash should be an array which is basically the order of rows to be printed
# it contains index, which is the offset of the row in the data @list
# When printing we should loop through chash and get the index in data
#
# should be zero here, but then we won't get textpad correct
@_header_adjustment = 0 #1
@col_min_width = 3
self.extend DefaultListSelection
super
create_default_renderer unless @renderer # 2014-04-10 - 11:01
# NOTE listselection takes + and - for ask_select
bind_key(?w, "next column") { self.next_column }
bind_key(?b, "prev column") { self.prev_column }
bind_key(?\M-\-, "contract column") { self.contract_column }
bind_key(?\M-+, "expand column") { self.expand_column }
bind_key(?=, "expand column to width") { self.expand_column_to_width }
bind_key(?\M-=, "expand column to width") { self.expand_column_to_max_width }
bind_key(?\C-s, "Save as") { self.save_as(nil) }
#@list_selection_model ||= DefaultListSelectionModel.new self
set_default_selection_model unless @list_selection_model
end
# set the default selection model as the operational one
def set_default_selection_model
@list_selection_model = nil
@list_selection_model = Canis::DefaultListSelectionModel.new self
end
# retrieve the column info structure for the given offset. The offset
# pertains to the visible offset not actual offset in data model.
# These two differ when we move a column.
# @return ColumnInfo object containing width align color bgcolor attrib hidden
def get_column index
return @chash[index] if @chash[index]
# create a new entry since none present
c = ColumnInfo.new
c.index = index
@chash[index] = c
return c
end
##
# returns collection of ColumnInfo objects
def column_model
@chash
end
# calculate pad width based on widths of columns
def content_cols
total = 0
#@chash.each_pair { |i, c|
#@chash.each_with_index { |c, i|
#next if c.hidden
each_column {|c,i|
w = c.width
# if you use prepare_format then use w+2 due to separator symbol
total += w + 1
}
return total
end
#
# This calculates and stores the offset at which each column starts.
# Used when going to next column or doing a find for a string in the table.
# TODO store this inside the hash so it's not calculated again in renderer
#
def _calculate_column_offsets
@coffsets = []
total = 0
#@chash.each_pair { |i, c|
#@chash.each_with_index { |c, i|
#next if c.hidden
each_column {|c,i|
w = c.width
@coffsets[i] = total
c.offset = total
# if you use prepare_format then use w+2 due to separator symbol
total += w + 1
}
end
# Convert current cursor position to a table column
# calculate column based on curpos since user may not have
# user w and b keys (:next_column)
# @return [Integer] column index base 0
def _convert_curpos_to_column #:nodoc:
_calculate_column_offsets unless @coffsets
x = 0
@coffsets.each_with_index { |i, ix|
if @curpos < i
break
else
x += 1
end
}
x -= 1 # since we start offsets with 0, so first auto becoming 1
return x
end
# convert the row into something searchable so that offsets returned by +index+
# are exactly what is seen on the screen.
def to_searchable index
if @renderer
@renderer.to_searchable(@list[index])
else
@list[index].to_s
end
end
# jump cursor to next column
# TODO : if cursor goes out of view, then pad should scroll right or left and down
def next_column
# TODO take care of multipliers
_calculate_column_offsets unless @coffsets
c = @column_pointer.next
cp = @coffsets[c]
#$log.debug " next_column #{c} , #{cp} "
@curpos = cp if cp
down() if c < @column_pointer.last_index
fire_column_event :ENTER_COLUMN
end
# jump cursor to previous column
# TODO : if cursor goes out of view, then pad should scroll right or left and down
def prev_column
# TODO take care of multipliers
_calculate_column_offsets unless @coffsets
c = @column_pointer.previous
cp = @coffsets[c]
#$log.debug " prev #{c} , #{cp} "
@curpos = cp if cp
up() if c > @column_pointer.last_index
fire_column_event :ENTER_COLUMN
end
# a column traversal has happened.
# FIXME needs to be looked into. is this consistent naming wise and are we using the correct object
# In old system it was TABLE_TRAVERSAL_EVENT
def fire_column_event eve
require 'canis/core/include/ractionevent'
aev = TextActionEvent.new self, eve, get_column(@column_pointer.current_index), @column_pointer.current_index, @column_pointer.last_index
fire_handler eve, aev
end
def expand_column
x = _convert_curpos_to_column
w = get_column(x).width
column_width x, w+1 if w
@coffsets = nil
fire_dimension_changed
end
def expand_column_to_width w=nil
x = _convert_curpos_to_column
unless w
# expand to width of current cell
s = @list[@current_index][x]
w = s.to_s.length + 1
end
column_width x, w
@coffsets = nil
fire_dimension_changed
end
# find the width of the longest item in the current columns and expand the width
# to that.
def expand_column_to_max_width
x = _convert_curpos_to_column
w = calculate_column_width x
expand_column_to_width w
end
def contract_column
x = _convert_curpos_to_column
w = get_column(x).width
return if w <= @col_min_width
column_width x, w-1 if w
@coffsets = nil
fire_dimension_changed
end
#def method_missing(name, *args)
#@tp.send(name, *args)
#end
#
# supply a custom renderer that implements +render()+
# @see render
def renderer r
@renderer = r
end
def header_adjustment
@_header_adjustment
end
##
# getter and setter for columns
# 2014-04-10 - 13:49
# @param [Array] columns to set as Array of Strings
# @return if no args, returns array of column names as Strings
# NOTE
# Appends columns to array, so it must be set before data, and thus it should
# clear the list
#
def columns(*val)
if val.empty?
# returns array of column names as Strings
@list[0]
else
array = val[0]
@_header_adjustment = 1
@list ||= []
@list.clear
@list << array
_init_model array
# update the names in column model
array.each_with_index { |n,i|
c = get_column(i)
c.name = name
}
self
end
end
##
# Set column titles with given array of strings.
# NOTE: This is only required to be called if first row of file or content does not contain
# titles. In that case, this should be called before setting the data as the array passed
# is appended into the content array.
# @deprecated complicated, just use `columns()`
def columns=(array)
columns(array)
self
end
alias :headings= :columns=
# size each column based on widths of this row of data.
# Only changed width if no width for that column
def _init_model array
# clear the column data -- this line should be called otherwise previous tables stuff will remain.
@chash.clear
array.each_with_index { |e,i|
# if columns added later we could be overwriting the width
c = get_column(i)
c.width ||= 10
}
# maintains index in current pointer and gives next or prev
@column_pointer = Circular.new array.size()-1
end
# size each column based on widths of this row of data.
def model_row index
array = @list[index]
array.each_with_index { |c,i|
# if columns added later we could be overwriting the width
ch = get_column(i)
ch.width = c.to_s.length + 2
}
# maintains index in current pointer and gives next or prev
@column_pointer = Circular.new array.size()-1
self
end
# estimate columns widths based on data in first 10 or so rows
# This will override any previous widths, so put custom widths
# after calling this.
def estimate_column_widths
each_column {|c,i|
c.width = suggest_column_width(i)
}
self
end
# calculates and returns a suggested columns width for given column
# based on data (first 10 rows)
# called by +estimate_column_widths+ in a loop
def suggest_column_width col
#ret = @cw[col] || 2
ret = get_column(col).width || 2
ctr = 0
@list.each_with_index { |r, i|
#next if i < @toprow # this is also a possibility, it checks visible rows
break if ctr > 10
ctr += 1
next if r == :separator
c = r[col]
x = c.to_s.length
ret = x if x > ret
}
ret
end
#------- data modification methods ------#
# I am assuming the column has been set using +columns=+
# Now only data is being sent in
# NOTE : calling set_content sends to TP's +text()+ which resets @list
# @param lines is an array or arrays
def text lines, fmt=:none
# maybe we can check this out
# should we not erase data, will user keep one column and resetting data ?
# set_content assumes data is gone.
@list ||= [] # this would work if no columns
@list.concat( lines)
fire_dimension_changed
self
end
##
# set column array and data array in one shot
# Erases any existing content
def resultset columns, data
@list = []
columns(columns)
text(data)
end
# Takes the name of a file containing delimited data
# and load it into the table.
# This method will load and split the file into the table.
# @param name is the file name
# @param config is a hash containing:
# - :separator - field separator, default is TAB
# - :columns - array of column names
# or true - first row is column names
# or false - no columns.
#
# == NOTE
# if columns is not mentioned, then it defaults to false
#
# == Example
#
# table = Table.new ...
# table.filename 'contacts.tsv', :separator => '|', :columns => true
#
def filename name, _config = {}
arr = File.open(name,"r").read.split("\n")
lines = []
sep = _config[:separator] || _config[:delimiter] || '\t'
arr.each { |l| lines << l.split(sep) }
cc = _config[:columns]
if cc.is_a? Array
columns(cc)
text(lines)
elsif cc
# cc is true, use first row as column names
columns(lines[0])
text(lines[1..-1])
else
# cc is false - no columns
# XXX since columns() is not called, so chash is not cleared.
_init_model lines[0]
text(lines)
end
end
alias :load :filename
# save the table as a file
# @param String name of output file. If nil, user is prompted
# Currently, tabs are used as delimiter, but this could be based on input
# separator, or prompted.
def save_as outfile
_t = "(all rows)"
if @selected_indices.size > 0
_t = "(selected rows)"
end
unless outfile
outfile = get_string "Enter file name to save #{_t} as "
return unless outfile
end
# if there is a selection, then write only selected rows
l = nil
if @selected_indices.size > 0
l = []
@list.each_with_index { |v,i| l << v if @selected_indices.include? i }
else
l = @list
end
File.open(outfile, 'w') {|f|
l.each {|r|
line = r.join "\t"
f.puts line
}
}
end
#
## add a row to the table
# The name add will be removed soon, pls use << instead.
def add array
unless @list
# columns were not added, this most likely is the title
@list ||= []
_init_model array
end
@list << array
fire_dimension_changed
self
end
alias :<< :add
# delete a data row at index
#
# NOTE : This does not adjust for header_adjustment. So zero will refer to the header if there is one.
# This is to keep consistent with textpad which does not know of header_adjustment and uses the actual
# index. Usually, programmers will be dealing with +@current_index+
#
def delete_at ix
return unless @list
raise ArgumentError, "Argument must be within 0 and #{@list.length}" if ix < 0 or ix >= @list.length
fire_dimension_changed
#@list.delete_at(ix + @_header_adjustment)
@list.delete_at(ix)
end
#
# clear the list completely
def clear
@selected_indices.clear
super
end
# get the value at the cell at row and col
# @return String
def get_value_at row,col
actrow = row + @_header_adjustment
@list[actrow, col]
end
# set value at the cell at row and col
# @param int row
# @param int col
# @param String value
# @return self
def set_value_at row,col,val
actrow = row + @_header_adjustment
@list[actrow , col] = val
fire_row_changed actrow
self
end
#------- column related methods ------#
#
# convenience method to set width of a column
# @param index of column
# @param width
# For setting other attributes, use get_column(index)
def column_width colindex, width
get_column(colindex).width = width
_invalidate_width_cache
end
# convenience method to set alignment of a column
# @param index of column
# @param align - :right (any other value is taken to be left)
def column_align colindex, align
get_column(colindex).align = align
end
# convenience method to hide or unhide a column
# Provided since column offsets need to be recalculated in the case of a width
# change or visibility change
def column_hidden colindex, hidden
get_column(colindex).hidden = hidden
_invalidate_width_cache
end
# http://www.opensource.apple.com/source/gcc/gcc-5483/libjava/javax/swing/table/DefaultTableColumnModel.java
def _invalidate_width_cache #:nodoc:
@coffsets = nil
end
##
# should all this move into table column model or somepn
# move a column from offset ix to offset newix
def move_column ix, newix
acol = @chash.delete_at ix
@chash.insert newix, acol
_invalidate_width_cache
#tmce = TableColumnModelEvent.new(ix, newix, self, :MOVE)
#fire_handler :TABLE_COLUMN_MODEL_EVENT, tmce
end
# TODO
def add_column tc
raise "to figure out add_column"
_invalidate_width_cache
end
# TODO
def remove_column tc
raise "to figure out add_column"
_invalidate_width_cache
end
def calculate_column_width col, maxrows=99
ret = 3
ctr = 0
@list.each_with_index { |r, i|
#next if i < @toprow # this is also a possibility, it checks visible rows
break if ctr > maxrows
ctr += 1
#next if r == :separator
c = r[col]
x = c.to_s.length
ret = x if x > ret
}
ret
end
##
# refresh pad onto window
# overrides super due to header_adjustment and the header too
def padrefresh
top = @window.top
left = @window.left
sr = @startrow + top
sc = @startcol + left
# first do header always in first row
retval = FFI::NCurses.prefresh(@pad,0,@pcol, sr , sc , 2 , @cols+ sc );
# now print rest of data
# h is header_adjustment
h = 1
retval = FFI::NCurses.prefresh(@pad,@prow + h,@pcol, sr + h , sc , @rows + sr , @cols+ sc );
$log.warn "XXX: PADREFRESH #{retval}, #{@prow}, #{@pcol}, #{sr}, #{sc}, #{@rows+sr}, #{@cols+sc}." if retval == -1
# padrefresh can fail if width is greater than NCurses.COLS
end
def create_default_sorter
raise "Data not sent in." unless @list
@table_row_sorter = DefaultTableRowSorter.new @list
end
# set a default renderer
#--
# we were not doing this automatically, so repaint was going to TP and failing on mvaddstr
# 2014-04-10 - 10:57
#++
def create_default_renderer
r = DefaultTableRenderer.new self
renderer(r)
end
# returns true if focus is on header_row
def header_row?
#@prow == 0
@prow == @current_index
end
# called when ENTER is pressed.
# Takes into account if user is on header_row
def fire_action_event
if header_row?
if @table_row_sorter
x = _convert_curpos_to_column
c = @chash[x]
# convert to index in data model since sorter only has data_model
index = c.index
@table_row_sorter.toggle_sort_order index
@table_row_sorter.sort
fire_dimension_changed
end
end
super
end
##
# Find the next row that contains given string
# Overrides textpad since each line is an array
# NOTE does not go to next match within row
# NOTE: FIXME ensure_visible puts prow = current_index so in this case, the header
# overwrites the matched row.
# @return row and col offset of match, or nil
# @param String to find
#@ deprecate since it does not get second match in line. textpad does
# however, the offset textpad shows is wrong
def OLDnext_match str
_calculate_column_offsets unless @coffsets
first = nil
## content can be string or Chunkline, so we had to write index for this.
@list.each_with_index do |fields, ix|
#col = line.index str
#fields.each_with_index do |f, jx|
#@chash.each_with_index do |c, jx|
#next if c.hidden
each_column do |c,jx|
f = fields[c.index]
# value can be numeric
col = f.to_s.index str
if col
col += @coffsets[jx]
first ||= [ ix, col ]
if ix > @current_index
return [ix, col]
end
end
end
end
return first
end
# yields each column to caller method
# if yield returns true, collects index of row into array and returns the array
# @returns array of indices which can be empty
# Value yielded can be fixnum or date etc
def matching_indices
raise "block required for matching_indices" unless block_given?
@indices = []
## content can be string or Chunkline, so we had to write index for this.
@list.each_with_index do |fields, ix|
flag = yield ix, fields
if flag
@indices << ix
end
end
#$log.debug "XXX: INDICES found #{@indices}"
if @indices.count > 0
fire_dimension_changed
init_vars
else
@indices = nil
end
#return @indices
end
def clear_matches
# clear previous match so all data can show again
if @indices && @indices.count > 0
fire_dimension_changed
init_vars
end
@indices = nil
end
##
# Ensure current row is visible, if not make it first row
# This overrides textpad due to header_adjustment, otherwise
# during next_match, the header overrides the found row.
# @param current_index (default if not given)
#
def ensure_visible row = @current_index
unless is_visible? row
@prow = @current_index - @_header_adjustment
end
end
#
# yields non-hidden columns (ColumnInfo) and the offset/index
# This is the order in which columns are to be printed
def each_column
@chash.each_with_index { |c, i|
next if c.hidden
yield c,i if block_given?
}
end
# calls the renderer for all rows of data giving them pad, lineno, and line data
def render_all
if @indices && @indices.count > 0
@indices.each_with_index do |ix, jx|
render @pad, jx, @list[ix]
end
else
@list.each_with_index { |line, ix|
#FFI::NCurses.mvwaddstr(@pad,ix, 0, @list[ix])
render @pad, ix, line
}
end
end
# print footer containing line and total, overriding textpad which prints column offset also
# This is called internally by +repaint()+ but can be overridden for more complex printing.
def print_foot
return unless @print_footer
ha = @_header_adjustment
# ha takes into account whether there are headers or not
footer = "#{@current_index+1-ha} of #{@list.length-ha} "
@graphic.printstring( @row + @height -1 , @col+2, footer, @color_pair || $datacolor, @footer_attrib)
@repaint_footer_required = false
end
end # class Table
end # module