canis 0.0.4

Sign up to get free protection for your applications and to get access to all the features.
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