weft-qda 0.9.6

Sign up to get free protection for your applications and to get access to all the features.
Files changed (55) hide show
  1. data/lib/weft.rb +21 -0
  2. data/lib/weft/WEFT-VERSION-STRING.rb +1 -0
  3. data/lib/weft/application.rb +130 -0
  4. data/lib/weft/backend.rb +39 -0
  5. data/lib/weft/backend/marshal.rb +26 -0
  6. data/lib/weft/backend/mysql.rb +267 -0
  7. data/lib/weft/backend/n6.rb +366 -0
  8. data/lib/weft/backend/sqlite.rb +633 -0
  9. data/lib/weft/backend/sqlite/category_tree.rb +104 -0
  10. data/lib/weft/backend/sqlite/schema.rb +152 -0
  11. data/lib/weft/backend/sqlite/upgradeable.rb +55 -0
  12. data/lib/weft/category.rb +157 -0
  13. data/lib/weft/coding.rb +355 -0
  14. data/lib/weft/document.rb +118 -0
  15. data/lib/weft/filters.rb +243 -0
  16. data/lib/weft/wxgui.rb +687 -0
  17. data/lib/weft/wxgui/category.xpm +26 -0
  18. data/lib/weft/wxgui/dialogs.rb +128 -0
  19. data/lib/weft/wxgui/document.xpm +25 -0
  20. data/lib/weft/wxgui/error_handler.rb +52 -0
  21. data/lib/weft/wxgui/inspectors.rb +361 -0
  22. data/lib/weft/wxgui/inspectors/category.rb +165 -0
  23. data/lib/weft/wxgui/inspectors/codereview.rb +275 -0
  24. data/lib/weft/wxgui/inspectors/document.rb +139 -0
  25. data/lib/weft/wxgui/inspectors/imagedocument.rb +56 -0
  26. data/lib/weft/wxgui/inspectors/script.rb +35 -0
  27. data/lib/weft/wxgui/inspectors/search.rb +265 -0
  28. data/lib/weft/wxgui/inspectors/textcontrols.rb +304 -0
  29. data/lib/weft/wxgui/lang.rb +17 -0
  30. data/lib/weft/wxgui/lang/en.rb +45 -0
  31. data/lib/weft/wxgui/mondrian.xpm +44 -0
  32. data/lib/weft/wxgui/search.xpm +25 -0
  33. data/lib/weft/wxgui/sidebar.rb +498 -0
  34. data/lib/weft/wxgui/utilities.rb +148 -0
  35. data/lib/weft/wxgui/weft16.xpm +31 -0
  36. data/lib/weft/wxgui/workarea.rb +249 -0
  37. data/test/001-document.rb +196 -0
  38. data/test/002-category.rb +138 -0
  39. data/test/003-code.rb +370 -0
  40. data/test/004-application.rb +52 -0
  41. data/test/006-filters.rb +139 -0
  42. data/test/009a-backend_sqlite_basic.rb +280 -0
  43. data/test/009b-backend_sqlite_complex.rb +175 -0
  44. data/test/009c_backend_sqlite_bench.rb +81 -0
  45. data/test/010-backend_nudist.rb +5 -0
  46. data/test/all-tests.rb +1 -0
  47. data/test/manual-gui-script.txt +24 -0
  48. data/test/testdata/autocoding-test.txt +15 -0
  49. data/test/testdata/iso-8859-1.txt +5 -0
  50. data/test/testdata/sample_doc.txt +19 -0
  51. data/test/testdata/search_results.txt +1254 -0
  52. data/test/testdata/text1-dos-ascii.txt +2 -0
  53. data/test/testdata/text1-unix-utf8.txt +2 -0
  54. data/weft-qda.rb +28 -0
  55. metadata +96 -0
@@ -0,0 +1,56 @@
1
+ module QDA
2
+ module GUI
3
+
4
+ class ImageDocumentWindow < InspectorWindow
5
+ Wx::init_all_image_handlers
6
+
7
+ def initialize(image_file, *args)
8
+ @img = Wx::Image.new(image_file)
9
+ # magnifier
10
+ @bitmap = Wx::Bitmap.new( @img )
11
+ @zoom = 0
12
+ # check height & widget to set size of child window
13
+ # @bitmap.get_height
14
+ # sizer = Wx::BoxSizer.new(Wx::VERTICAL)
15
+ # sizer.add(img, 1, Wx::SHAPED|Wx::GROW, 5)
16
+ super(*args) do
17
+ # client_size = Wx::Size.new(@bitmap.width, @bitmap.height)
18
+ set_client_size(Wx::Size.new(@bitmap.width, @bitmap.height))
19
+ end
20
+ self.set_cursor( Wx::Cursor.new(Wx::CURSOR_MAGNIFIER) )
21
+ evt_paint { on_paint }
22
+ evt_left_up { | e | zoom_in(e) }
23
+ evt_right_up { | e | zoom_out(e) }
24
+ end
25
+
26
+ def on_paint
27
+ paint do | dc |
28
+ dc.clear
29
+ dc.draw_bitmap(@bitmap, 0, 0, false)
30
+ end
31
+ end
32
+
33
+ def zoom_in(*args)
34
+ @zoom += 1
35
+ zoom(*args)
36
+ end
37
+
38
+ def zoom_out(*args)
39
+ @zoom -= 1
40
+ zoom(*args)
41
+ end
42
+
43
+ def zoom(event)
44
+ img_x2 = @img.scale(@img.get_width * ( 2 ** @zoom ),
45
+ @img.get_height * ( 2 ** @zoom) )
46
+ #rotate90
47
+ @bitmap = Wx::Bitmap.new( img_x2 )
48
+ dc = Wx::ClientDC.new(self)
49
+ dc.clear
50
+ dc.draw_bitmap(@bitmap, 0, 0, false)
51
+ set_client_size(Wx::Size.new( @img.get_width * ( 2 ** @zoom ),
52
+ @img.get_height * ( 2 ** @zoom) ) )
53
+ end
54
+ end
55
+ end
56
+ end
@@ -0,0 +1,35 @@
1
+ module QDA::GUI
2
+ class ScriptWindow < WorkAreaWindow
3
+ def initialize(obj, app, workarea, layout)
4
+ super(workarea, "Script #{obj.dbid}", nil)
5
+ @app = app
6
+ code = ''
7
+ sizer = Wx::BoxSizer.new(Wx::VERTICAL)
8
+ @code = Wx::TextCtrl.new(self, -1, code, Wx::DEFAULT_POSITION,
9
+ Wx::DEFAULT_SIZE, Wx::TE_MULTILINE)
10
+ sizer.add(@code, 2, Wx::GROW|Wx::ALL, 5)
11
+
12
+ button = Wx::Button.new(self, -1, 'Run')
13
+ button.evt_button(button.get_id) { | e | on_run(e) }
14
+ sizer.add(button, 0, Wx::ALL, 5)
15
+
16
+ @output = Wx::TextCtrl.new(self, -1, '', Wx::DEFAULT_POSITION,
17
+ Wx::DEFAULT_SIZE,
18
+ Wx::TE_MULTILINE|Wx::TE_READONLY)
19
+ sizer.add(@output, 2, Wx::GROW|Wx::ALL, 5)
20
+ self.sizer = sizer
21
+ end
22
+
23
+ def on_run(e)
24
+ $stdout = result = StringIO.new()
25
+ @app.instance_eval( @code.get_value() )
26
+ # STDERR.puts result.inspect()
27
+ result.rewind()
28
+ @output.value = result.read()
29
+ $stdout = STDOUT
30
+ rescue Exception => err
31
+ @output.value = err
32
+ @output.value += err.backtrace.join("\n")
33
+ end
34
+ end
35
+ end
@@ -0,0 +1,265 @@
1
+ module QDA::GUI
2
+ # a window that allows the entry of criteria (category
3
+ # intersections and unions, text search, document attributes), and
4
+ # interactively presents the text meeting the criteria, and to save
5
+ # the results as a permanent category (memo describing the rules
6
+ # used to produce the codes.
7
+ class QueryWindow < InspectorWindow
8
+ include Subscriber
9
+
10
+ # obj is a simple object with a +dbid+ representing a project
11
+ # unique identifier for this query
12
+ def initialize(obj, app, workarea, layout)
13
+ @app = app
14
+ @query = obj # currently a pretty much empty object
15
+
16
+ super(workarea, "Query #{@query.dbid}", layout) do | parent |
17
+ @text_box = CompositeText.new(parent, '', 0, @app)
18
+ end
19
+ set_icon( app.fetch_icon("search.xpm") )
20
+ construct_query_panel
21
+ @notebook.selection = 1
22
+ subscribe()
23
+ # evt_set_focus() { | e | p "focussed #{e.event_object}"; e.skip() }
24
+ end
25
+
26
+ def associated_subscribers()
27
+ super << @objects.grep(CategoryDropDown)
28
+ end
29
+
30
+ # runs a query using the currently selected query options
31
+ def get_results()
32
+ begin
33
+ # remember the last query we did
34
+ @last_query = get_query_string
35
+ @last_results = @app.app.do_query( *get_query_arguments )
36
+ rescue Exception => err
37
+ Wx::MessageDialog.new(nil, err.to_s,
38
+ "Error running query",
39
+ Wx::OK|Wx::ICON_EXCLAMATION).show_modal()
40
+ return
41
+ end
42
+ return @last_results
43
+ end
44
+
45
+ def get_target_argument(offset, as_string = false)
46
+ return nil unless @objects[offset].is_shown
47
+ if @rules[offset].value == 'IS CODED BY'
48
+ if @objects[offset].current_category
49
+ category = @objects[offset].current_category
50
+ return as_string ? category.name : category.dbid
51
+ else
52
+ # use fail rather than raise as it gets interp as a method...
53
+ fail RuntimeError.new("No category value set for CODED BY")
54
+ end
55
+ elsif @rules[offset].value == 'CONTAINS WORD'
56
+ str = @objects[offset].get_value
57
+ if str.empty?
58
+ fail RuntimeError.new("No text search set for CONTAINS WORD")
59
+ else
60
+ return str
61
+ end
62
+ end
63
+ end
64
+
65
+ def get_query_arguments()
66
+ return @rules[0].value, get_target_argument(0),
67
+ @ops[0].value,
68
+ @rules[1].value, get_target_argument(1)
69
+ end
70
+
71
+ # TODO
72
+ # returns a stringified representation of the query. The only
73
+ # reason this is here at the moment is because the underlying
74
+ # query code parser doesn't accept string literal paths or names
75
+ # for categories- only database ids. This should be remedied, so
76
+ # the string passed to query and this are the same.
77
+ def get_query_string(join_char = " ")
78
+ begin
79
+ query_parts = [ @rules[0].value, get_target_argument(0, true) ]
80
+ # add on the second bit if used
81
+ # TODO - this is barbaric - this whole thing needs tidying
82
+ # and being made more extensible n = many
83
+ if @ops[0].value != ''
84
+ query_parts += [ @ops[0].value,
85
+ @rules[1].value, get_target_argument(1, true) ]
86
+ end
87
+ return query_parts.join(join_char)
88
+ rescue Exception => err
89
+ return ''
90
+ end
91
+ end
92
+
93
+ # run the query, display the results, view them
94
+ def on_view(e)
95
+ Wx::BusyCursor.busy do
96
+ results = get_results()
97
+ return unless results
98
+ @text_box.populate( results )
99
+ if curr_category = @drop_down.current_category()
100
+ @text_box.highlight_codingtable(curr_category.codes)
101
+ end
102
+ end
103
+ @notebook.selection = 0
104
+ end
105
+
106
+ def on_save_results(e)
107
+ # check for saved results
108
+ unless @last_results and ! @last_query.empty? and
109
+ @last_query == get_query_string()
110
+ Wx::BusyCursor.busy do
111
+ results = get_results()
112
+ return unless results
113
+ @text_box.populate(results)
114
+ end
115
+ end
116
+
117
+ q_string = get_query_string
118
+ title = "'#{q_string[0, 30]}...' (query results)"
119
+ search_parent = @app.app.get_root_category('SEARCHES')
120
+ query_cat = Category.new( title, search_parent )
121
+ query_cat.codetable = @last_results
122
+ query_cat.memo = get_query_string("\n")
123
+ $wxapp.app.save_category(query_cat)
124
+ $wxapp.broadcast(:category_added, query_cat)
125
+ end
126
+
127
+ def on_focus_dropdown(e)
128
+ # p "focussed #{e.event_object}"
129
+ # e.skip()
130
+ end
131
+
132
+ def on_blur_dropdown(e)
133
+ # p "blurred #{e.event_object}"
134
+ # p active?
135
+ # e.skip()
136
+ end
137
+
138
+ def construct_query_panel()
139
+ panel2 = Wx::Panel.new(@notebook, -1)
140
+ sizer2 = Wx::BoxSizer.new(Wx::VERTICAL)
141
+ sizer2.add( Wx::StaticText.new(panel2, -1, 'Find text that:'),
142
+ 0, Wx::ALL|Wx::ADJUST_MINSIZE, 4 )
143
+
144
+ # criteria consist of rules ('is coded by', 'text matches'),
145
+ # targets ('category "Foo"', 'text "Bar"') and may be joined by
146
+ # ops ('AND', 'OR')
147
+
148
+ @rules = []
149
+ @objects = []
150
+ @ops = []
151
+
152
+ @crit_panel = Wx::Panel.new(panel2, -1)
153
+ @crit_sizer = Wx::BoxSizer.new(Wx::VERTICAL)
154
+
155
+ add_criterion(true)
156
+ add_criterion(false)
157
+
158
+
159
+ @ops[0].evt_combobox(@ops[0].get_id) do | e |
160
+ operator = @ops[0].get_string_selection
161
+ if operator != ""
162
+ @rules[1].show(true)
163
+ @objects[1].show(true)
164
+ else
165
+ @rules[1].hide()
166
+ @objects[1].hide()
167
+ end
168
+ end
169
+ @rules[1].hide()
170
+ @objects[1].hide()
171
+
172
+ @crit_panel.set_sizer(@crit_sizer)
173
+ sizer2.add(@crit_panel, 1, Wx::GROW|Wx::ALL|Wx::ADJUST_MINSIZE, 4 )
174
+
175
+ butt_panel = Wx::Panel.new(panel2, -1)
176
+ bott_sizer = Wx::BoxSizer.new(Wx::HORIZONTAL)
177
+
178
+ button = Wx::Button.new(butt_panel, -1, 'View')
179
+ button.evt_button(button.get_id) { | e | on_view(e) }
180
+ bott_sizer.add(button, 1, Wx::ALL, 4)
181
+
182
+ button = Wx::Button.new(butt_panel, -1, 'Save Results')
183
+ button.evt_button(button.get_id) { | e | on_save_results(e) }
184
+ bott_sizer.add(button, 1, Wx::ALL, 4)
185
+
186
+ butt_panel.set_sizer(bott_sizer)
187
+ sizer2.add(butt_panel, 0,
188
+ Wx::GROW|Wx::ADJUST_MINSIZE|Wx::ALIGN_BOTTOM)
189
+ @notebook.add_page(panel2, 'query')
190
+ panel2.set_sizer_and_fit(sizer2)
191
+ # sizer2.set_size_hints(panel2)
192
+ end
193
+
194
+ def add_criterion(with_bool = false)
195
+ row_sizer = Wx::BoxSizer.new(Wx::HORIZONTAL)
196
+ row_n = @rules.length
197
+
198
+ type_op1 = Wx::ComboBox.new(@crit_panel, -1, '', Wx::DEFAULT_POSITION,
199
+ Wx::DEFAULT_SIZE, [],
200
+ Wx::CB_READONLY|Wx::CB_DROPDOWN)
201
+ type_op1.append('IS CODED BY')
202
+ type_op1.append('CONTAINS WORD')
203
+
204
+ type_op1.selection = 0
205
+ row_sizer.add( type_op1, 1, Wx::GROW|Wx::ALL)
206
+ @rules.push( type_op1 )
207
+
208
+ op2_placeholder = Wx::BoxSizer.new(Wx::VERTICAL)
209
+ type_op2 = CategoryDropDown.new(@app, @crit_panel, nil, true)
210
+ # TODO - allow currently highlighted category dropdown to
211
+ # receive global focus_category events
212
+ # type_op2.evt_set_focus() { | e | on_focus_dropdown(e) }
213
+ # type_op2.evt_kill_focus() { | e | on_blur_dropdown(e) }
214
+
215
+ op2_placeholder.add(type_op2, 1,
216
+ Wx:: GROW|Wx::ALL)
217
+
218
+ type_op2_a = Wx::TextCtrl.new(@crit_panel, -1, '')
219
+ type_op2_a.hide()
220
+ row_sizer.add( op2_placeholder, 1, Wx::ADJUST_MINSIZE)
221
+
222
+ @objects.push( type_op2 )
223
+
224
+
225
+ # switch inputs
226
+ evt_combobox(type_op1.get_id) do | e |
227
+ function = type_op1.get_string_selection()
228
+ if function == "IS CODED BY" and not type_op2.shown?
229
+ type_op2_a.hide()
230
+ op2_placeholder.remove(type_op2_a)
231
+
232
+ op2_placeholder.add(type_op2, 1,
233
+ Wx::GROW|Wx::ADJUST_MINSIZE|Wx::ALL)
234
+ type_op2.show()
235
+ op2_placeholder.layout()
236
+ @objects[row_n] = type_op2
237
+ elsif function == "CONTAINS WORD" and not type_op2_a.shown?
238
+ type_op2.hide()
239
+ op2_placeholder.remove(type_op2)
240
+ op2_placeholder.add(type_op2_a, 1,
241
+ Wx:: GROW|Wx::ADJUST_MINSIZE|Wx::ALL)
242
+ type_op2_a.show()
243
+ op2_placeholder.layout()
244
+ @objects[row_n] = type_op2_a
245
+ end
246
+ end
247
+
248
+ type_op3 = Wx::ComboBox.new(@crit_panel, -1, '', Wx::DEFAULT_POSITION,
249
+ Wx::DEFAULT_SIZE, [],
250
+ Wx::CB_READONLY|Wx::CB_DROPDOWN)
251
+ type_op3.append('')
252
+ type_op3.append('AND')
253
+ type_op3.append('AND NOT')
254
+ type_op3.append('OR')
255
+ type_op3.selection = 0
256
+
257
+ row_sizer.add( type_op3, 1, Wx::GROW|Wx::ALL, 2)
258
+ @ops.push( type_op3 )
259
+ unless with_bool
260
+ type_op3.hide()
261
+ end
262
+ @crit_sizer.add( row_sizer, 0, Wx::GROW|Wx::ALL|Wx::ALIGN_TOP )
263
+ end
264
+ end
265
+ end
@@ -0,0 +1,304 @@
1
+ module QDA::GUI
2
+ # a text control that is able to relate the selection back to the
3
+ # source text - addresses issues with the cross-platform
4
+ # representation of newlines in multiline text controls.
5
+ class TrueSelectionTextCtrl < Wx::TextCtrl
6
+ # because of the varying x-platform representation of newlines,
7
+ # we need a variable correction factor.
8
+ if Wx::RUBY_PLATFORM == 'WXMSW'
9
+ NEWLINE_CORRECTION_FACTOR = 1
10
+ else
11
+ NEWLINE_CORRECTION_FACTOR = 0
12
+ end
13
+
14
+ # background colour doesn't seem to work in MSW
15
+ HEADER_STYLE = Wx::TextAttr.new(Wx::RED)
16
+ # SUBHEADER_STYLE = Wx::TextAttr.new(Wx::BLUE)
17
+ NORMAL_STYLE = Wx::TextAttr.new(Wx::BLACK)
18
+ HIGHLIGHTED_STYLE = Wx::TextAttr.new(Wx::BLUE)
19
+
20
+ def initialize(*args)
21
+ super
22
+ @highlights = []
23
+ end
24
+
25
+ # returns the start and end of the selection as indexes within
26
+ # the underlying string
27
+ def true_selection()
28
+ lines = get_range(0, get_insertion_point()).count("\n")
29
+ lines *= NEWLINE_CORRECTION_FACTOR
30
+ [ get_insertion_point() - lines,
31
+ get_insertion_point() + get_string_selection().length - lines ]
32
+ end
33
+ alias :get_true_selection :true_selection
34
+
35
+ # returns the current insertion point as an index within the
36
+ # underlying string
37
+ def true_insertion_point()
38
+ lines = get_range(0, get_insertion_point()).count("\n")
39
+ lines *= NEWLINE_CORRECTION_FACTOR
40
+ get_insertion_point() - lines
41
+ end
42
+ alias :get_true_insertion_point :true_insertion_point
43
+
44
+ # given an index within the underlying string, returns the
45
+ # corresponding position within the TextCtrl
46
+ def true_index_to_pos(index)
47
+ str = get_value()[0, index]
48
+ adjustment = str.count("\n") * NEWLINE_CORRECTION_FACTOR
49
+ index + adjustment
50
+ end
51
+
52
+ # remove all highlights
53
+ def unhighlight()
54
+ while extent = @highlights.shift()
55
+ from, to = *extent
56
+ true_from, true_to = true_index_to_pos(from), true_index_to_pos(to)
57
+ set_style( true_from, true_to, NORMAL_STYLE )
58
+ end
59
+ end
60
+
61
+ # highlight the characters from +from+ to +to+. These are
62
+ # characters within the underlying text - this method will
63
+ # automatically translate those to real characters displayed within
64
+ # the text control.
65
+ def highlight(from, to)
66
+ true_from, true_to = true_index_to_pos(from), true_index_to_pos(to)
67
+ set_style( true_from, true_to, HIGHLIGHTED_STYLE )
68
+ @highlights.push( [from, to] )
69
+ end
70
+
71
+ def clear()
72
+ super()
73
+ @highlights = []
74
+ end
75
+
76
+ # call this with a block that alters the appearance or content of
77
+ # the text control - this includes call to set_style(), highlight()
78
+ # as well as changing text content. It reduces flicker and keeps the
79
+ # viewable area positioned correctly.
80
+ def save_position()
81
+ freeze()
82
+ saved_pos = get_scroll_pos(Wx::VERTICAL)
83
+ # calculate how many pixels it moves when we nudge the scrollbar
84
+ # down a 'line'
85
+ scroll_lines(1)
86
+ pos_down = saved_pos - get_scroll_pos(Wx::VERTICAL)
87
+ # calculate how many pixels it moves when we nudge the scrollbar
88
+ # up a 'line'
89
+ scroll_lines(-1)
90
+ pos_up = saved_pos - get_scroll_pos(Wx::VERTICAL)
91
+
92
+ # how long is a 'line'
93
+ movement = 0 - pos_down + pos_up
94
+ yield
95
+ show_position(0)
96
+ if movement > 0
97
+ scroll_lines(saved_pos / movement)
98
+ end
99
+ thaw()
100
+ end
101
+ end
102
+
103
+ class DocTextViewer < TrueSelectionTextCtrl
104
+ attr_reader :docid
105
+ DOCTEXT_STYLE = Wx::TE_MULTILINE|Wx::TE_READONLY|
106
+ Wx::TE_RICH|Wx::TE_NOHIDESEL
107
+
108
+ def initialize(parent, docid)
109
+ super( parent, -1, '', Wx::DEFAULT_POSITION,
110
+ Wx::DEFAULT_SIZE,
111
+ DOCTEXT_STYLE)
112
+ @docid = docid
113
+ end
114
+
115
+ # in this case simply returns a single code, representing the
116
+ # extent of the document currently highlighted.
117
+ def selection_to_fragments()
118
+ start, finish = true_selection()
119
+ length = finish - start
120
+ return [] if length == 0
121
+ QDA::CodeSet[ QDA::Code.new(@docid, start, length) ]
122
+ end
123
+
124
+ def highlight_codingtable(codes)
125
+ highlight_codeset( codes[@docid] )
126
+ end
127
+
128
+ # highlight all the passages found
129
+ def highlight_codeset(codeset)
130
+ save_position do
131
+ unhighlight()
132
+ codeset.each do | area |
133
+ highlight( area.offset, area.end )
134
+ end
135
+ end
136
+ end
137
+ end
138
+
139
+ # a text display that is made up of text fragments from multiple
140
+ # documents
141
+ # It keeps track of which fragment is found at which point
142
+ class CompositeText < TrueSelectionTextCtrl
143
+ class TextTable < Hash
144
+ def initialize(*args)
145
+ @reverse_table = {}
146
+ super(*args)
147
+ end
148
+
149
+ # returns the object associated with the character point #{key}
150
+ # within the text table. Note that
151
+ def [] (key)
152
+ keys.sort.each { | k | return super(k) if key < k }
153
+ return nil
154
+ end
155
+
156
+ # returns the object associated with the character point #{key}
157
+ # within the text table. Note that
158
+ def []=(key, value)
159
+ super(key, value)
160
+ @reverse_table[ value.to_code() ] = key unless value.nil?
161
+ end
162
+
163
+ def fetch(key)
164
+ offset = 0
165
+ keys.sort.each do | k |
166
+ if key < k
167
+ return super(k), key - offset
168
+ end
169
+ offset = k
170
+ end
171
+ return nil
172
+ end
173
+
174
+ # given a range across the text +length+ characters long from
175
+ # +start+, returns an Array of Vectors describing it
176
+ def range_to_vectors(start, length)
177
+ offset = 0
178
+ results = QDA::CodeSet[]
179
+
180
+ # at each iteration, offset is the position that we're starting from
181
+ # k is the offset we're going to.
182
+ keys.sort.each do | k |
183
+ word = self[k]
184
+ next unless word
185
+ this_start = [ k, start ].max
186
+ this_end = [ k + word.length, start + length ].min
187
+ # they're not overlapping
188
+ next if this_start >= this_end
189
+ results.add( QDA::Code.new( word.docid,
190
+ word.offset - k + this_start,
191
+ this_end - this_start ) )
192
+ # break if k >= start + length
193
+ offset = k
194
+ end
195
+ return results
196
+ end
197
+
198
+ # Returns the notional offset of the point +point+ within the
199
+ # document +docid+, or nil if that point is not displayed. Note
200
+ # that this represents position within the notional underlying
201
+ # contents, not the actual display in text control. If you wish to
202
+ # use these to refer to text displayed within the text control,
203
+ # call true_index_to_pos() on the return value.
204
+ def translate(docid, point)
205
+ target = @reverse_table.keys.find do | frag |
206
+ frag.docid == docid && frag.contains?(point)
207
+ end
208
+ return @reverse_table[target] - target.length +
209
+ ( point - target.offset )
210
+ end
211
+ end
212
+
213
+ def initialize(parent, text, offset, app)
214
+ @saved_styles = Hash.new()
215
+ super(parent, -1, text, Wx::DEFAULT_POSITION, Wx::DEFAULT_SIZE,
216
+ Wx::TE_MULTILINE|Wx::TE_READONLY|Wx::TE_RICH|Wx::TE_NOHIDESEL)
217
+ @app = app
218
+ evt_enter_window do | e |
219
+ set_cursor Wx::Cursor.new(Wx::CURSOR_IBEAM)
220
+ end
221
+ set_font( @app.display_font )
222
+ end
223
+
224
+ # displays the hash table of fragments, keyed on document title as a set
225
+ # of results, ordered by case-insensitive document title order.
226
+ def populate(fragments)
227
+ self.clear()
228
+ @table = TextTable.new()
229
+ @cursor = 0
230
+ @fragments = fragments
231
+ # sort by document title
232
+ save_position() do
233
+ fragments.each_title do | doc_title, frags |
234
+ frags.each do | frag |
235
+ header = "#{doc_title} [#{frag.offset}-#{frag.end}]\n"
236
+ write_range(header, nil, HEADER_STYLE)
237
+ write_range(frag, frag)
238
+ write_range("\n\n", nil)
239
+ end
240
+ write_range("\n", nil)
241
+ end
242
+ end
243
+ evt_left_dclick() { | e | jump_to_fragment(e) }
244
+ end
245
+
246
+ # writes the text +text+ to the control using the sytle +style+,
247
+ # and associates the object +bound_value+ to that text range.
248
+ def write_range(text, bound_value = nil, style = NORMAL_STYLE)
249
+ set_default_style(style)
250
+ ins_start = get_last_position()
251
+ append_text(text)
252
+ ins_end = get_last_position
253
+
254
+ if style != NORMAL_STYLE
255
+ @saved_styles[style] ||= []
256
+ @saved_styles[style].push([ ins_start, ins_end ])
257
+ end
258
+ # @table[ get_last_position() ] = bound_value
259
+ @cursor += text.length
260
+ @table[ @cursor ] = bound_value
261
+ end
262
+ private :write_range
263
+
264
+ def jump_to_fragment(evt)
265
+ frag, offset = *@table.fetch(true_insertion_point)
266
+ # if we've clicked on a text bit
267
+ return unless frag
268
+ @app.on_document_open( frag.docid,
269
+ frag.offset + offset )
270
+
271
+ end
272
+
273
+ def selection_to_fragments()
274
+ cursor, finish = *true_selection()
275
+ length = finish - cursor
276
+ results = @table.range_to_vectors(cursor, length)
277
+ end
278
+
279
+ # highlights the coded vectors or fragments
280
+ def highlight_codingtable(codingtable)
281
+ return unless @fragments
282
+ areas = @fragments.to_codingtable.join(codingtable)
283
+ save_position do
284
+ unhighlight()
285
+ areas.each do | docid, codes |
286
+ codes.each do | code |
287
+ translated = @table.translate(docid, code.offset)
288
+ highlight(translated, translated + code.length)
289
+ end
290
+ end
291
+ end
292
+ end
293
+
294
+ # need to repaint headers and so on
295
+ def set_font(font)
296
+ super(font)
297
+ @saved_styles.each do | style, segments |
298
+ segments.each do | seg |
299
+ set_style(seg[0], seg[1], style)
300
+ end
301
+ end
302
+ end
303
+ end
304
+ end