weft-qda 0.9.6
Sign up to get free protection for your applications and to get access to all the features.
- 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
|