canis 0.0.4
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +45 -0
- data/CHANGES +52 -0
- data/Gemfile +4 -0
- data/LICENSE.txt +22 -0
- data/README.md +24 -0
- data/Rakefile +2 -0
- data/canis.gemspec +25 -0
- data/examples/alpmenu.rb +46 -0
- data/examples/app.sample +19 -0
- data/examples/appemail.rb +191 -0
- data/examples/atree.rb +105 -0
- data/examples/bline.rb +181 -0
- data/examples/common/devel.rb +319 -0
- data/examples/common/file.rb +93 -0
- data/examples/data/README.markdown +9 -0
- data/examples/data/brew.txt +38 -0
- data/examples/data/color.2 +37 -0
- data/examples/data/gemlist.txt +59 -0
- data/examples/data/lotr.txt +12 -0
- data/examples/data/ports.txt +136 -0
- data/examples/data/table.txt +37 -0
- data/examples/data/tasks.csv +88 -0
- data/examples/data/tasks.txt +27 -0
- data/examples/data/todo.txt +16 -0
- data/examples/data/todocsv.csv +28 -0
- data/examples/data/unix1.txt +21 -0
- data/examples/data/unix2.txt +11 -0
- data/examples/dbdemo.rb +506 -0
- data/examples/dirtree.rb +177 -0
- data/examples/newtabbedwindow.rb +100 -0
- data/examples/newtesttabp.rb +92 -0
- data/examples/tabular.rb +212 -0
- data/examples/tasks.rb +179 -0
- data/examples/term2.rb +88 -0
- data/examples/testbuttons.rb +307 -0
- data/examples/testcombo.rb +102 -0
- data/examples/testdb.rb +182 -0
- data/examples/testfields.rb +208 -0
- data/examples/testflowlayout.rb +43 -0
- data/examples/testkeypress.rb +98 -0
- data/examples/testlistbox.rb +187 -0
- data/examples/testlistbox1.rb +199 -0
- data/examples/testmessagebox.rb +144 -0
- data/examples/testprogress.rb +116 -0
- data/examples/testree.rb +107 -0
- data/examples/testsplitlayout.rb +53 -0
- data/examples/testsplitlayout1.rb +49 -0
- data/examples/teststacklayout.rb +48 -0
- data/examples/testwsshortcuts.rb +68 -0
- data/examples/testwsshortcuts2.rb +129 -0
- data/lib/canis.rb +16 -0
- data/lib/canis/core/docs/index.txt +104 -0
- data/lib/canis/core/docs/list.txt +16 -0
- data/lib/canis/core/docs/style_help.yml +34 -0
- data/lib/canis/core/docs/tabbedpane.txt +15 -0
- data/lib/canis/core/docs/table.txt +31 -0
- data/lib/canis/core/docs/textpad.txt +48 -0
- data/lib/canis/core/docs/tree.txt +23 -0
- data/lib/canis/core/include/.DS_Store +0 -0
- data/lib/canis/core/include/action.rb +83 -0
- data/lib/canis/core/include/actionmanager.rb +49 -0
- data/lib/canis/core/include/appmethods.rb +179 -0
- data/lib/canis/core/include/bordertitle.rb +49 -0
- data/lib/canis/core/include/canisparser.rb +100 -0
- data/lib/canis/core/include/colorparser.rb +437 -0
- data/lib/canis/core/include/defaultfilerenderer.rb +64 -0
- data/lib/canis/core/include/io.rb +320 -0
- data/lib/canis/core/include/layouts/SplitLayout.rb +161 -0
- data/lib/canis/core/include/layouts/abstractlayout.rb +213 -0
- data/lib/canis/core/include/layouts/flowlayout.rb +104 -0
- data/lib/canis/core/include/layouts/stacklayout.rb +109 -0
- data/lib/canis/core/include/listbindings.rb +89 -0
- data/lib/canis/core/include/listeditable.rb +319 -0
- data/lib/canis/core/include/listoperations.rb +61 -0
- data/lib/canis/core/include/listselectionmodel.rb +388 -0
- data/lib/canis/core/include/multibuffer.rb +173 -0
- data/lib/canis/core/include/ractionevent.rb +73 -0
- data/lib/canis/core/include/rchangeevent.rb +27 -0
- data/lib/canis/core/include/rhistory.rb +95 -0
- data/lib/canis/core/include/rinputdataevent.rb +47 -0
- data/lib/canis/core/include/textdocument.rb +111 -0
- data/lib/canis/core/include/vieditable.rb +175 -0
- data/lib/canis/core/include/widgetmenu.rb +66 -0
- data/lib/canis/core/system/colormap.rb +165 -0
- data/lib/canis/core/system/keydefs.rb +32 -0
- data/lib/canis/core/system/ncurses.rb +237 -0
- data/lib/canis/core/system/panel.rb +129 -0
- data/lib/canis/core/system/window.rb +1081 -0
- data/lib/canis/core/util/ansiparser.rb +119 -0
- data/lib/canis/core/util/app.rb +696 -0
- data/lib/canis/core/util/basestack.rb +412 -0
- data/lib/canis/core/util/defaultcolorparser.rb +84 -0
- data/lib/canis/core/util/extras/README +5 -0
- data/lib/canis/core/util/extras/bottomline.rb +1815 -0
- data/lib/canis/core/util/extras/padreader.rb +192 -0
- data/lib/canis/core/util/focusmanager.rb +31 -0
- data/lib/canis/core/util/helpmanager.rb +160 -0
- data/lib/canis/core/util/oldwidgetshortcuts.rb +304 -0
- data/lib/canis/core/util/promptmenu.rb +235 -0
- data/lib/canis/core/util/rcommandwindow.rb +933 -0
- data/lib/canis/core/util/rdialogs.rb +520 -0
- data/lib/canis/core/util/textutils.rb +74 -0
- data/lib/canis/core/util/viewer.rb +238 -0
- data/lib/canis/core/util/widgetshortcuts.rb +508 -0
- data/lib/canis/core/widgets/applicationheader.rb +103 -0
- data/lib/canis/core/widgets/box.rb +58 -0
- data/lib/canis/core/widgets/divider.rb +310 -0
- data/lib/canis/core/widgets/extras/README.md +12 -0
- data/lib/canis/core/widgets/extras/rtextarea.rb +960 -0
- data/lib/canis/core/widgets/extras/stackflow.rb +474 -0
- data/lib/canis/core/widgets/keylabelprinter.rb +194 -0
- data/lib/canis/core/widgets/listbox.rb +326 -0
- data/lib/canis/core/widgets/listfooter.rb +86 -0
- data/lib/canis/core/widgets/rcombo.rb +210 -0
- data/lib/canis/core/widgets/rcontainer.rb +415 -0
- data/lib/canis/core/widgets/rlink.rb +30 -0
- data/lib/canis/core/widgets/rmenu.rb +970 -0
- data/lib/canis/core/widgets/rmenulink.rb +30 -0
- data/lib/canis/core/widgets/rmessagebox.rb +400 -0
- data/lib/canis/core/widgets/rprogress.rb +118 -0
- data/lib/canis/core/widgets/rtabbedpane.rb +631 -0
- data/lib/canis/core/widgets/rtabbedwindow.rb +70 -0
- data/lib/canis/core/widgets/rwidget.rb +3634 -0
- data/lib/canis/core/widgets/scrollbar.rb +147 -0
- data/lib/canis/core/widgets/statusline.rb +113 -0
- data/lib/canis/core/widgets/table.rb +1072 -0
- data/lib/canis/core/widgets/tabular.rb +264 -0
- data/lib/canis/core/widgets/textpad.rb +1674 -0
- data/lib/canis/core/widgets/tree.rb +690 -0
- data/lib/canis/core/widgets/tree/treecellrenderer.rb +150 -0
- data/lib/canis/core/widgets/tree/treemodel.rb +432 -0
- data/lib/canis/version.rb +3 -0
- metadata +229 -0
@@ -0,0 +1,147 @@
|
|
1
|
+
require 'canis/core/util/app'
|
2
|
+
#include Ncurses # FFI 2011-09-8
|
3
|
+
include Canis
|
4
|
+
|
5
|
+
# This paints a vertical white bar given row and col, and length. It also calculates and prints
|
6
|
+
# a small bar over this based on relaetd objects list.length and current_index.
|
7
|
+
# Typically, after setup one would keep updating only current_index from the repaint method
|
8
|
+
# of caller or in the traversal event. This would look best if the listbox also has a reverse video border, or none.
|
9
|
+
# @example
|
10
|
+
# lb = list_box ....
|
11
|
+
# sb = Scrollbar.new @form, :row => lb.row, :col => lb.col, :length => lb.height, :list_length => lb.row_count, :current_index => 0
|
12
|
+
# .... later as user traverses
|
13
|
+
# sb.current_index = lb.current_index
|
14
|
+
# sb = Scrollbar.new @form, :parent => list
|
15
|
+
#
|
16
|
+
# At a later stage, we will integrate this with lists and tables, so it will happen automatically.
|
17
|
+
#
|
18
|
+
# @since 1.2.0 UNTESTED
|
19
|
+
module Canis
|
20
|
+
class Scrollbar < Widget
|
21
|
+
# row to start, same as listbox, required.
|
22
|
+
dsl_property :row
|
23
|
+
# column to start, same as listbox, required.
|
24
|
+
dsl_property :col
|
25
|
+
# how many rows is this (should be same as listboxes height, required.
|
26
|
+
dsl_property :length
|
27
|
+
# vertical or horizontal currently only VERTICAL
|
28
|
+
dsl_property :orientation
|
29
|
+
# initialize based on parent's values
|
30
|
+
dsl_property :parent
|
31
|
+
# which row is focussed, current_index of listbox, required.
|
32
|
+
dsl_property :current_index
|
33
|
+
# how many total rows of data does the list have, same as @list.length, required.
|
34
|
+
dsl_property :list_length
|
35
|
+
|
36
|
+
# TODO: if parent passed, we shold bind to ON_ENTER and get current_index, so no extra work is required.
|
37
|
+
|
38
|
+
def initialize form, config={}, &block
|
39
|
+
|
40
|
+
# setting default first or else Widget will place its BW default
|
41
|
+
#@color, @bgcolor = ColorMap.get_colors_for_pair $bottomcolor
|
42
|
+
super
|
43
|
+
@color_pair = get_color $datacolor, @color, @bgcolor
|
44
|
+
@scroll_pair = get_color $bottomcolor, :green, :white
|
45
|
+
#$log.debug "SCROLLBAR COLOR cp #{@color_pair} sp #{@scroll_pair} " if $log.debug?
|
46
|
+
@window = form.window
|
47
|
+
@editable = false
|
48
|
+
@focusable = false
|
49
|
+
@repaint_required = true
|
50
|
+
@orientation = :V
|
51
|
+
if @parent
|
52
|
+
@parent.bind :ENTER_ROW do |p|
|
53
|
+
# textview sent self, textpad sends textactionevent
|
54
|
+
if p.instance_of? TextActionEvent
|
55
|
+
p = p.source
|
56
|
+
end
|
57
|
+
# parent must implement row_count, and have a @current_index
|
58
|
+
raise StandardError, "Parent (#{p.class.to_s}) must implement row_count" unless p.respond_to? :row_count
|
59
|
+
self.current_index = p.current_index
|
60
|
+
@repaint_required = true #requred otherwise at end when same value sent, prop handler
|
61
|
+
# will not be fired (due to optimization).
|
62
|
+
end
|
63
|
+
# in some cases, on leaving a listbox or other component redraws itself to reduce
|
64
|
+
# selected or highlighted object, so the scrollbar gets overwritten. We need to repaint it.
|
65
|
+
@parent.bind :LEAVE do |p|
|
66
|
+
@repaint_required = true
|
67
|
+
end
|
68
|
+
end
|
69
|
+
end
|
70
|
+
|
71
|
+
##
|
72
|
+
# repaint the scrollbar
|
73
|
+
# Taking the data from parent as late as possible in case parent resized, or
|
74
|
+
# moved around by a container.
|
75
|
+
def repaint
|
76
|
+
if @parent
|
77
|
+
@row = @parent.row+1
|
78
|
+
@col = @parent.col + @parent.width - 1
|
79
|
+
@length = @parent.height - 2
|
80
|
+
@list_length = @parent.row_count
|
81
|
+
@current_index ||= @parent.current_index
|
82
|
+
@border_attrib ||= @parent.border_attrib
|
83
|
+
end
|
84
|
+
raise ArgumentError, "current_index must be provided" unless @current_index
|
85
|
+
raise ArgumentError, "list_length must be provided" unless @list_length
|
86
|
+
my_win = @form ? @form.window : @target_window
|
87
|
+
@graphic = my_win unless @graphic
|
88
|
+
return unless @repaint_required
|
89
|
+
|
90
|
+
# first print a right side vertical line
|
91
|
+
#bc = $bottomcolor # dark blue
|
92
|
+
bc = $datacolor
|
93
|
+
bordercolor = @border_color || bc
|
94
|
+
borderatt = @border_attrib || Ncurses::A_REVERSE
|
95
|
+
#$log.debug "SCROLL bordercolor #{bordercolor} , #{borderatt} " if $log.debug?
|
96
|
+
|
97
|
+
|
98
|
+
@graphic.attron(Ncurses.COLOR_PAIR(bordercolor) | borderatt)
|
99
|
+
#$log.debug " XXX SCROLL #{@row} #{@col} #{@length} "
|
100
|
+
@graphic.mvvline(@row+0, @col, 1, @length-0)
|
101
|
+
@graphic.attroff(Ncurses.COLOR_PAIR(bordercolor) | borderatt)
|
102
|
+
|
103
|
+
# now calculate and paint the scrollbar
|
104
|
+
pht = @length
|
105
|
+
listlen = @list_length * 1.0
|
106
|
+
@current_index = 0 if @current_index < 0
|
107
|
+
@current_index = listlen-1 if @current_index >= listlen
|
108
|
+
sclen = (pht/listlen)* @length
|
109
|
+
sclen = 1 if sclen < 1 # sometimes 0.7 for large lists 100 items 2011-10-1
|
110
|
+
scloc = (@current_index/listlen)* @length
|
111
|
+
scloc = (@length - sclen) if scloc > @length - sclen # don't exceed end
|
112
|
+
if @current_index == @list_length - 1
|
113
|
+
scloc = @length - sclen + 0 # earlier 1, but removed since sclen min 1 2011-10-1
|
114
|
+
end
|
115
|
+
@graphic.attron(Ncurses.COLOR_PAIR(@scroll_pair) | borderatt)
|
116
|
+
r = @row + scloc
|
117
|
+
c = @col + 0
|
118
|
+
#$log.debug " XXX SCROLLBAR #{r} #{c} #{sclen} "
|
119
|
+
@graphic.mvvline(r, c, 1, sclen)
|
120
|
+
@graphic.attroff(Ncurses.COLOR_PAIR(@scroll_pair) | borderatt)
|
121
|
+
@repaint_required = false
|
122
|
+
end
|
123
|
+
##
|
124
|
+
##
|
125
|
+
# ADD HERE
|
126
|
+
end
|
127
|
+
end
|
128
|
+
if __FILE__ == $PROGRAM_NAME
|
129
|
+
App.new do
|
130
|
+
r = 5
|
131
|
+
len = 20
|
132
|
+
list = []
|
133
|
+
0.upto(100) { |v| list << "#{v} scrollable data" }
|
134
|
+
lb = list_box "A list", :list => list
|
135
|
+
#sb = Scrollbar.new @form, :row => r, :col => 20, :length => len, :list_length => 50, :current_index => 0
|
136
|
+
rb = Scrollbar.new @form, :parent => lb
|
137
|
+
#hline :width => 20, :row => len+r
|
138
|
+
#keypress do |ch|
|
139
|
+
#case ch
|
140
|
+
#when :down
|
141
|
+
#sb.current_index += 1
|
142
|
+
#when :up
|
143
|
+
#sb.current_index -= 1
|
144
|
+
#end
|
145
|
+
#end
|
146
|
+
end
|
147
|
+
end
|
@@ -0,0 +1,113 @@
|
|
1
|
+
require 'canis'
|
2
|
+
|
3
|
+
module Canis
|
4
|
+
|
5
|
+
#
|
6
|
+
# A vim-like application status bar that can display time and various other statuses
|
7
|
+
# at the bottom, typically above the dock (3rd line from last).
|
8
|
+
#
|
9
|
+
class StatusLine < Widget
|
10
|
+
#attr_accessor :row_relative # lets only advertise this when we've tested it out
|
11
|
+
|
12
|
+
def initialize form, config={}, &block
|
13
|
+
@row_relative = -3
|
14
|
+
if form.window.height == 0
|
15
|
+
@row = Ncurses.LINES-3 # fix, what about smaller windows, use window dimensions and watch out for 0,0
|
16
|
+
else
|
17
|
+
@row = form.window.height-3 # fix, what about smaller windows, use window dimensions and watch out for 0,0
|
18
|
+
end
|
19
|
+
# in root windows FIXME
|
20
|
+
@col = 0
|
21
|
+
@name = "sl"
|
22
|
+
super
|
23
|
+
# if negativ row passed we store as relative to bottom, so we can maintain that.
|
24
|
+
if @row < 0
|
25
|
+
@row_relative = @row
|
26
|
+
@row = Ncurses.LINES - @row
|
27
|
+
else
|
28
|
+
@row_relative = (Ncurses.LINES - @row) * -1
|
29
|
+
end
|
30
|
+
@focusable = false
|
31
|
+
@editable = false
|
32
|
+
@command = nil
|
33
|
+
@repaint_required = true
|
34
|
+
bind(:PROPERTY_CHANGE) { |e| @color_pair = nil ; }
|
35
|
+
end
|
36
|
+
#
|
37
|
+
# command that returns a string that populates the status line (left aligned)
|
38
|
+
# @see :right
|
39
|
+
# See dbdemo.rb
|
40
|
+
# e.g.
|
41
|
+
# @l.command { "%-20s [DB: %-s | %-s ]" % [ Time.now, $current_db || "None", $current_table || "----"] }
|
42
|
+
#
|
43
|
+
def command *args, &blk
|
44
|
+
@command = blk
|
45
|
+
@args = args
|
46
|
+
end
|
47
|
+
alias :left :command
|
48
|
+
|
49
|
+
#
|
50
|
+
# Procudure for text to be right aligned in statusline
|
51
|
+
def right *args, &blk
|
52
|
+
@right_text = blk
|
53
|
+
@right_args = args
|
54
|
+
end
|
55
|
+
|
56
|
+
# NOTE: I have not put a check of repaint_required, so this will print on each key-stroke OR
|
57
|
+
# rather whenever form.repaint is called.
|
58
|
+
def repaint
|
59
|
+
@color_pair ||= get_color($datacolor, @color, @bgcolor)
|
60
|
+
len = @form.window.getmaxx # width does not change upon resizing so useless, fix or do something
|
61
|
+
len = Ncurses.COLS if len == 0 || len > Ncurses.COLS
|
62
|
+
# this should only happen if there's a change in window
|
63
|
+
if @row_relative
|
64
|
+
@row = Ncurses.LINES+@row_relative
|
65
|
+
end
|
66
|
+
|
67
|
+
# first print dashes through
|
68
|
+
@form.window.printstring @row, @col, "%s" % "-" * len, @color_pair, Ncurses::A_REVERSE
|
69
|
+
|
70
|
+
# now call the block to get current values
|
71
|
+
if @command
|
72
|
+
ftext = @command.call(self, @args)
|
73
|
+
else
|
74
|
+
status = $status_message ? $status_message.value : ""
|
75
|
+
#ftext = " %-20s | %s" % [Time.now, status] # should we print a default value just in case user doesn't
|
76
|
+
ftext = status # should we print a default value just in case user doesn't
|
77
|
+
end
|
78
|
+
# 2013-03-25 - 11:52 replaced $datacolor with @color_pair - how could this have been ?
|
79
|
+
# what if user wants to change attrib ?
|
80
|
+
if ftext =~ /#\[/
|
81
|
+
# hopefully color_pair does not clash with formatting
|
82
|
+
@form.window.printstring_formatted @row, @col, ftext, @color_pair, Ncurses::A_REVERSE
|
83
|
+
else
|
84
|
+
@form.window.printstring @row, @col, ftext, @color_pair, Ncurses::A_REVERSE
|
85
|
+
end
|
86
|
+
|
87
|
+
if @right_text
|
88
|
+
ftext = @right_text.call(self, @right_args)
|
89
|
+
if ftext =~ /#\[/
|
90
|
+
# hopefully color_pair does not clash with formatting
|
91
|
+
@form.window.printstring_formatted_right @row, nil, ftext, @color_pair, Ncurses::A_REVERSE
|
92
|
+
else
|
93
|
+
c = len - ftext.length
|
94
|
+
@form.window.printstring @row, c, ftext, @color_pair, Ncurses::A_REVERSE
|
95
|
+
end
|
96
|
+
else
|
97
|
+
t = Time.now
|
98
|
+
tt = t.strftime "%F %H:%M:%S"
|
99
|
+
#r = Ncurses.LINES
|
100
|
+
# somehow the bg defined here affects the bg in left text, if left does not define
|
101
|
+
# a bg. The bgcolor defined of statusline is ignored in left or overriden by this
|
102
|
+
#ftext = "#[fg=white,bg=blue] %-20s#[/end]" % [tt] # print a default
|
103
|
+
@form.window.printstring_formatted_right @row, nil, tt, @color_pair, Ncurses::A_REVERSE
|
104
|
+
end
|
105
|
+
|
106
|
+
@repaint_required = false
|
107
|
+
end
|
108
|
+
def handle_keys ch
|
109
|
+
return :UNHANDLED
|
110
|
+
end
|
111
|
+
|
112
|
+
end # class
|
113
|
+
end # module
|
@@ -0,0 +1,1072 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
# header {{{
|
3
|
+
# vim: set foldlevel=0 foldmethod=marker :
|
4
|
+
# ----------------------------------------------------------------------------- #
|
5
|
+
# File: table.rb
|
6
|
+
# Description: A tabular widget based on textpad
|
7
|
+
# Author: jkepler http://github.com/mare-imbrium/canis/
|
8
|
+
# Date: 2013-03-29 - 20:07
|
9
|
+
# License: Same as Ruby's License (http://www.ruby-lang.org/LICENSE.txt)
|
10
|
+
# Last update: 2014-06-01 13:10
|
11
|
+
# ----------------------------------------------------------------------------- #
|
12
|
+
# table.rb Copyright (C) 2012-2014 kepler
|
13
|
+
|
14
|
+
# == CHANGES:
|
15
|
+
# - changed @content to @list since all multirow wids and utils expect @list
|
16
|
+
# - changed name from tablewidget to table
|
17
|
+
#
|
18
|
+
# == TODO
|
19
|
+
# [ ] if no columns, then init_model is called so chash is not cleared.
|
20
|
+
# _ compare to tabular_widget and see what's missing
|
21
|
+
# _ filtering rows without losing data
|
22
|
+
# . selection stuff
|
23
|
+
# x test with resultset from sqlite to see if we can use Array or need to make model
|
24
|
+
# should we use a datamodel so resultsets can be sent in, what about tabular
|
25
|
+
# _ header to handle events ?
|
26
|
+
# header }}}
|
27
|
+
|
28
|
+
require 'logger'
|
29
|
+
require 'canis'
|
30
|
+
require 'canis/core/widgets/textpad'
|
31
|
+
|
32
|
+
##
|
33
|
+
# The motivation to create yet another table widget is because tabular_widget
|
34
|
+
# is based on textview etc which have a lot of complex processing and rendering
|
35
|
+
# whereas textpad is quite simple. It is easy to just add one's own renderer
|
36
|
+
# making the code base simpler to understand and maintain.
|
37
|
+
#
|
38
|
+
#
|
39
|
+
module Canis
|
40
|
+
# structures {{{
|
41
|
+
# column data, one instance for each column
|
42
|
+
# index is the index in the data of this column. This index will not change.
|
43
|
+
# Order of printing columns is determined by the ordering of the objects.
|
44
|
+
class ColumnInfo < Struct.new(:name, :index, :offset, :width, :align, :hidden, :attrib, :color, :bgcolor)
|
45
|
+
end
|
46
|
+
# a structure that maintains position and gives
|
47
|
+
# next and previous taking max index into account.
|
48
|
+
# it also circles. Can be used for traversing next component
|
49
|
+
# in a form, or container, or columns in a table.
|
50
|
+
class Circular < Struct.new(:max_index, :current_index)
|
51
|
+
attr_reader :last_index
|
52
|
+
attr_reader :current_index
|
53
|
+
def initialize m, c=0
|
54
|
+
raise "max index cannot be nil" unless m
|
55
|
+
@max_index = m
|
56
|
+
@current_index = c
|
57
|
+
@last_index = c
|
58
|
+
end
|
59
|
+
def next
|
60
|
+
@last_index = @current_index
|
61
|
+
if @current_index + 1 > @max_index
|
62
|
+
@current_index = 0
|
63
|
+
else
|
64
|
+
@current_index += 1
|
65
|
+
end
|
66
|
+
end
|
67
|
+
def previous
|
68
|
+
@last_index = @current_index
|
69
|
+
if @current_index - 1 < 0
|
70
|
+
@current_index = @max_index
|
71
|
+
else
|
72
|
+
@current_index -= 1
|
73
|
+
end
|
74
|
+
end
|
75
|
+
def is_last?
|
76
|
+
@current_index == @max_index
|
77
|
+
end
|
78
|
+
end
|
79
|
+
# structures }}}
|
80
|
+
# sorter {{{
|
81
|
+
# This is our default table row sorter.
|
82
|
+
# It does a multiple sort and allows for reverse sort also.
|
83
|
+
# It's a pretty simple sorter and uses sort, not sort_by.
|
84
|
+
# Improvements welcome.
|
85
|
+
# Usage: provide model in constructor or using model method
|
86
|
+
# Call toggle_sort_order(column_index)
|
87
|
+
# Call sort.
|
88
|
+
# Currently, this sorts the provided model in-place. Future versions
|
89
|
+
# may maintain a copy, or use a table that provides a mapping of model to result.
|
90
|
+
# # TODO check if column_sortable
|
91
|
+
class DefaultTableRowSorter
|
92
|
+
attr_reader :sort_keys
|
93
|
+
# model is array of data
|
94
|
+
def initialize data_model=nil
|
95
|
+
self.model = data_model
|
96
|
+
@columns_sort = []
|
97
|
+
@sort_keys = nil
|
98
|
+
end
|
99
|
+
def model=(model)
|
100
|
+
@model = model
|
101
|
+
@sort_keys = nil
|
102
|
+
end
|
103
|
+
def sortable colindex, tf
|
104
|
+
@columns_sort[colindex] = tf
|
105
|
+
end
|
106
|
+
def sortable? colindex
|
107
|
+
return false if @columns_sort[colindex]==false
|
108
|
+
return true
|
109
|
+
end
|
110
|
+
# should to_s be used for this column
|
111
|
+
def use_to_s colindex
|
112
|
+
return true # TODO
|
113
|
+
end
|
114
|
+
# sorts the model based on sort keys and reverse flags
|
115
|
+
# @sort_keys contains indices to sort on
|
116
|
+
# @reverse_flags is an array of booleans, true for reverse, nil or false for ascending
|
117
|
+
def sort
|
118
|
+
return unless @model
|
119
|
+
return if @sort_keys.empty?
|
120
|
+
$log.debug "TABULAR SORT KEYS #{sort_keys} "
|
121
|
+
# first row is the header which should remain in place
|
122
|
+
# We could have kept column headers separate, but then too much of mucking around
|
123
|
+
# with textpad, this way we avoid touching it
|
124
|
+
header = @model.delete_at 0
|
125
|
+
begin
|
126
|
+
# next line often can give error "array within array" - i think on date fields that
|
127
|
+
# contain nils
|
128
|
+
@model.sort!{|x,y|
|
129
|
+
res = 0
|
130
|
+
@sort_keys.each { |ee|
|
131
|
+
e = ee.abs-1 # since we had offsetted by 1 earlier
|
132
|
+
abse = e.abs
|
133
|
+
if ee < 0
|
134
|
+
xx = x[abse]
|
135
|
+
yy = y[abse]
|
136
|
+
# the following checks are since nil values cause an error to be raised
|
137
|
+
if xx.nil? && yy.nil?
|
138
|
+
res = 0
|
139
|
+
elsif xx.nil?
|
140
|
+
res = 1
|
141
|
+
elsif yy.nil?
|
142
|
+
res = -1
|
143
|
+
else
|
144
|
+
res = y[abse] <=> x[abse]
|
145
|
+
end
|
146
|
+
else
|
147
|
+
xx = x[e]
|
148
|
+
yy = y[e]
|
149
|
+
# the following checks are since nil values cause an error to be raised
|
150
|
+
# whereas we want a nil to be wither treated as a zero or a blank
|
151
|
+
if xx.nil? && yy.nil?
|
152
|
+
res = 0
|
153
|
+
elsif xx.nil?
|
154
|
+
res = -1
|
155
|
+
elsif yy.nil?
|
156
|
+
res = 1
|
157
|
+
else
|
158
|
+
res = x[e] <=> y[e]
|
159
|
+
end
|
160
|
+
end
|
161
|
+
break if res != 0
|
162
|
+
}
|
163
|
+
res
|
164
|
+
}
|
165
|
+
ensure
|
166
|
+
@model.insert 0, header if header
|
167
|
+
end
|
168
|
+
end
|
169
|
+
# toggle the sort order if given column offset is primary sort key
|
170
|
+
# Otherwise, insert as primary sort key, ascending.
|
171
|
+
def toggle_sort_order index
|
172
|
+
index += 1 # increase by 1, since 0 won't multiple by -1
|
173
|
+
# internally, reverse sort is maintained by multiplying number by -1
|
174
|
+
@sort_keys ||= []
|
175
|
+
if @sort_keys.first && index == @sort_keys.first.abs
|
176
|
+
@sort_keys[0] *= -1
|
177
|
+
else
|
178
|
+
@sort_keys.delete index # in case its already there
|
179
|
+
@sort_keys.delete(index*-1) # in case its already there
|
180
|
+
@sort_keys.unshift index
|
181
|
+
# don't let it go on increasing
|
182
|
+
if @sort_keys.size > 3
|
183
|
+
@sort_keys.pop
|
184
|
+
end
|
185
|
+
end
|
186
|
+
end
|
187
|
+
def set_sort_keys list
|
188
|
+
@sort_keys = list
|
189
|
+
end
|
190
|
+
end #class
|
191
|
+
|
192
|
+
# sorter }}}
|
193
|
+
# renderer {{{
|
194
|
+
#
|
195
|
+
# TODO see how jtable does the renderers and columns stuff.
|
196
|
+
#
|
197
|
+
# perhaps we can combine the two but have different methods or some flag
|
198
|
+
# that way oter methods can be shared
|
199
|
+
class DefaultTableRenderer
|
200
|
+
|
201
|
+
# source is the textpad or extending widget needed so we can call show_colored_chunks
|
202
|
+
# if the user specifies column wise colors
|
203
|
+
def initialize source
|
204
|
+
@source = source
|
205
|
+
@y = '|'
|
206
|
+
@x = '+'
|
207
|
+
@coffsets = []
|
208
|
+
@header_color = :white
|
209
|
+
@header_bgcolor = :red
|
210
|
+
@header_attrib = NORMAL
|
211
|
+
@color = :white
|
212
|
+
@bgcolor = :black
|
213
|
+
@color_pair = $datacolor
|
214
|
+
@attrib = NORMAL
|
215
|
+
@_check_coloring = nil
|
216
|
+
# adding setting column_model auto on 2014-04-10 - 10:53 why wasn;t this here already
|
217
|
+
column_model(source.column_model)
|
218
|
+
end
|
219
|
+
def header_colors fg, bg
|
220
|
+
@header_color = fg
|
221
|
+
@header_bgcolor = bg
|
222
|
+
end
|
223
|
+
def header_attrib att
|
224
|
+
@header_attrib = att
|
225
|
+
end
|
226
|
+
# set fg and bg color of content rows, default is $datacolor (white on black).
|
227
|
+
def content_colors fg, bg
|
228
|
+
@color = fg
|
229
|
+
@bgcolor = bg
|
230
|
+
@color_pair = get_color($datacolor, fg, bg)
|
231
|
+
end
|
232
|
+
def content_attrib att
|
233
|
+
@attrib = att
|
234
|
+
end
|
235
|
+
# set column model (Table Renderer)
|
236
|
+
def column_model c
|
237
|
+
@chash = c
|
238
|
+
end
|
239
|
+
##
|
240
|
+
# Takes the array of row data and formats it using column widths
|
241
|
+
# and returns an array which is used for printing
|
242
|
+
#
|
243
|
+
# return an array so caller can color columns if need be
|
244
|
+
def convert_value_to_text r
|
245
|
+
str = []
|
246
|
+
fmt = nil
|
247
|
+
field = nil
|
248
|
+
# we need to loop through chash and get index from it and get that row from r
|
249
|
+
each_column {|c,i|
|
250
|
+
e = r[c.index]
|
251
|
+
w = c.width
|
252
|
+
l = e.to_s.length
|
253
|
+
# if value is longer than width, then truncate it
|
254
|
+
if l > w
|
255
|
+
fmt = "%.#{w}s "
|
256
|
+
else
|
257
|
+
case c.align
|
258
|
+
when :right
|
259
|
+
fmt = "%#{w}s "
|
260
|
+
else
|
261
|
+
fmt = "%-#{w}s "
|
262
|
+
end
|
263
|
+
end
|
264
|
+
field = fmt % e
|
265
|
+
# if we really want to print a single column with color, we need to print here itself
|
266
|
+
# each cell. If we want the user to use tmux formatting in the column itself ...
|
267
|
+
# FIXME - this must not be done for headers.
|
268
|
+
#if c.color
|
269
|
+
#field = "#[fg=#{c.color}]#{field}#[/end]"
|
270
|
+
#end
|
271
|
+
str << field
|
272
|
+
}
|
273
|
+
return str
|
274
|
+
end
|
275
|
+
# return a string representation of the row so that +index+ can be applied to it.
|
276
|
+
# This must take into account columns widths and offsets. This is used by textpad's
|
277
|
+
# next_match method
|
278
|
+
def to_searchable arr
|
279
|
+
convert_value_to_text(arr).join
|
280
|
+
end
|
281
|
+
#
|
282
|
+
# @param pad for calling print methods on
|
283
|
+
# @param lineno the line number on the pad to print on
|
284
|
+
# @param [String] data to print which will be an array (@list[index])
|
285
|
+
def render pad, lineno, str
|
286
|
+
#lineno += 1 # header_adjustment
|
287
|
+
# header_adjustment means columns have been set
|
288
|
+
return render_header pad, lineno, 0, str if lineno == 0 && @source.header_adjustment > 0
|
289
|
+
#text = str.join " | "
|
290
|
+
#text = @fmstr % str
|
291
|
+
text = convert_value_to_text str
|
292
|
+
if @_check_coloring
|
293
|
+
#$log.debug "XXX: INSIDE COLORIIN"
|
294
|
+
text = colorize pad, lineno, text
|
295
|
+
return
|
296
|
+
end
|
297
|
+
# check if any specific colors , if so then print colors in a loop with no dependence on colored chunks
|
298
|
+
# then we don't need source pointer
|
299
|
+
render_data pad, lineno, text
|
300
|
+
|
301
|
+
end
|
302
|
+
# passes padded data for final printing or data row
|
303
|
+
# this allows user to do row related coloring without having to tamper
|
304
|
+
# with the headers or other internal workings. This will not be called
|
305
|
+
# if column specific colorign is in effect.
|
306
|
+
# @param text is an array of strings, in the order of actual printing with hidden cols removed
|
307
|
+
def render_data pad, lineno, text
|
308
|
+
text = text.join
|
309
|
+
# FIXME why repeatedly getting this colorpair
|
310
|
+
cp = @color_pair
|
311
|
+
att = @attrib
|
312
|
+
# added for selection, but will crash if selection is not extended !!! XXX
|
313
|
+
if @source.is_row_selected? lineno
|
314
|
+
att = REVERSE
|
315
|
+
# FIXME currentl this overflows into next row
|
316
|
+
end
|
317
|
+
|
318
|
+
FFI::NCurses.wattron(pad,FFI::NCurses.COLOR_PAIR(cp) | att)
|
319
|
+
FFI::NCurses.mvwaddstr(pad, lineno, 0, text)
|
320
|
+
FFI::NCurses.wattroff(pad,FFI::NCurses.COLOR_PAIR(cp) | att)
|
321
|
+
end
|
322
|
+
|
323
|
+
def render_header pad, lineno, col, columns
|
324
|
+
# I could do it once only but if user sets colors midway we can check once whenvever
|
325
|
+
# repainting
|
326
|
+
check_colors #if @_check_coloring.nil?
|
327
|
+
#text = columns.join " | "
|
328
|
+
#text = @fmstr % columns
|
329
|
+
text = convert_value_to_text columns
|
330
|
+
text = text.join
|
331
|
+
bg = @header_bgcolor
|
332
|
+
fg = @header_color
|
333
|
+
att = @header_attrib
|
334
|
+
#cp = $datacolor
|
335
|
+
cp = get_color($datacolor, fg, bg)
|
336
|
+
FFI::NCurses.wattron(pad,FFI::NCurses.COLOR_PAIR(cp) | att)
|
337
|
+
FFI::NCurses.mvwaddstr(pad, lineno, col, text)
|
338
|
+
FFI::NCurses.wattroff(pad,FFI::NCurses.COLOR_PAIR(cp) | att)
|
339
|
+
end
|
340
|
+
# check if we need to individually color columns or we can do the entire
|
341
|
+
# row in one shot
|
342
|
+
def check_colors
|
343
|
+
each_column {|c,i|
|
344
|
+
if c.color || c.bgcolor || c.attrib
|
345
|
+
@_check_coloring = true
|
346
|
+
return
|
347
|
+
end
|
348
|
+
@_check_coloring = false
|
349
|
+
}
|
350
|
+
end
|
351
|
+
def each_column
|
352
|
+
@chash.each_with_index { |c, i|
|
353
|
+
next if c.hidden
|
354
|
+
yield c,i if block_given?
|
355
|
+
}
|
356
|
+
end
|
357
|
+
def colorize pad, lineno, r
|
358
|
+
# the incoming data is already in the order of display based on chash,
|
359
|
+
# so we cannot run chash on it again, so how do we get the color info
|
360
|
+
_offset = 0
|
361
|
+
each_column {|c,i|
|
362
|
+
text = r[i]
|
363
|
+
color = c.color
|
364
|
+
bg = c.bgcolor
|
365
|
+
if color || bg
|
366
|
+
cp = get_color(@color_pair, color || @color, bg || @bgcolor)
|
367
|
+
else
|
368
|
+
cp = @color_pair
|
369
|
+
end
|
370
|
+
att = c.attrib || @attrib
|
371
|
+
FFI::NCurses.wattron(pad,FFI::NCurses.COLOR_PAIR(cp) | att)
|
372
|
+
FFI::NCurses.mvwaddstr(pad, lineno, _offset, text)
|
373
|
+
FFI::NCurses.wattroff(pad,FFI::NCurses.COLOR_PAIR(cp) | att)
|
374
|
+
_offset += text.length
|
375
|
+
}
|
376
|
+
end
|
377
|
+
end
|
378
|
+
# renderer }}}
|
379
|
+
|
380
|
+
#--
|
381
|
+
# If we make a pad of the whole thing then the columns will also go out when scrolling
|
382
|
+
# So then there's no point storing columns separately. Might as well keep in content
|
383
|
+
# so scrolling works fine, otherwise textpad will have issues scrolling.
|
384
|
+
# Making a pad of the content but not column header complicates stuff,
|
385
|
+
# do we make a pad of that, or print it like the old thing.
|
386
|
+
#++
|
387
|
+
# A table widget containing rows and columns and the ability to resize and hide or align
|
388
|
+
# columns. Also may have first row as column names.
|
389
|
+
#
|
390
|
+
# == NOTE
|
391
|
+
# The most important methods to use probably are `text()` or `resultset` or `filename` to load
|
392
|
+
# data. With `text` you will want to first specify column names with `columns()`.
|
393
|
+
#
|
394
|
+
# +@current_index+ inherited from +Textpad+ continues to be the index of the list that has user's
|
395
|
+
# focus, and should be used for row operations.
|
396
|
+
#
|
397
|
+
# In order to use Textpad easily, the first row of the table model is the column names. Data is maintained
|
398
|
+
# in an Array. Several operations are delegated to Array, or have the same name. You can get the list
|
399
|
+
# using `list()` to run other Array operations on it.
|
400
|
+
#
|
401
|
+
# If you modify the Array directly, you may have to use `fire_row_changed(index)` to reflect the update to
|
402
|
+
# a single row. If you delete or add a row, you will have to use `fire_dimension_changed()`. However,
|
403
|
+
# internal functions do this automatically.
|
404
|
+
#
|
405
|
+
require 'canis/core/include/listselectionmodel'
|
406
|
+
class Table < TextPad
|
407
|
+
|
408
|
+
dsl_accessor :print_footer
|
409
|
+
#attr_reader :columns
|
410
|
+
attr_accessor :table_row_sorter
|
411
|
+
|
412
|
+
def initialize form = nil, config={}, &block
|
413
|
+
|
414
|
+
# array of column info objects
|
415
|
+
@chash = []
|
416
|
+
# chash should be an array which is basically the order of rows to be printed
|
417
|
+
# it contains index, which is the offset of the row in the data @list
|
418
|
+
# When printing we should loop through chash and get the index in data
|
419
|
+
#
|
420
|
+
# should be zero here, but then we won't get textpad correct
|
421
|
+
@_header_adjustment = 0 #1
|
422
|
+
@col_min_width = 3
|
423
|
+
|
424
|
+
self.extend DefaultListSelection
|
425
|
+
super
|
426
|
+
create_default_renderer unless @renderer # 2014-04-10 - 11:01
|
427
|
+
# NOTE listselection takes + and - for ask_select
|
428
|
+
bind_key(?w, "next column") { self.next_column }
|
429
|
+
bind_key(?b, "prev column") { self.prev_column }
|
430
|
+
bind_key(?\M-\-, "contract column") { self.contract_column }
|
431
|
+
bind_key(?\M-+, "expand column") { self.expand_column }
|
432
|
+
bind_key(?=, "expand column to width") { self.expand_column_to_width }
|
433
|
+
bind_key(?\M-=, "expand column to width") { self.expand_column_to_max_width }
|
434
|
+
bind_key(?\C-s, "Save as") { self.save_as(nil) }
|
435
|
+
#@list_selection_model ||= DefaultListSelectionModel.new self
|
436
|
+
set_default_selection_model unless @list_selection_model
|
437
|
+
end
|
438
|
+
|
439
|
+
# set the default selection model as the operational one
|
440
|
+
def set_default_selection_model
|
441
|
+
@list_selection_model = nil
|
442
|
+
@list_selection_model = Canis::DefaultListSelectionModel.new self
|
443
|
+
end
|
444
|
+
|
445
|
+
# retrieve the column info structure for the given offset. The offset
|
446
|
+
# pertains to the visible offset not actual offset in data model.
|
447
|
+
# These two differ when we move a column.
|
448
|
+
# @return ColumnInfo object containing width align color bgcolor attrib hidden
|
449
|
+
def get_column index
|
450
|
+
return @chash[index] if @chash[index]
|
451
|
+
# create a new entry since none present
|
452
|
+
c = ColumnInfo.new
|
453
|
+
c.index = index
|
454
|
+
@chash[index] = c
|
455
|
+
return c
|
456
|
+
end
|
457
|
+
##
|
458
|
+
# returns collection of ColumnInfo objects
|
459
|
+
def column_model
|
460
|
+
@chash
|
461
|
+
end
|
462
|
+
|
463
|
+
# calculate pad width based on widths of columns
|
464
|
+
def content_cols
|
465
|
+
total = 0
|
466
|
+
#@chash.each_pair { |i, c|
|
467
|
+
#@chash.each_with_index { |c, i|
|
468
|
+
#next if c.hidden
|
469
|
+
each_column {|c,i|
|
470
|
+
w = c.width
|
471
|
+
# if you use prepare_format then use w+2 due to separator symbol
|
472
|
+
total += w + 1
|
473
|
+
}
|
474
|
+
return total
|
475
|
+
end
|
476
|
+
|
477
|
+
#
|
478
|
+
# This calculates and stores the offset at which each column starts.
|
479
|
+
# Used when going to next column or doing a find for a string in the table.
|
480
|
+
# TODO store this inside the hash so it's not calculated again in renderer
|
481
|
+
#
|
482
|
+
def _calculate_column_offsets
|
483
|
+
@coffsets = []
|
484
|
+
total = 0
|
485
|
+
|
486
|
+
#@chash.each_pair { |i, c|
|
487
|
+
#@chash.each_with_index { |c, i|
|
488
|
+
#next if c.hidden
|
489
|
+
each_column {|c,i|
|
490
|
+
w = c.width
|
491
|
+
@coffsets[i] = total
|
492
|
+
c.offset = total
|
493
|
+
# if you use prepare_format then use w+2 due to separator symbol
|
494
|
+
total += w + 1
|
495
|
+
}
|
496
|
+
end
|
497
|
+
# Convert current cursor position to a table column
|
498
|
+
# calculate column based on curpos since user may not have
|
499
|
+
# user w and b keys (:next_column)
|
500
|
+
# @return [Fixnum] column index base 0
|
501
|
+
def _convert_curpos_to_column #:nodoc:
|
502
|
+
_calculate_column_offsets unless @coffsets
|
503
|
+
x = 0
|
504
|
+
@coffsets.each_with_index { |i, ix|
|
505
|
+
if @curpos < i
|
506
|
+
break
|
507
|
+
else
|
508
|
+
x += 1
|
509
|
+
end
|
510
|
+
}
|
511
|
+
x -= 1 # since we start offsets with 0, so first auto becoming 1
|
512
|
+
return x
|
513
|
+
end
|
514
|
+
# convert the row into something searchable so that offsets returned by +index+
|
515
|
+
# are exactly what is seen on the screen.
|
516
|
+
def to_searchable index
|
517
|
+
if @renderer
|
518
|
+
@renderer.to_searchable(@list[index])
|
519
|
+
else
|
520
|
+
@list[index].to_s
|
521
|
+
end
|
522
|
+
end
|
523
|
+
# jump cursor to next column
|
524
|
+
# TODO : if cursor goes out of view, then pad should scroll right or left and down
|
525
|
+
def next_column
|
526
|
+
# TODO take care of multipliers
|
527
|
+
_calculate_column_offsets unless @coffsets
|
528
|
+
c = @column_pointer.next
|
529
|
+
cp = @coffsets[c]
|
530
|
+
#$log.debug " next_column #{c} , #{cp} "
|
531
|
+
@curpos = cp if cp
|
532
|
+
down() if c < @column_pointer.last_index
|
533
|
+
fire_column_event :ENTER_COLUMN
|
534
|
+
end
|
535
|
+
# jump cursor to previous column
|
536
|
+
# TODO : if cursor goes out of view, then pad should scroll right or left and down
|
537
|
+
def prev_column
|
538
|
+
# TODO take care of multipliers
|
539
|
+
_calculate_column_offsets unless @coffsets
|
540
|
+
c = @column_pointer.previous
|
541
|
+
cp = @coffsets[c]
|
542
|
+
#$log.debug " prev #{c} , #{cp} "
|
543
|
+
@curpos = cp if cp
|
544
|
+
up() if c > @column_pointer.last_index
|
545
|
+
fire_column_event :ENTER_COLUMN
|
546
|
+
end
|
547
|
+
# a column traversal has happened.
|
548
|
+
# FIXME needs to be looked into. is this consistent naming wise and are we using the correct object
|
549
|
+
# In old system it was TABLE_TRAVERSAL_EVENT
|
550
|
+
def fire_column_event eve
|
551
|
+
require 'canis/core/include/ractionevent'
|
552
|
+
aev = TextActionEvent.new self, eve, get_column(@column_pointer.current_index), @column_pointer.current_index, @column_pointer.last_index
|
553
|
+
fire_handler eve, aev
|
554
|
+
end
|
555
|
+
def expand_column
|
556
|
+
x = _convert_curpos_to_column
|
557
|
+
w = get_column(x).width
|
558
|
+
column_width x, w+1 if w
|
559
|
+
@coffsets = nil
|
560
|
+
fire_dimension_changed
|
561
|
+
end
|
562
|
+
def expand_column_to_width w=nil
|
563
|
+
x = _convert_curpos_to_column
|
564
|
+
unless w
|
565
|
+
# expand to width of current cell
|
566
|
+
s = @list[@current_index][x]
|
567
|
+
w = s.to_s.length + 1
|
568
|
+
end
|
569
|
+
column_width x, w
|
570
|
+
@coffsets = nil
|
571
|
+
fire_dimension_changed
|
572
|
+
end
|
573
|
+
# find the width of the longest item in the current columns and expand the width
|
574
|
+
# to that.
|
575
|
+
def expand_column_to_max_width
|
576
|
+
x = _convert_curpos_to_column
|
577
|
+
w = calculate_column_width x
|
578
|
+
expand_column_to_width w
|
579
|
+
end
|
580
|
+
def contract_column
|
581
|
+
x = _convert_curpos_to_column
|
582
|
+
w = get_column(x).width
|
583
|
+
return if w <= @col_min_width
|
584
|
+
column_width x, w-1 if w
|
585
|
+
@coffsets = nil
|
586
|
+
fire_dimension_changed
|
587
|
+
end
|
588
|
+
|
589
|
+
#def method_missing(name, *args)
|
590
|
+
#@tp.send(name, *args)
|
591
|
+
#end
|
592
|
+
#
|
593
|
+
# supply a custom renderer that implements +render()+
|
594
|
+
# @see render
|
595
|
+
def renderer r
|
596
|
+
@renderer = r
|
597
|
+
end
|
598
|
+
def header_adjustment
|
599
|
+
@_header_adjustment
|
600
|
+
end
|
601
|
+
|
602
|
+
##
|
603
|
+
# getter and setter for columns
|
604
|
+
# 2014-04-10 - 13:49
|
605
|
+
# @param [Array] columns to set as Array of Strings
|
606
|
+
# @return if no args, returns array of column names as Strings
|
607
|
+
# NOTE
|
608
|
+
# Appends columns to array, so it must be set before data, and thus it should
|
609
|
+
# clear the list
|
610
|
+
#
|
611
|
+
def columns(*val)
|
612
|
+
if val.empty?
|
613
|
+
# returns array of column names as Strings
|
614
|
+
@list[0]
|
615
|
+
else
|
616
|
+
array = val[0]
|
617
|
+
@_header_adjustment = 1
|
618
|
+
@list ||= []
|
619
|
+
@list.clear
|
620
|
+
@list << array
|
621
|
+
_init_model array
|
622
|
+
|
623
|
+
# update the names in column model
|
624
|
+
array.each_with_index { |n,i|
|
625
|
+
c = get_column(i)
|
626
|
+
c.name = name
|
627
|
+
}
|
628
|
+
self
|
629
|
+
end
|
630
|
+
end
|
631
|
+
|
632
|
+
##
|
633
|
+
# Set column titles with given array of strings.
|
634
|
+
# NOTE: This is only required to be called if first row of file or content does not contain
|
635
|
+
# titles. In that case, this should be called before setting the data as the array passed
|
636
|
+
# is appended into the content array.
|
637
|
+
# @deprecated complicated, just use `columns()`
|
638
|
+
def columns=(array)
|
639
|
+
columns(array)
|
640
|
+
self
|
641
|
+
end
|
642
|
+
alias :headings= :columns=
|
643
|
+
|
644
|
+
|
645
|
+
# size each column based on widths of this row of data.
|
646
|
+
# Only changed width if no width for that column
|
647
|
+
def _init_model array
|
648
|
+
# clear the column data -- this line should be called otherwise previous tables stuff will remain.
|
649
|
+
@chash.clear
|
650
|
+
array.each_with_index { |e,i|
|
651
|
+
# if columns added later we could be overwriting the width
|
652
|
+
c = get_column(i)
|
653
|
+
c.width ||= 10
|
654
|
+
}
|
655
|
+
# maintains index in current pointer and gives next or prev
|
656
|
+
@column_pointer = Circular.new array.size()-1
|
657
|
+
end
|
658
|
+
# size each column based on widths of this row of data.
|
659
|
+
def model_row index
|
660
|
+
array = @list[index]
|
661
|
+
array.each_with_index { |c,i|
|
662
|
+
# if columns added later we could be overwriting the width
|
663
|
+
ch = get_column(i)
|
664
|
+
ch.width = c.to_s.length + 2
|
665
|
+
}
|
666
|
+
# maintains index in current pointer and gives next or prev
|
667
|
+
@column_pointer = Circular.new array.size()-1
|
668
|
+
self
|
669
|
+
end
|
670
|
+
# estimate columns widths based on data in first 10 or so rows
|
671
|
+
# This will override any previous widths, so put custom widths
|
672
|
+
# after calling this.
|
673
|
+
def estimate_column_widths
|
674
|
+
each_column {|c,i|
|
675
|
+
c.width = suggest_column_width(i)
|
676
|
+
}
|
677
|
+
self
|
678
|
+
end
|
679
|
+
# calculates and returns a suggested columns width for given column
|
680
|
+
# based on data (first 10 rows)
|
681
|
+
# called by +estimate_column_widths+ in a loop
|
682
|
+
def suggest_column_width col
|
683
|
+
#ret = @cw[col] || 2
|
684
|
+
ret = get_column(col).width || 2
|
685
|
+
ctr = 0
|
686
|
+
@list.each_with_index { |r, i|
|
687
|
+
#next if i < @toprow # this is also a possibility, it checks visible rows
|
688
|
+
break if ctr > 10
|
689
|
+
ctr += 1
|
690
|
+
next if r == :separator
|
691
|
+
c = r[col]
|
692
|
+
x = c.to_s.length
|
693
|
+
ret = x if x > ret
|
694
|
+
}
|
695
|
+
ret
|
696
|
+
end
|
697
|
+
|
698
|
+
#------- data modification methods ------#
|
699
|
+
|
700
|
+
# I am assuming the column has been set using +columns=+
|
701
|
+
# Now only data is being sent in
|
702
|
+
# NOTE : calling set_content sends to TP's +text()+ which resets @list
|
703
|
+
# @param lines is an array or arrays
|
704
|
+
def text lines, fmt=:none
|
705
|
+
# maybe we can check this out
|
706
|
+
# should we not erase data, will user keep one column and resetting data ?
|
707
|
+
# set_content assumes data is gone.
|
708
|
+
@list ||= [] # this would work if no columns
|
709
|
+
@list.concat( lines)
|
710
|
+
fire_dimension_changed
|
711
|
+
self
|
712
|
+
end
|
713
|
+
|
714
|
+
##
|
715
|
+
# set column array and data array in one shot
|
716
|
+
# Erases any existing content
|
717
|
+
def resultset columns, data
|
718
|
+
@list = []
|
719
|
+
columns(columns)
|
720
|
+
text(data)
|
721
|
+
end
|
722
|
+
# Takes the name of a file containing delimited data
|
723
|
+
# and load it into the table.
|
724
|
+
# This method will load and split the file into the table.
|
725
|
+
# @param name is the file name
|
726
|
+
# @param config is a hash containing:
|
727
|
+
# - :separator - field separator, default is TAB
|
728
|
+
# - :columns - array of column names
|
729
|
+
# or true - first row is column names
|
730
|
+
# or false - no columns.
|
731
|
+
#
|
732
|
+
# == NOTE
|
733
|
+
# if columns is not mentioned, then it defaults to false
|
734
|
+
#
|
735
|
+
# == Example
|
736
|
+
#
|
737
|
+
# table = Table.new ...
|
738
|
+
# table.filename 'contacts.tsv', :separator => '|', :columns => true
|
739
|
+
#
|
740
|
+
def filename name, _config = {}
|
741
|
+
arr = File.open(name,"r").read.split("\n")
|
742
|
+
lines = []
|
743
|
+
sep = _config[:separator] || _config[:delimiter] || '\t'
|
744
|
+
arr.each { |l| lines << l.split(sep) }
|
745
|
+
cc = _config[:columns]
|
746
|
+
if cc.is_a? Array
|
747
|
+
columns(cc)
|
748
|
+
text(lines)
|
749
|
+
elsif cc
|
750
|
+
# cc is true, use first row as column names
|
751
|
+
columns(lines[0])
|
752
|
+
text(lines[1..-1])
|
753
|
+
else
|
754
|
+
# cc is false - no columns
|
755
|
+
# XXX since columns() is not called, so chash is not cleared.
|
756
|
+
_init_model lines[0]
|
757
|
+
text(lines)
|
758
|
+
end
|
759
|
+
end
|
760
|
+
alias :load :filename
|
761
|
+
|
762
|
+
# save the table as a file
|
763
|
+
# @param String name of output file. If nil, user is prompted
|
764
|
+
# Currently, tabs are used as delimiter, but this could be based on input
|
765
|
+
# separator, or prompted.
|
766
|
+
def save_as outfile
|
767
|
+
_t = "(all rows)"
|
768
|
+
if @selected_indices.size > 0
|
769
|
+
_t = "(selected rows)"
|
770
|
+
end
|
771
|
+
unless outfile
|
772
|
+
outfile = get_string "Enter file name to save #{_t} as "
|
773
|
+
return unless outfile
|
774
|
+
end
|
775
|
+
|
776
|
+
# if there is a selection, then write only selected rows
|
777
|
+
l = nil
|
778
|
+
if @selected_indices.size > 0
|
779
|
+
l = []
|
780
|
+
@list.each_with_index { |v,i| l << v if @selected_indices.include? i }
|
781
|
+
else
|
782
|
+
l = @list
|
783
|
+
end
|
784
|
+
|
785
|
+
File.open(outfile, 'w') {|f|
|
786
|
+
l.each {|r|
|
787
|
+
line = r.join "\t"
|
788
|
+
f.puts line
|
789
|
+
}
|
790
|
+
}
|
791
|
+
end
|
792
|
+
#
|
793
|
+
|
794
|
+
## add a row to the table
|
795
|
+
# The name add will be removed soon, pls use << instead.
|
796
|
+
def add array
|
797
|
+
unless @list
|
798
|
+
# columns were not added, this most likely is the title
|
799
|
+
@list ||= []
|
800
|
+
_init_model array
|
801
|
+
end
|
802
|
+
@list << array
|
803
|
+
fire_dimension_changed
|
804
|
+
self
|
805
|
+
end
|
806
|
+
alias :<< :add
|
807
|
+
|
808
|
+
# delete a data row at index
|
809
|
+
#
|
810
|
+
# NOTE : This does not adjust for header_adjustment. So zero will refer to the header if there is one.
|
811
|
+
# This is to keep consistent with textpad which does not know of header_adjustment and uses the actual
|
812
|
+
# index. Usually, programmers will be dealing with +@current_index+
|
813
|
+
#
|
814
|
+
def delete_at ix
|
815
|
+
return unless @list
|
816
|
+
raise ArgumentError, "Argument must be within 0 and #{@list.length}" if ix < 0 or ix >= @list.length
|
817
|
+
fire_dimension_changed
|
818
|
+
#@list.delete_at(ix + @_header_adjustment)
|
819
|
+
@list.delete_at(ix)
|
820
|
+
end
|
821
|
+
#
|
822
|
+
# clear the list completely
|
823
|
+
def clear
|
824
|
+
@selected_indices.clear
|
825
|
+
super
|
826
|
+
end
|
827
|
+
|
828
|
+
# get the value at the cell at row and col
|
829
|
+
# @return String
|
830
|
+
def get_value_at row,col
|
831
|
+
actrow = row + @_header_adjustment
|
832
|
+
@list[actrow, col]
|
833
|
+
end
|
834
|
+
|
835
|
+
# set value at the cell at row and col
|
836
|
+
# @param int row
|
837
|
+
# @param int col
|
838
|
+
# @param String value
|
839
|
+
# @return self
|
840
|
+
def set_value_at row,col,val
|
841
|
+
actrow = row + @_header_adjustment
|
842
|
+
@list[actrow , col] = val
|
843
|
+
fire_row_changed actrow
|
844
|
+
self
|
845
|
+
end
|
846
|
+
|
847
|
+
#------- column related methods ------#
|
848
|
+
#
|
849
|
+
# convenience method to set width of a column
|
850
|
+
# @param index of column
|
851
|
+
# @param width
|
852
|
+
# For setting other attributes, use get_column(index)
|
853
|
+
def column_width colindex, width
|
854
|
+
get_column(colindex).width = width
|
855
|
+
_invalidate_width_cache
|
856
|
+
end
|
857
|
+
# convenience method to set alignment of a column
|
858
|
+
# @param index of column
|
859
|
+
# @param align - :right (any other value is taken to be left)
|
860
|
+
def column_align colindex, align
|
861
|
+
get_column(colindex).align = align
|
862
|
+
end
|
863
|
+
# convenience method to hide or unhide a column
|
864
|
+
# Provided since column offsets need to be recalculated in the case of a width
|
865
|
+
# change or visibility change
|
866
|
+
def column_hidden colindex, hidden
|
867
|
+
get_column(colindex).hidden = hidden
|
868
|
+
_invalidate_width_cache
|
869
|
+
end
|
870
|
+
# http://www.opensource.apple.com/source/gcc/gcc-5483/libjava/javax/swing/table/DefaultTableColumnModel.java
|
871
|
+
def _invalidate_width_cache #:nodoc:
|
872
|
+
@coffsets = nil
|
873
|
+
end
|
874
|
+
##
|
875
|
+
# should all this move into table column model or somepn
|
876
|
+
# move a column from offset ix to offset newix
|
877
|
+
def move_column ix, newix
|
878
|
+
acol = @chash.delete_at ix
|
879
|
+
@chash.insert newix, acol
|
880
|
+
_invalidate_width_cache
|
881
|
+
#tmce = TableColumnModelEvent.new(ix, newix, self, :MOVE)
|
882
|
+
#fire_handler :TABLE_COLUMN_MODEL_EVENT, tmce
|
883
|
+
end
|
884
|
+
# TODO
|
885
|
+
def add_column tc
|
886
|
+
raise "to figure out add_column"
|
887
|
+
_invalidate_width_cache
|
888
|
+
end
|
889
|
+
# TODO
|
890
|
+
def remove_column tc
|
891
|
+
raise "to figure out add_column"
|
892
|
+
_invalidate_width_cache
|
893
|
+
end
|
894
|
+
def calculate_column_width col, maxrows=99
|
895
|
+
ret = 3
|
896
|
+
ctr = 0
|
897
|
+
@list.each_with_index { |r, i|
|
898
|
+
#next if i < @toprow # this is also a possibility, it checks visible rows
|
899
|
+
break if ctr > maxrows
|
900
|
+
ctr += 1
|
901
|
+
#next if r == :separator
|
902
|
+
c = r[col]
|
903
|
+
x = c.to_s.length
|
904
|
+
ret = x if x > ret
|
905
|
+
}
|
906
|
+
ret
|
907
|
+
end
|
908
|
+
##
|
909
|
+
# refresh pad onto window
|
910
|
+
# overrides super due to header_adjustment and the header too
|
911
|
+
def padrefresh
|
912
|
+
top = @window.top
|
913
|
+
left = @window.left
|
914
|
+
sr = @startrow + top
|
915
|
+
sc = @startcol + left
|
916
|
+
# first do header always in first row
|
917
|
+
retval = FFI::NCurses.prefresh(@pad,0,@pcol, sr , sc , 2 , @cols+ sc );
|
918
|
+
# now print rest of data
|
919
|
+
# h is header_adjustment
|
920
|
+
h = 1
|
921
|
+
retval = FFI::NCurses.prefresh(@pad,@prow + h,@pcol, sr + h , sc , @rows + sr , @cols+ sc );
|
922
|
+
$log.warn "XXX: PADREFRESH #{retval}, #{@prow}, #{@pcol}, #{sr}, #{sc}, #{@rows+sr}, #{@cols+sc}." if retval == -1
|
923
|
+
# padrefresh can fail if width is greater than NCurses.COLS
|
924
|
+
end
|
925
|
+
|
926
|
+
def create_default_sorter
|
927
|
+
raise "Data not sent in." unless @list
|
928
|
+
@table_row_sorter = DefaultTableRowSorter.new @list
|
929
|
+
end
|
930
|
+
# set a default renderer
|
931
|
+
#--
|
932
|
+
# we were not doing this automatically, so repaint was going to TP and failing on mvaddstr
|
933
|
+
# 2014-04-10 - 10:57
|
934
|
+
#++
|
935
|
+
def create_default_renderer
|
936
|
+
r = DefaultTableRenderer.new self
|
937
|
+
renderer(r)
|
938
|
+
end
|
939
|
+
# returns true if focus is on header_row
|
940
|
+
def header_row?
|
941
|
+
#@prow == 0
|
942
|
+
@prow == @current_index
|
943
|
+
end
|
944
|
+
|
945
|
+
# called when ENTER is pressed.
|
946
|
+
# Takes into account if user is on header_row
|
947
|
+
def fire_action_event
|
948
|
+
if header_row?
|
949
|
+
if @table_row_sorter
|
950
|
+
x = _convert_curpos_to_column
|
951
|
+
c = @chash[x]
|
952
|
+
# convert to index in data model since sorter only has data_model
|
953
|
+
index = c.index
|
954
|
+
@table_row_sorter.toggle_sort_order index
|
955
|
+
@table_row_sorter.sort
|
956
|
+
fire_dimension_changed
|
957
|
+
end
|
958
|
+
end
|
959
|
+
super
|
960
|
+
end
|
961
|
+
##
|
962
|
+
# Find the next row that contains given string
|
963
|
+
# Overrides textpad since each line is an array
|
964
|
+
# NOTE does not go to next match within row
|
965
|
+
# NOTE: FIXME ensure_visible puts prow = current_index so in this case, the header
|
966
|
+
# overwrites the matched row.
|
967
|
+
# @return row and col offset of match, or nil
|
968
|
+
# @param String to find
|
969
|
+
#@ deprecate since it does not get second match in line. textpad does
|
970
|
+
# however, the offset textpad shows is wrong
|
971
|
+
def OLDnext_match str
|
972
|
+
_calculate_column_offsets unless @coffsets
|
973
|
+
first = nil
|
974
|
+
## content can be string or Chunkline, so we had to write <tt>index</tt> for this.
|
975
|
+
@list.each_with_index do |fields, ix|
|
976
|
+
#col = line.index str
|
977
|
+
#fields.each_with_index do |f, jx|
|
978
|
+
#@chash.each_with_index do |c, jx|
|
979
|
+
#next if c.hidden
|
980
|
+
each_column do |c,jx|
|
981
|
+
f = fields[c.index]
|
982
|
+
# value can be numeric
|
983
|
+
col = f.to_s.index str
|
984
|
+
if col
|
985
|
+
col += @coffsets[jx]
|
986
|
+
first ||= [ ix, col ]
|
987
|
+
if ix > @current_index
|
988
|
+
return [ix, col]
|
989
|
+
end
|
990
|
+
end
|
991
|
+
end
|
992
|
+
end
|
993
|
+
return first
|
994
|
+
end
|
995
|
+
# yields each column to caller method
|
996
|
+
# if yield returns true, collects index of row into array and returns the array
|
997
|
+
# @returns array of indices which can be empty
|
998
|
+
# Value yielded can be fixnum or date etc
|
999
|
+
def matching_indices
|
1000
|
+
raise "block required for matching_indices" unless block_given?
|
1001
|
+
@indices = []
|
1002
|
+
## content can be string or Chunkline, so we had to write <tt>index</tt> for this.
|
1003
|
+
@list.each_with_index do |fields, ix|
|
1004
|
+
flag = yield ix, fields
|
1005
|
+
if flag
|
1006
|
+
@indices << ix
|
1007
|
+
end
|
1008
|
+
end
|
1009
|
+
#$log.debug "XXX: INDICES found #{@indices}"
|
1010
|
+
if @indices.count > 0
|
1011
|
+
fire_dimension_changed
|
1012
|
+
init_vars
|
1013
|
+
else
|
1014
|
+
@indices = nil
|
1015
|
+
end
|
1016
|
+
#return @indices
|
1017
|
+
end
|
1018
|
+
def clear_matches
|
1019
|
+
# clear previous match so all data can show again
|
1020
|
+
if @indices && @indices.count > 0
|
1021
|
+
fire_dimension_changed
|
1022
|
+
init_vars
|
1023
|
+
end
|
1024
|
+
@indices = nil
|
1025
|
+
end
|
1026
|
+
##
|
1027
|
+
# Ensure current row is visible, if not make it first row
|
1028
|
+
# This overrides textpad due to header_adjustment, otherwise
|
1029
|
+
# during next_match, the header overrides the found row.
|
1030
|
+
# @param current_index (default if not given)
|
1031
|
+
#
|
1032
|
+
def ensure_visible row = @current_index
|
1033
|
+
unless is_visible? row
|
1034
|
+
@prow = @current_index - @_header_adjustment
|
1035
|
+
end
|
1036
|
+
end
|
1037
|
+
#
|
1038
|
+
# yields non-hidden columns (ColumnInfo) and the offset/index
|
1039
|
+
# This is the order in which columns are to be printed
|
1040
|
+
def each_column
|
1041
|
+
@chash.each_with_index { |c, i|
|
1042
|
+
next if c.hidden
|
1043
|
+
yield c,i if block_given?
|
1044
|
+
}
|
1045
|
+
end
|
1046
|
+
# calls the renderer for all rows of data giving them pad, lineno, and line data
|
1047
|
+
def render_all
|
1048
|
+
if @indices && @indices.count > 0
|
1049
|
+
@indices.each_with_index do |ix, jx|
|
1050
|
+
render @pad, jx, @list[ix]
|
1051
|
+
end
|
1052
|
+
else
|
1053
|
+
@list.each_with_index { |line, ix|
|
1054
|
+
#FFI::NCurses.mvwaddstr(@pad,ix, 0, @list[ix])
|
1055
|
+
render @pad, ix, line
|
1056
|
+
}
|
1057
|
+
end
|
1058
|
+
end
|
1059
|
+
# print footer containing line and total, overriding textpad which prints column offset also
|
1060
|
+
# This is called internally by +repaint()+ but can be overridden for more complex printing.
|
1061
|
+
def print_foot
|
1062
|
+
return unless @print_footer
|
1063
|
+
ha = @_header_adjustment
|
1064
|
+
# ha takes into account whether there are headers or not
|
1065
|
+
footer = "#{@current_index+1-ha} of #{@list.length-ha} "
|
1066
|
+
@graphic.printstring( @row + @height -1 , @col+2, footer, @color_pair || $datacolor, @footer_attrib)
|
1067
|
+
@repaint_footer_required = false
|
1068
|
+
end
|
1069
|
+
|
1070
|
+
end # class Table
|
1071
|
+
|
1072
|
+
end # module
|