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,319 @@
1
+ # Some methods for manipulating lists
2
+ # Different components may bind different keys to these
3
+ # Currently will be called by TextArea and the editable version
4
+ # of TextView (vieditable).
5
+ #
6
+ require 'canis/core/include/rinputdataevent'
7
+ module ListEditable
8
+
9
+ def remove_all
10
+ # don't create a new object, other dependents like selection model may suffer 2014-04-08 - 20:00
11
+ #@list = []
12
+ @list.clear
13
+ set_modified # added 2009-02-13 22:28 so repaints
14
+ end
15
+ # current behav is a mix of vim's D and C-k from alpine, i don;t know how i screwed it up like this
16
+ # Should be:
17
+ # 1. do not take cursor back by 1 (this is vims D behavior)
18
+ # 2. retain EOL, we need to evaluate at undo
19
+ # 3. if nothing coming in delete buffer then join next line here
20
+ # 4. if line is blank, it will go to delete line (i think).
21
+ # Earlier, a C-k at pos 0 would blank the line and not delete it (copied from alpine).
22
+ # The next C-k would delete. emacs deletes if C-k at pos 0.
23
+ def delete_eol
24
+ return -1 unless @editable
25
+ pos = @curpos -1 # retain from 0 till prev char
26
+ @delete_buffer = @buffer[@curpos..-1]
27
+ # currently eol is there in delete_buff often. Should i maintain it ? 2010-03-08 18:29 UNDO
28
+ #@delete_buffer.chomp! # new 2010-03-08 18:29 UNDO - this worked but hope does not have othe impact
29
+
30
+ # if pos is 0, pos-1 becomes -1, end of line!
31
+ @list[@current_index] = pos == -1 ? "" : @buffer[0..pos]
32
+ $log.debug "delete EOL :pos=#{pos}, #{@delete_buffer}: row: #{@list[@current_index]}:"
33
+ @buffer = @list[@current_index]
34
+ if @delete_buffer == ""
35
+ $log.debug " TA: DELETE going to join next "
36
+ join_next_line # pull next line in
37
+ end
38
+ oldcur = @curpos
39
+ #x cursor_backward if @curpos > 0 # this was vims behavior -- knoecked off
40
+ #fire_handler :CHANGE, self # 2008-12-09 14:56
41
+ fire_handler :CHANGE, InputDataEvent.new(oldcur,oldcur+@delete_buffer.length, self, :DELETE, @current_index, @delete_buffer) # 2008-12-24 18:34
42
+ set_modified
43
+ return @delete_buffer
44
+ end
45
+ def join_next_line
46
+ # return if last line TODO
47
+ buff = @list.delete_at(@current_index + 1)
48
+ if buff
49
+ $log.debug " TA: DELETE inside to join next #{buff} "
50
+ fire_handler :CHANGE, InputDataEvent.new(0,0+buff.length, self, :DELETE_LINE, @current_index+1, buff)
51
+ @buffer << buff
52
+ end
53
+ end
54
+ # deletes given line or current
55
+ # now fires DELETE_LINE so no guessing by undo manager
56
+ def delete_line line=@current_index
57
+ return -1 unless @editable
58
+ if !$multiplier || $multiplier == 0
59
+ @delete_buffer = @list.delete_at line
60
+ else
61
+ @delete_buffer = @list.slice!(line, $multiplier)
62
+ end
63
+ @curpos ||= 0 # rlist has no such var
64
+ $multiplier = 0
65
+ add_to_kill_ring @delete_buffer
66
+ @buffer = @list[@current_index]
67
+ if @buffer.nil?
68
+ up
69
+ setrowcol @row + 1, nil # @form.col
70
+ end
71
+ # warning: delete buffer can now be an array
72
+ fire_handler :CHANGE, InputDataEvent.new(@curpos,@curpos+@delete_buffer.length, self, :DELETE_LINE, line, @delete_buffer) # 2008-12-24 18:34
73
+ set_modified
74
+ # next line being called from textarea which is old style and thus bombs
75
+ fire_dimension_changed if respond_to? :fire_dimension_changed
76
+ end
77
+ def delete_curr_char num=($multiplier == 0 ? 1 : $multiplier)
78
+ return -1 unless @editable
79
+ delete_at @curpos, num # changed so only one event, and one undo
80
+ set_modified
81
+ $multiplier = 0
82
+ end
83
+ #
84
+ # 2010-03-08 23:30 does not seem to be working well when backspacing at first char of line
85
+ # FIXME should work as a unit, so one undo and one fire_handler, at least if on one line.
86
+ def delete_prev_char num=($multiplier == 0 ? 1 : $multiplier)
87
+ return -1 if !@editable
88
+ num.times do
89
+ if @curpos <= 0
90
+ join_to_prev_line
91
+ return
92
+ end
93
+ @curpos -= 1 if @curpos > 0
94
+ delete_at
95
+ set_modified
96
+ addcol -1
97
+ end
98
+ $multiplier = 0
99
+ end
100
+ # open a new line and add chars to it.
101
+ # FIXME does not fire handler, thus won't undo
102
+ def append_row lineno=@current_index, chars=""
103
+ $log.debug "append row sapce:#{chars}."
104
+ @list.insert lineno+1, chars
105
+ end
106
+ ##
107
+ # delete character/s on current line
108
+ def delete_at index=@curpos, howmany=1
109
+ return -1 if !@editable
110
+ $log.debug "delete_at (characters) : #{@current_index} #{@buffer} #{index}"
111
+ char = @buffer.slice!(@curpos,howmany) # changed added ,1 and take char for event
112
+ # if no newline at end of this then bring up prev character/s till maxlen
113
+ # NO WE DON'T DO THIS ANYLONGER 2008-12-26 21:09 lets see
114
+ =begin
115
+ if @buffer[-1,1]!="\r"
116
+ @buffer[-1]=" " if @buffer[-1,1]=="\n"
117
+ if !next_line.nil? and next_line.length > 0
118
+ move_chars_up
119
+ end
120
+ end
121
+ =end
122
+ set_modified true
123
+ fire_handler :CHANGE, InputDataEvent.new(@curpos,@curpos+howmany, self, :DELETE, @current_index, char) # 2008-12-24 18:34
124
+ end
125
+ def undo_handler(uh)
126
+ @undo_handler = uh
127
+ end
128
+ ## THIS ONE SHOULD BE IN TEXTVIEW ALSO
129
+ # saves current or n lines into kill ring, appending to earlier contents
130
+ # Use yank (paste) or yank-pop to retrieve
131
+ def kill_ring_save
132
+ pointer = @current_index
133
+ list = []
134
+ repeatm {
135
+ line = @list[pointer]
136
+ list << line unless line.nil?
137
+ pointer += 1
138
+ }
139
+ add_to_kill_ring list
140
+ end
141
+ ## THIS ONE SHOULD BE IN TEXTVIEW ALSO
142
+ # add given line or lines to kill_ring
143
+ def add_to_kill_ring list
144
+ # directly referenceing kill_ring. We need to OO it a bit, so we can change internals w'o breaking all.
145
+ # FIXME
146
+ if $append_next_kill
147
+ # user requested this kill to be appened to last kill, so it can be yanked as one
148
+ #$kill_ring.last << list
149
+ last = $kill_ring.pop
150
+ $log.debug "YANK: addto : last= #{last} , list= #{list} "
151
+ case list
152
+ when Array
153
+ #list.insert 0, last
154
+ list.insert 0, *last # 2011-10-10 changed as it was wrong in textarea
155
+ $kill_ring << list
156
+ when String
157
+ $kill_ring << [last, list]
158
+ end
159
+ else
160
+ $kill_ring << list
161
+ end
162
+ $kill_ring_pointer = $kill_ring.size
163
+ $append_next_kill = false
164
+ $log.debug "YANK: kill_ring: #{$kill_ring} "
165
+ end
166
+
167
+ # pastes recent (last) entry of kill_ring.
168
+ # This can be one or more lines. Please note that for us vimmer's yank means copy
169
+ # but for emacsers it seems to mean paste. Aargh!!
170
+ # earlier it was not +1, it was pasting before not after
171
+ def yank where=@current_index+1
172
+ return -1 if !@editable
173
+ return if $kill_ring.empty?
174
+ row = $kill_ring.last
175
+ $log.debug "YANK: row #{row} "
176
+ index = where
177
+ case row
178
+ when Array
179
+ #index = @current_index
180
+ row.each{ |r|
181
+ @list.insert index, r.dup
182
+ index += 1
183
+ }
184
+ $kill_last_pop_size = row.size
185
+ when String
186
+ #@list[@current_index].insert row.dup
187
+ #@list.insert @current_index, row.dup
188
+ @list.insert index, row.dup
189
+ $kill_last_pop_size = 1
190
+ else
191
+ raise "textarea yank got uncertain datatype from kill_ring #{row.class} "
192
+ end
193
+ $kill_ring_pointer = $kill_ring.size - 1
194
+ $kill_ring_index = @current_index # pops will replace data in this row, never an insert
195
+ @repaint_required = true
196
+ @widget_scrolled = true
197
+ # XXX not firing anything here, so i can't undo. yet, i don't know whether a yank will
198
+ # be followed by a yank-pop, in which case it will not be undone.
199
+ # object row can be string or array - time to use INSERT_LINE so we are clear
200
+ # row.length can be array's size or string length - beware
201
+ fire_handler :CHANGE, InputDataEvent.new(0,row.length, self, :INSERT_LINE, @current_index, row)
202
+ return 0 # don't want a UNHANDLED or NO_BLOCK going back
203
+ end
204
+
205
+ # paste previous entries from kill ring
206
+ # I am not totally clear on this, not being an emacs user. but seems you have to do C-y
207
+ # once (yank) before you can do a yank pop.
208
+ def yank_pop
209
+ return -1 if !@editable
210
+ return if $kill_ring.empty?
211
+ mapped_key = @current_key # we are mapped to this
212
+ # checking that user has done a yank on this row. We only replace on the given row, never
213
+ # insert. But what if user edited after yank, Sheesh ! XXX
214
+ if $kill_ring_index != @current_index
215
+ Ncurses.beep
216
+ return # error message required that user must yank first
217
+ end
218
+ # the real reason i put this into a loop is so that i can properly undo the
219
+ # action later if required. I only need to store the final selection.
220
+ # This also ensures the user doesn't wander off in between and come back.
221
+ row = nil
222
+ while true
223
+ # remove lines from last replace, then insert
224
+ index = @current_index
225
+ $kill_last_pop_size.times {
226
+ del = @list.delete_at index
227
+ }
228
+ row = $kill_ring[$kill_ring_pointer-$multiplier]
229
+ $multiplier = 0
230
+ index = @current_index
231
+ case row
232
+ when Array
233
+ row.each{ |r|
234
+ @list.insert index, r.dup
235
+ index += 1
236
+ }
237
+ $kill_last_pop_size = row.size
238
+ when String
239
+ @list.insert index, row.dup
240
+ $kill_last_pop_size = 1
241
+ else
242
+ raise "textarea yank_pop got uncertain datatype from kill_ring #{row.class} "
243
+ end
244
+
245
+ $kill_ring_pointer -= 1
246
+ if $kill_ring_pointer < 0
247
+ # should be size, but that'll give an error. need to find a way!
248
+ $kill_ring_pointer = $kill_ring.size - 1
249
+ end
250
+ @repaint_required = true
251
+ @widget_scrolled = true
252
+ my_win = @form || @parent_component.form # 2010-02-12 12:51
253
+ my_win.repaint
254
+ ch = @graphic.getchar
255
+ if ch != mapped_key
256
+ @graphic.ungetch ch # seems to work fine
257
+ return ch # XXX to be picked up by handle_key loop and processed
258
+ end
259
+ end
260
+ # object row can be string or array - time to use INSERT_LINE so we are clear
261
+ # row.length can be array's size or string length - beware
262
+ fire_handler :CHANGE, InputDataEvent.new(0,row.length, self, :INSERT_LINE, @current_index, row)
263
+ return 0
264
+ end
265
+ def append_next_kill
266
+ $append_next_kill = true
267
+ end
268
+ # deletes count words on current line
269
+ # Does not at this point go beyond the line
270
+ def delete_word
271
+ return -1 unless @editable
272
+ $multiplier = 1 if !$multiplier || $multiplier == 0
273
+ line = @current_index
274
+ pos = @curpos
275
+ @delete_buffer = ""
276
+ # currently only look in current line
277
+ $multiplier.times {
278
+ found = @buffer.index(/[[:punct:][:space:]]/, pos)
279
+ break if !found
280
+ $log.debug " delete_word: pos #{pos} found #{found} buff: #{@buffer} "
281
+ @delete_buffer << @buffer.slice!(pos..found)
282
+ }
283
+ return if @delete_buffer == ""
284
+ $log.debug " delete_word: delbuff #{@delete_buffer} "
285
+ add_to_kill_ring @delete_buffer
286
+ fire_handler :CHANGE, InputDataEvent.new(@curpos,@curpos+@delete_buffer.length, self, :DELETE, line, @delete_buffer) # 2008-12-24 18:34
287
+ set_modified
288
+ end
289
+ ##
290
+ # deletes forward till the occurence of a character
291
+ # it gets the char from the user
292
+ # Should we pass in the character (and accept it as a separate func) ???
293
+ def delete_forward
294
+ return -1 unless @editable
295
+ ch = @graphic.getchar
296
+ return if ch < 0 || ch > 255
297
+ char = ch.chr
298
+ $multiplier = 1 if !$multiplier || $multiplier == 0
299
+ line = @current_index
300
+ pos = @curpos
301
+ tmpbuf = ""
302
+ # currently only look in current line
303
+ $multiplier.times {
304
+ found = @buffer.index(char, pos)
305
+ break if !found
306
+ #$log.debug " delete_forward: pos #{pos} found #{found} buff: #{@buffer} "
307
+ # ideally do this in one shot outside loop, but its okay here for now
308
+ tmpbuf << @buffer.slice!(pos..found)
309
+ }
310
+ return if tmpbuf == ""
311
+ @delete_buffer = tmpbuf
312
+ $log.debug " delete_forward: delbuff #{@delete_buffer} "
313
+ add_to_kill_ring @delete_buffer
314
+ fire_handler :CHANGE, InputDataEvent.new(@curpos,@curpos+@delete_buffer.length, self, :DELETE, line, @delete_buffer) # 2008-12-24 18:34
315
+ set_modified
316
+ $multiplier = 0
317
+ end
318
+
319
+ end # end module
@@ -0,0 +1,61 @@
1
+ # Some methods for traversing list like widgets such as tree, listbox and maybe table
2
+ # Different components may bind different keys to these
3
+ #
4
+ module Canis
5
+ module ListOperations
6
+
7
+ # get a char ensure it is a char or number
8
+ # In this state, it could accept control and other chars.
9
+ private
10
+ def _ask_a_char
11
+ ch = @graphic.getch
12
+ #message "achar is #{ch}"
13
+ if ch < 26 || ch > 255
14
+ @graphic.ungetch ch
15
+ return :UNHANDLED
16
+ end
17
+ return ch.chr
18
+ end
19
+ public
20
+ # sets the selection to the next row starting with char
21
+ # Trying to return unhandled is having no effect right now. if only we could pop it into a
22
+ # stack or unget it.
23
+ def set_selection_for_char char=nil
24
+ char = _ask_a_char unless char
25
+ #alert "got #{char} "
26
+ return :UNHANDLED if char == :UNHANDLED
27
+ @oldrow = @current_index
28
+ @last_regex = /^#{char}/
29
+ ix = next_regex @last_regex
30
+ #alert "next returned #{ix}"
31
+ return unless ix
32
+ @current_index = ix[0]
33
+ @search_found_ix = @current_index
34
+ @curpos = ix[1]
35
+ ensure_visible
36
+ return @current_index
37
+ end
38
+ # Find the next row that contains given string
39
+ # @return row and col offset of match, or nil
40
+ # @param String to find
41
+ def next_regex str
42
+ first = nil
43
+ ## content can be string or Chunkline, so we had to write <tt>index</tt> for this.
44
+ ## =~ does not give an error, but it does not work.
45
+ @list.each_with_index do |line, ix|
46
+ #col = line.index str
47
+ # for treemodel which will give us user_object.to_s
48
+ col = line.to_s.index str
49
+ if col
50
+ first ||= [ ix, col ]
51
+ if ix > @current_index
52
+ return [ix, col]
53
+ end
54
+ end
55
+ end
56
+ return first
57
+ end
58
+
59
+
60
+ end # end module
61
+ end # end module
@@ -0,0 +1,388 @@
1
+ #!/usr/bin/env ruby -w
2
+ # ----------------------------------------------------------------------------- #
3
+ # File: listselectionmodel.rb
4
+ # Description: Used by textpad derivates to give selection of rows
5
+ # Author: j kepler http://github.com/mare-imbrium/canis/
6
+ # Date: 2014-04-10 - 21:04
7
+ # License: Same as ruby license
8
+ # Last update: 2014-07-07 00:36
9
+ # ----------------------------------------------------------------------------- #
10
+ # listselectionmodel.rb Copyright (C) 2012-2014 j kepler
11
+ # ----------------------------------------------------------------------------- #
12
+ #
13
+ require 'forwardable'
14
+
15
+ # The +DefaultListSelection+ mixin provides Textpad derived classes with
16
+ # selection methods and bindings.
17
+ # == Example
18
+ # Inside the constructor of the multiline object use the following line, before the call to `super()`
19
+ #
20
+ # self.extend DefaultListSelection
21
+ #
22
+ # At any other portion, for example after the call to `super()` you may set the
23
+ # default model if the user has not done so in the calling block
24
+ #
25
+ # @list_selection_model ||= Canis::DefaultListSelectionModel.new self
26
+ #
27
+ # When clearing data, as in `clear`, +selected_indices+ should also be cleared.
28
+ #
29
+ # == Note
30
+ #
31
+ # This module does not take care of rendering a selected row. This must still be handled
32
+ # by the default or custom renderer using `is_row_selected?`.
33
+ #
34
+ # Note that changing the order of data, or deleting, inserting etc will not correct the selection
35
+ # indices. Indices are assumed to be stable, and they may be cleared using `clear` on +@selected_indices+
36
+ # if the data indices change.
37
+ #
38
+ module Canis
39
+ extend self
40
+ module DefaultListSelection
41
+ def self.extended(obj)
42
+ extend Forwardable
43
+ # selection modes may be :multiple, :single or :none
44
+ dsl_accessor :selection_mode
45
+ # color of selected rows, and attribute of selected rows
46
+ dsl_accessor :selected_color, :selected_bgcolor, :selected_attr
47
+ # indices of selected rows
48
+ dsl_accessor :selected_indices
49
+ # model that takes care of selection operations
50
+ dsl_accessor :list_selection_model
51
+ #
52
+ # all operations of selection are delegated to the ListSelectionModel
53
+ def_delegators :@list_selection_model, :is_row_selected?, :toggle_row_selection, :select, :unselect, :is_selection_empty?, :clear_selection, :selected_rows, :select_all, :selected_values, :selected_value
54
+
55
+
56
+ obj.instance_exec {
57
+ @selected_indices = []
58
+ @selection_mode = :multiple # default is multiple intervals
59
+ #@list_selection_model = DefaultListSelectionModel.new obj
60
+ }
61
+
62
+ end
63
+ end # mod DefaultListSelection
64
+ # Whenever user selects one or more rows, this object is sent via event
65
+ # giving start row and last row of selection, object
66
+ # and type which is :INSERT :DELETE :CLEAR
67
+ class ListSelectionEvent < Struct.new(:firstrow, :lastrow, :source, :type)
68
+ end
69
+
70
+ ##
71
+ # Object that takes care of selection of rows.
72
+ # This may be replace with a custom object at time of instantiation of list
73
+ # Note that there are only two selection modes: single and multiple.
74
+ # Multiple refers to multiple intervals. There is also a multiple row selection
75
+ # mode, single interval, which only allows one range to be selected, much like a
76
+ # text object, i.e. any text editor.
77
+ #
78
+ ## I am copying this from listselectable. that was a module so was included and shared variables
79
+ # but now this is a class, and cannot access state as directly
80
+
81
+ class DefaultListSelectionModel
82
+
83
+ def initialize component
84
+ raise "Components passed to DefaultListSelectionModel is nil" unless component
85
+ @obj = component
86
+
87
+ @selected_indices = @obj.selected_indices
88
+ # in this case since it is called immediately upon extend, user cannot change this
89
+ # Need a method to let user change after extending
90
+ @selection_mode = @obj.selection_mode
91
+ list_bindings
92
+ end
93
+
94
+ # change selection of current row on pressing space bar (or keybinding)
95
+ # If mode is multiple, then this row is added to previous selections
96
+ # @example
97
+ # bind_key(32) { toggle_row_selection }
98
+ #
99
+ #
100
+ def toggle_row_selection crow=@obj.current_index
101
+ @last_clicked = crow
102
+ @repaint_required = true
103
+ case @obj.selection_mode
104
+ when :multiple
105
+ if @selected_indices.include? crow
106
+ @selected_indices.delete crow
107
+ lse = ListSelectionEvent.new(crow, crow, @obj, :DELETE)
108
+ @obj.fire_handler :LIST_SELECTION_EVENT, lse
109
+ else
110
+ @selected_indices << crow
111
+ lse = ListSelectionEvent.new(crow, crow, @obj, :INSERT)
112
+ @obj.fire_handler :LIST_SELECTION_EVENT, lse
113
+ end
114
+ else
115
+ # single - now change to use array only
116
+ @selected_index = @selected_indices[0]
117
+ if @selected_index == crow
118
+ @old_selected_index = @selected_index # 2011-10-15 so we can unhighlight
119
+ @selected_index = nil
120
+ @selected_indices.clear
121
+ lse = ListSelectionEvent.new(crow, crow, @obj, :DELETE)
122
+ @obj.fire_handler :LIST_SELECTION_EVENT, lse
123
+ else
124
+ @selected_indices[0] = crow
125
+ @obj.fire_row_changed(@old_selected_index) if @old_selected_index
126
+ @old_selected_index = crow # 2011-10-15 so we can unhighlight
127
+ lse = ListSelectionEvent.new(crow, crow, @obj, :INSERT)
128
+ @obj.fire_handler :LIST_SELECTION_EVENT, lse
129
+ end
130
+ end
131
+ @obj.fire_row_changed crow
132
+ #alert "toggling #{@selected_indices.join(',')}"
133
+ end
134
+ #
135
+ # Range select.
136
+ # Only for multiple mode.
137
+ # Uses the last row clicked on, till the current one.
138
+ # If user clicks inside a selcted range, then deselect from last click till current (remove from earlier)
139
+ # If user clicks outside selected range, then select from last click till current (add to earlier)
140
+ # typically bound to Ctrl-Space (0)
141
+ #
142
+ # @example
143
+ #
144
+ # bind_key(0) { range_select }
145
+ #
146
+ def range_select crow=@obj.current_index
147
+ #alert "add to selection fired #{@last_clicked}"
148
+ @last_clicked ||= crow
149
+ min = [@last_clicked, crow].min
150
+ max = [@last_clicked, crow].max
151
+ case @obj.selection_mode
152
+ when :multiple
153
+ if @selected_indices.include? crow
154
+ # delete from last_clicked until this one in any direction
155
+ min.upto(max){ |i| @selected_indices.delete i
156
+ @obj.fire_row_changed i
157
+ }
158
+ lse = ListSelectionEvent.new(min, max, @obj, :DELETE)
159
+ @obj.fire_handler :LIST_SELECTION_EVENT, lse
160
+ else
161
+ # add to selection from last_clicked until this one in any direction
162
+ min.upto(max){ |i| @selected_indices << i unless @selected_indices.include?(i)
163
+ @obj.fire_row_changed i
164
+ }
165
+ lse = ListSelectionEvent.new(min, max, @obj, :INSERT)
166
+ @obj.fire_handler :LIST_SELECTION_EVENT, lse
167
+ end
168
+ else
169
+ end
170
+ @last_clicked = crow # 2014-04-08 - 01:21 this was missing, i think it is required
171
+ self
172
+ end
173
+ # clears selected indices, typically called when multiple select
174
+ # Key binding is application specific
175
+ def clear_selection
176
+ return if @selected_indices.nil? || @selected_indices.empty?
177
+ arr = @selected_indices.dup # to un highlight
178
+ @selected_indices.clear
179
+ arr.each {|i| @obj.fire_row_changed(i) }
180
+ @selected_index = nil
181
+ @old_selected_index = nil
182
+ # User should ignore first two params
183
+ lse = ListSelectionEvent.new(0, arr.size, @obj, :CLEAR)
184
+ @obj.fire_handler :LIST_SELECTION_EVENT, lse
185
+ arr = nil
186
+ end
187
+
188
+ # returns +true+ if given row has been selected
189
+ # Now that we use only the array, the multiple check is good enough
190
+ def is_row_selected? crow
191
+ case @obj.selection_mode
192
+ when :multiple
193
+ @selected_indices.include? crow
194
+ else
195
+ @selected_index = @selected_indices[0]
196
+ crow == @selected_index
197
+ end
198
+ end
199
+ alias :is_selected? is_row_selected?
200
+
201
+ # after selecting, traverse selections forward
202
+ def goto_next_selection
203
+ return if selected_rows().length == 0
204
+ row = selected_rows().sort.find { |i| i > @obj.current_index }
205
+ row ||= @obj.current_index
206
+ #@obj.current_index = row
207
+ @obj.goto_line row
208
+ end
209
+
210
+ # after selecting, traverse selections backward
211
+ def goto_prev_selection
212
+ return if selected_rows().length == 0
213
+ row = selected_rows().sort{|a,b| b <=> a}.find { |i| i < @obj.current_index }
214
+ row ||= @obj.current_index
215
+ #@obj.current_index = row
216
+ @obj.goto_line row
217
+ end
218
+ # add the following range to selected items, unless already present
219
+ # should only be used if multiple selection interval
220
+ def add_row_selection_interval ix0, ix1
221
+ return if @obj.selection_mode != :multiple
222
+ @anchor_selection_index = ix0
223
+ @lead_selection_index = ix1
224
+ ix0.upto(ix1) {|i|
225
+ @selected_indices << i unless @selected_indices.include? i
226
+ @obj.fire_row_changed i
227
+ }
228
+ lse = ListSelectionEvent.new(ix0, ix1, @obj, :INSERT)
229
+ @obj.fire_handler :LIST_SELECTION_EVENT, lse
230
+ #$log.debug " DLSM firing LIST_SELECTION EVENT #{lse}"
231
+ end
232
+
233
+ # remove selected indices between given indices inclusive
234
+ def remove_row_selection_interval ix0, ix1
235
+ @anchor_selection_index = ix0
236
+ @lead_selection_index = ix1
237
+ arr = @selected_indices.dup # to un highlight
238
+ @selected_indices.delete_if {|x| x >= ix0 and x <= ix1 }
239
+ arr.each {|i| @obj.fire_row_changed(i) }
240
+ lse = ListSelectionEvent.new(ix0, ix1, @obj, :DELETE)
241
+ @obj.fire_handler :LIST_SELECTION_EVENT, lse
242
+ end
243
+ # convenience method to select next len rows
244
+ def insert_index_interval ix0, len
245
+ @anchor_selection_index = ix0
246
+ @lead_selection_index = ix0+len
247
+ add_row_selection_interval @anchor_selection_index, @lead_selection_index
248
+ end
249
+ # select all rows, you may specify starting row.
250
+ # if header row, then 1 else should be 0. Actually we should have a way to determine
251
+ # this, and the default should be zero.
252
+ def select_all start_row=0 #+@_header_adjustment
253
+ # don't select header row - need to make sure this works for all cases. we may
254
+ # need a variable instead of hardoded value
255
+ add_row_selection_interval start_row, @obj.list.count()-1
256
+ end
257
+
258
+ # toggle selection of entire list
259
+ # Requires application specific key binding
260
+ def invert_selection start_row=0 #+@_header_adjustment
261
+ start_row.upto(@obj.list.count()-1){|i| invert_row_selection i }
262
+ end
263
+
264
+ # toggles selection for given row
265
+ # Typically called by invert_selection
266
+ def invert_row_selection row=@obj.current_index
267
+ @repaint_required = true
268
+ if is_selected? row
269
+ remove_row_selection_interval(row, row)
270
+ else
271
+ add_row_selection_interval(row, row)
272
+ end
273
+ end
274
+ # selects all rows with the values given, leaving existing selections
275
+ # intact. Typically used after accepting search criteria, and getting a list of values
276
+ # to select (such as file names). Will not work with tables (array or array)
277
+ # TODO is this even needed, scrap
278
+ def select_values values
279
+ return unless values
280
+ values.each do |val|
281
+ row = @list.index val
282
+ add_row_selection_interval row, row unless row.nil?
283
+ end
284
+ end
285
+ # TODO is this even needed, scrap
286
+ # unselects all rows with the values given, leaving all other rows intact
287
+ # You can map "-" to ask_select and call this from there.
288
+ # bind_key(?+, :ask_select) # --> calls select_values
289
+ # bind_key(?-, :ask_unselect)
290
+ def unselect_values values
291
+ return unless values
292
+ values.each do |val|
293
+ row = @list.index val
294
+ remove_row_selection_interval row, row unless row.nil?
295
+ end
296
+ end
297
+ #
298
+ # Asks user to enter a string or pattern for selecting rows
299
+ # Selects rows based on pattern, leaving other selections as-is
300
+ def ask_select prompt="Enter selection pattern: "
301
+ ret = get_string prompt
302
+ return if ret.nil? || ret == ""
303
+ indices = get_matching_indices ret
304
+ #$log.debug "listselectionmodel: ask_select got matches#{@indices} "
305
+ return if indices.nil? || indices.empty?
306
+ indices.each { |e|
307
+ # will not work if single select !! FIXME
308
+ add_row_selection_interval e,e
309
+ }
310
+ end
311
+ # returns a list of matching indices using a simple regex match on given pattern
312
+ # returns an empty list if no match
313
+ def get_matching_indices pattern
314
+ matches = []
315
+ @obj.content.each_with_index { |e,i|
316
+ # convert to string for tables
317
+ e = e.to_s unless e.is_a? String
318
+ if e =~ /#{pattern}/
319
+ matches << i
320
+ end
321
+ }
322
+ return matches
323
+ end
324
+ # Asks user to enter a string or pattern for UNselecting rows
325
+ # UNSelects rows based on pattern, leaving other selections as-is
326
+ def ask_unselect prompt="Enter selection pattern: "
327
+ ret = get_string prompt
328
+ return if ret.nil? || ret == ""
329
+ indices = get_matching_indices ret
330
+ return if indices.nil? || indices.empty?
331
+ indices.each { |e|
332
+ # will not work if single select !! FIXME
333
+ remove_row_selection_interval e,e
334
+ }
335
+ end
336
+
337
+ ##
338
+ # bindings related to selection
339
+ #
340
+ def list_bindings
341
+ # freeing space for paging, now trying out 'v' as selector. 2014-04-14 - 18:57
342
+ @obj.bind_key($row_selector || 'v'.ord, 'toggle selection') { toggle_row_selection }
343
+
344
+ # the mode may be set to single after the constructor, so this would have taken effect.
345
+ if @obj.selection_mode == :multiple
346
+ # freeing ctrl_space for back paging, now trying out 'V' as selector. 2014-04-14 - 18:57
347
+ @obj.bind_key($range_selector || 'V'.ord, 'range select') { range_select }
348
+ @obj.bind_key(?+, 'ask_select') { ask_select }
349
+ @obj.bind_key(?-, 'ask_unselect') { ask_unselect }
350
+ @obj.bind_key(?a, 'select_all') {select_all}
351
+ @obj.bind_key(?*, 'invert_selection') { invert_selection }
352
+ @obj.bind_key(?u, 'clear_selection') { clear_selection }
353
+ @obj.bind_key([?g,?n], 'goto next selection'){ goto_next_selection } # mapping double keys like vim
354
+ @obj.bind_key([?g,?p], 'goto prev selection'){ goto_prev_selection } # mapping double keys like vim
355
+ end
356
+ @_header_adjustment ||= 0 # incase caller does not use
357
+ #@obj._events << :LIST_SELECTION_EVENT unless @obj._events.include? :LIST_SELECTION_EVENT
358
+ end
359
+ def list_init_vars
360
+ # uncommenting since link with obj will be broken
361
+ #@selected_indices = []
362
+ @selected_index = nil
363
+ @old_selected_index = nil
364
+ #@row_selected_symbol = ''
365
+ ## FIXME we are not doing selectors at present. should we, else remove this
366
+ if @show_selector
367
+ @row_selected_symbol ||= '*'
368
+ @row_unselected_symbol ||= ' '
369
+ @left_margin ||= @row_selected_symbol.length
370
+ end
371
+ end
372
+ # return the indices selected
373
+ def selected_rows
374
+ @selected_indices
375
+ end
376
+ # return the values selected
377
+ def selected_values
378
+ @obj.values_at(*@selected_indices)
379
+ end
380
+ # returns first selection, meant for convenience of single select listboxes
381
+ # earlier called selected_item
382
+ def selected_value
383
+ return nil if @selected_indices.empty?
384
+ @obj[@selected_indices.first]
385
+ #selected_values.first
386
+ end
387
+ end # class
388
+ end # mod Canis