canis 0.0.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (134) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +45 -0
  3. data/CHANGES +52 -0
  4. data/Gemfile +4 -0
  5. data/LICENSE.txt +22 -0
  6. data/README.md +24 -0
  7. data/Rakefile +2 -0
  8. data/canis.gemspec +25 -0
  9. data/examples/alpmenu.rb +46 -0
  10. data/examples/app.sample +19 -0
  11. data/examples/appemail.rb +191 -0
  12. data/examples/atree.rb +105 -0
  13. data/examples/bline.rb +181 -0
  14. data/examples/common/devel.rb +319 -0
  15. data/examples/common/file.rb +93 -0
  16. data/examples/data/README.markdown +9 -0
  17. data/examples/data/brew.txt +38 -0
  18. data/examples/data/color.2 +37 -0
  19. data/examples/data/gemlist.txt +59 -0
  20. data/examples/data/lotr.txt +12 -0
  21. data/examples/data/ports.txt +136 -0
  22. data/examples/data/table.txt +37 -0
  23. data/examples/data/tasks.csv +88 -0
  24. data/examples/data/tasks.txt +27 -0
  25. data/examples/data/todo.txt +16 -0
  26. data/examples/data/todocsv.csv +28 -0
  27. data/examples/data/unix1.txt +21 -0
  28. data/examples/data/unix2.txt +11 -0
  29. data/examples/dbdemo.rb +506 -0
  30. data/examples/dirtree.rb +177 -0
  31. data/examples/newtabbedwindow.rb +100 -0
  32. data/examples/newtesttabp.rb +92 -0
  33. data/examples/tabular.rb +212 -0
  34. data/examples/tasks.rb +179 -0
  35. data/examples/term2.rb +88 -0
  36. data/examples/testbuttons.rb +307 -0
  37. data/examples/testcombo.rb +102 -0
  38. data/examples/testdb.rb +182 -0
  39. data/examples/testfields.rb +208 -0
  40. data/examples/testflowlayout.rb +43 -0
  41. data/examples/testkeypress.rb +98 -0
  42. data/examples/testlistbox.rb +187 -0
  43. data/examples/testlistbox1.rb +199 -0
  44. data/examples/testmessagebox.rb +144 -0
  45. data/examples/testprogress.rb +116 -0
  46. data/examples/testree.rb +107 -0
  47. data/examples/testsplitlayout.rb +53 -0
  48. data/examples/testsplitlayout1.rb +49 -0
  49. data/examples/teststacklayout.rb +48 -0
  50. data/examples/testwsshortcuts.rb +68 -0
  51. data/examples/testwsshortcuts2.rb +129 -0
  52. data/lib/canis.rb +16 -0
  53. data/lib/canis/core/docs/index.txt +104 -0
  54. data/lib/canis/core/docs/list.txt +16 -0
  55. data/lib/canis/core/docs/style_help.yml +34 -0
  56. data/lib/canis/core/docs/tabbedpane.txt +15 -0
  57. data/lib/canis/core/docs/table.txt +31 -0
  58. data/lib/canis/core/docs/textpad.txt +48 -0
  59. data/lib/canis/core/docs/tree.txt +23 -0
  60. data/lib/canis/core/include/.DS_Store +0 -0
  61. data/lib/canis/core/include/action.rb +83 -0
  62. data/lib/canis/core/include/actionmanager.rb +49 -0
  63. data/lib/canis/core/include/appmethods.rb +179 -0
  64. data/lib/canis/core/include/bordertitle.rb +49 -0
  65. data/lib/canis/core/include/canisparser.rb +100 -0
  66. data/lib/canis/core/include/colorparser.rb +437 -0
  67. data/lib/canis/core/include/defaultfilerenderer.rb +64 -0
  68. data/lib/canis/core/include/io.rb +320 -0
  69. data/lib/canis/core/include/layouts/SplitLayout.rb +161 -0
  70. data/lib/canis/core/include/layouts/abstractlayout.rb +213 -0
  71. data/lib/canis/core/include/layouts/flowlayout.rb +104 -0
  72. data/lib/canis/core/include/layouts/stacklayout.rb +109 -0
  73. data/lib/canis/core/include/listbindings.rb +89 -0
  74. data/lib/canis/core/include/listeditable.rb +319 -0
  75. data/lib/canis/core/include/listoperations.rb +61 -0
  76. data/lib/canis/core/include/listselectionmodel.rb +388 -0
  77. data/lib/canis/core/include/multibuffer.rb +173 -0
  78. data/lib/canis/core/include/ractionevent.rb +73 -0
  79. data/lib/canis/core/include/rchangeevent.rb +27 -0
  80. data/lib/canis/core/include/rhistory.rb +95 -0
  81. data/lib/canis/core/include/rinputdataevent.rb +47 -0
  82. data/lib/canis/core/include/textdocument.rb +111 -0
  83. data/lib/canis/core/include/vieditable.rb +175 -0
  84. data/lib/canis/core/include/widgetmenu.rb +66 -0
  85. data/lib/canis/core/system/colormap.rb +165 -0
  86. data/lib/canis/core/system/keydefs.rb +32 -0
  87. data/lib/canis/core/system/ncurses.rb +237 -0
  88. data/lib/canis/core/system/panel.rb +129 -0
  89. data/lib/canis/core/system/window.rb +1081 -0
  90. data/lib/canis/core/util/ansiparser.rb +119 -0
  91. data/lib/canis/core/util/app.rb +696 -0
  92. data/lib/canis/core/util/basestack.rb +412 -0
  93. data/lib/canis/core/util/defaultcolorparser.rb +84 -0
  94. data/lib/canis/core/util/extras/README +5 -0
  95. data/lib/canis/core/util/extras/bottomline.rb +1815 -0
  96. data/lib/canis/core/util/extras/padreader.rb +192 -0
  97. data/lib/canis/core/util/focusmanager.rb +31 -0
  98. data/lib/canis/core/util/helpmanager.rb +160 -0
  99. data/lib/canis/core/util/oldwidgetshortcuts.rb +304 -0
  100. data/lib/canis/core/util/promptmenu.rb +235 -0
  101. data/lib/canis/core/util/rcommandwindow.rb +933 -0
  102. data/lib/canis/core/util/rdialogs.rb +520 -0
  103. data/lib/canis/core/util/textutils.rb +74 -0
  104. data/lib/canis/core/util/viewer.rb +238 -0
  105. data/lib/canis/core/util/widgetshortcuts.rb +508 -0
  106. data/lib/canis/core/widgets/applicationheader.rb +103 -0
  107. data/lib/canis/core/widgets/box.rb +58 -0
  108. data/lib/canis/core/widgets/divider.rb +310 -0
  109. data/lib/canis/core/widgets/extras/README.md +12 -0
  110. data/lib/canis/core/widgets/extras/rtextarea.rb +960 -0
  111. data/lib/canis/core/widgets/extras/stackflow.rb +474 -0
  112. data/lib/canis/core/widgets/keylabelprinter.rb +194 -0
  113. data/lib/canis/core/widgets/listbox.rb +326 -0
  114. data/lib/canis/core/widgets/listfooter.rb +86 -0
  115. data/lib/canis/core/widgets/rcombo.rb +210 -0
  116. data/lib/canis/core/widgets/rcontainer.rb +415 -0
  117. data/lib/canis/core/widgets/rlink.rb +30 -0
  118. data/lib/canis/core/widgets/rmenu.rb +970 -0
  119. data/lib/canis/core/widgets/rmenulink.rb +30 -0
  120. data/lib/canis/core/widgets/rmessagebox.rb +400 -0
  121. data/lib/canis/core/widgets/rprogress.rb +118 -0
  122. data/lib/canis/core/widgets/rtabbedpane.rb +631 -0
  123. data/lib/canis/core/widgets/rtabbedwindow.rb +70 -0
  124. data/lib/canis/core/widgets/rwidget.rb +3634 -0
  125. data/lib/canis/core/widgets/scrollbar.rb +147 -0
  126. data/lib/canis/core/widgets/statusline.rb +113 -0
  127. data/lib/canis/core/widgets/table.rb +1072 -0
  128. data/lib/canis/core/widgets/tabular.rb +264 -0
  129. data/lib/canis/core/widgets/textpad.rb +1674 -0
  130. data/lib/canis/core/widgets/tree.rb +690 -0
  131. data/lib/canis/core/widgets/tree/treecellrenderer.rb +150 -0
  132. data/lib/canis/core/widgets/tree/treemodel.rb +432 -0
  133. data/lib/canis/version.rb +3 -0
  134. 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