weft-qda 0.9.6

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