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.
- data/lib/weft.rb +21 -0
- data/lib/weft/WEFT-VERSION-STRING.rb +1 -0
- data/lib/weft/application.rb +130 -0
- data/lib/weft/backend.rb +39 -0
- data/lib/weft/backend/marshal.rb +26 -0
- data/lib/weft/backend/mysql.rb +267 -0
- data/lib/weft/backend/n6.rb +366 -0
- data/lib/weft/backend/sqlite.rb +633 -0
- data/lib/weft/backend/sqlite/category_tree.rb +104 -0
- data/lib/weft/backend/sqlite/schema.rb +152 -0
- data/lib/weft/backend/sqlite/upgradeable.rb +55 -0
- data/lib/weft/category.rb +157 -0
- data/lib/weft/coding.rb +355 -0
- data/lib/weft/document.rb +118 -0
- data/lib/weft/filters.rb +243 -0
- data/lib/weft/wxgui.rb +687 -0
- data/lib/weft/wxgui/category.xpm +26 -0
- data/lib/weft/wxgui/dialogs.rb +128 -0
- data/lib/weft/wxgui/document.xpm +25 -0
- data/lib/weft/wxgui/error_handler.rb +52 -0
- data/lib/weft/wxgui/inspectors.rb +361 -0
- data/lib/weft/wxgui/inspectors/category.rb +165 -0
- data/lib/weft/wxgui/inspectors/codereview.rb +275 -0
- data/lib/weft/wxgui/inspectors/document.rb +139 -0
- data/lib/weft/wxgui/inspectors/imagedocument.rb +56 -0
- data/lib/weft/wxgui/inspectors/script.rb +35 -0
- data/lib/weft/wxgui/inspectors/search.rb +265 -0
- data/lib/weft/wxgui/inspectors/textcontrols.rb +304 -0
- data/lib/weft/wxgui/lang.rb +17 -0
- data/lib/weft/wxgui/lang/en.rb +45 -0
- data/lib/weft/wxgui/mondrian.xpm +44 -0
- data/lib/weft/wxgui/search.xpm +25 -0
- data/lib/weft/wxgui/sidebar.rb +498 -0
- data/lib/weft/wxgui/utilities.rb +148 -0
- data/lib/weft/wxgui/weft16.xpm +31 -0
- data/lib/weft/wxgui/workarea.rb +249 -0
- data/test/001-document.rb +196 -0
- data/test/002-category.rb +138 -0
- data/test/003-code.rb +370 -0
- data/test/004-application.rb +52 -0
- data/test/006-filters.rb +139 -0
- data/test/009a-backend_sqlite_basic.rb +280 -0
- data/test/009b-backend_sqlite_complex.rb +175 -0
- data/test/009c_backend_sqlite_bench.rb +81 -0
- data/test/010-backend_nudist.rb +5 -0
- data/test/all-tests.rb +1 -0
- data/test/manual-gui-script.txt +24 -0
- data/test/testdata/autocoding-test.txt +15 -0
- data/test/testdata/iso-8859-1.txt +5 -0
- data/test/testdata/sample_doc.txt +19 -0
- data/test/testdata/search_results.txt +1254 -0
- data/test/testdata/text1-dos-ascii.txt +2 -0
- data/test/testdata/text1-unix-utf8.txt +2 -0
- data/weft-qda.rb +28 -0
- 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
|