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,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