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,243 @@
1
+ require 'weft/category'
2
+ require 'weft/coding'
3
+ require 'English'
4
+
5
+ module QDA
6
+ class InputFilter
7
+ attr_reader :cursor
8
+
9
+ def initialize()
10
+ @cursor = 0
11
+ @indexers = []
12
+ end
13
+
14
+ def add_indexer(indexer)
15
+ unless indexer.respond_to?(:feed)
16
+ raise "Document indexers should have a feed method"
17
+ end
18
+ @indexers.push(indexer)
19
+ end
20
+
21
+ # reads +file+ and creates a new document titled +doctitle+. +file+
22
+ # may be a String filename or an open stream.
23
+ # Under the hood, calls +read_content+ to extract the content. This
24
+ # method must be implemented in subclasses. Then +process_content+
25
+ # is called to create the documents text. This class does something
26
+ # reasonable with plain text, but structured text formats will want
27
+ # to subclass this method to process non-text information (for
28
+ # example, HTML or XML tags)
29
+ def read(file, doctitle)
30
+ @content = ''
31
+ case file
32
+ when IO
33
+ @content = file.read()
34
+ when QDA::Document
35
+ @content = file.text()
36
+ when String
37
+ @content = File.read(file)
38
+ end
39
+ process_content(doctitle)
40
+ end
41
+
42
+ def process_content(doctitle)
43
+ # signal to indexers we're about to start
44
+ @indexers.each { | indexer | indexer.prepare(@content) }
45
+ doc = QDA::Document.new(doctitle)
46
+ @content.each_line do | line |
47
+ doc.append(line.to_s.chomp)
48
+ # inform AutoCoders, reverse indexers and so on.
49
+ @indexers.each { | indexer | indexer.feed(line) }
50
+ end
51
+ @indexers.each { | indexer | indexer.terminate() }
52
+ doc.create
53
+ return doc
54
+ end
55
+ end
56
+
57
+ class TextFilter < InputFilter
58
+ EXTENSIONS = [ 'txt' ]
59
+ def read_content(file)
60
+ text = file.read()
61
+ file.close()
62
+ text
63
+ end
64
+ end
65
+
66
+ class PDFFilter < InputFilter
67
+ EXTENSIONS = [ 'pdf' ]
68
+ PDF_TO_TEXT_EXEC = 'pdftotext'
69
+ begin
70
+ out = `#{PDF_TO_TEXT_EXEC} -v 2>&1`
71
+ unless out =~ /pdftotext version 3/
72
+ warn 'PDFtotext Version 3 not found in path' +
73
+ 'PDF Filters will not be avaialabl'
74
+ end
75
+ end
76
+
77
+ NO_COPYING_ERROR_TEXT =
78
+ "The author or publisher of this PDF document has locked it to
79
+ prevent copying and extraction of its text. It is not possible to
80
+ import this document."
81
+ def read(file, doctitle)
82
+ case file
83
+ when IO
84
+ raise NotImplementedError
85
+ @content = `#{PDF_TO_TEXT_EXEC} -nopgbrk #{file.path} - 2>&1`
86
+ file.close()
87
+ when String
88
+ @content = `#{PDF_TO_TEXT_EXEC} -nopgbrk #{file} - 2>&1`
89
+ end
90
+
91
+ case $CHILD_STATUS
92
+ when 0
93
+ process_content(doctitle)
94
+ when 3
95
+ raise RuntimeError.new(NO_COPYING_ERROR_TEXT)
96
+ else
97
+ raise RuntimeError.new("Could not extract PDF text: #{text}")
98
+ end
99
+ end
100
+
101
+ end
102
+
103
+ class OutputFilter
104
+
105
+ end
106
+
107
+ # ...
108
+ class HTMLFilter < OutputFilter
109
+
110
+ end
111
+
112
+ class Indexer
113
+ attr_reader :cursor
114
+ def initialize()
115
+ @cursor = 0
116
+ end
117
+
118
+ def index(str)
119
+ prepare(str)
120
+ str.each_line { | line | feed(line) }
121
+ end
122
+
123
+ def terminate()
124
+ end
125
+
126
+ def prepare(content)
127
+ end
128
+
129
+ def feed(line)
130
+ @cursor += line.length
131
+ end
132
+ end
133
+
134
+ # An indexer which records the position of words for later reverse
135
+ # retrieval
136
+ class WordIndexer < Indexer
137
+ attr_reader :words
138
+ # includes accented latin-1 characters
139
+ WORD_TOKENIZER = /[\w\xC0-\xD6\xD8-\xF6\xF8-\xFF][\w\xC0-\xD6\xD8-\xF6\xF8-\xFF\']+/
140
+ def initialize()
141
+ super
142
+ @words = Hash.new { | h, k | h[k] = [] }
143
+ end
144
+
145
+ def feed(line)
146
+ line.scan( WORD_TOKENIZER ) do | word |
147
+ next if word.length == 1
148
+ @words[word].push(cursor + Regexp.last_match.begin(0))
149
+ end
150
+ super
151
+ end
152
+ end
153
+
154
+ # An indexer that uses text patterns to identify, for example,
155
+ # passages by a particular speaker, or text headings.
156
+ # The indexer can recognise a number of different types of codes,
157
+ # each denoted by a pattern of punctuation in a line of text. A
158
+ # default coder recognises the following
159
+ # A 'Heading', marked by a line **NAME OF HEADING**
160
+ # A 'Speaker', marked by a line SpeakerName:
161
+ #
162
+ # After the filter has run, the results of the coding can be
163
+ # retrieved by calling Autocoder#codes
164
+ # This is a hash of codetype names to inner hashes of codevalue names
165
+ # (strings) to QDA::Codesets corresponding to them.
166
+ class AutoCoder < Indexer
167
+ STANDARD_TRIGGER_RULES = {
168
+ /^(\w+)\:\s*$/ => 'Speaker',
169
+ /^\*\*(.*)\*\*$/ => 'Heading'
170
+ }
171
+
172
+ attr_reader :codes
173
+ # +rules+ should be a hash of string keys, naming types of autocode
174
+ # (e.g. "Speaker", "Heading", "Topic") mapped to values, which
175
+ # should be regular expressions specifying how the start of such a
176
+ # code should be recognised.
177
+ # For example, to find topics marked by the characters '##' at the
178
+ # start of the line:
179
+ # 'Heading' => /^##(.*)$/
180
+ def initialize(rules = STANDARD_TRIGGER_RULES)
181
+ super()
182
+ @trigger_rules = rules
183
+ @codes = Hash.new { | h, k | h[k] = {} }
184
+ @curr_codes = {}
185
+ end
186
+
187
+ # check a line of document content for triggers
188
+ def feed(line)
189
+ @trigger_rules.each do | rule, type |
190
+ if match = rule.match(line)
191
+ trigger(cursor, type, match[1])
192
+ end
193
+ end
194
+ super
195
+ end
196
+
197
+ # take action on finding a autocode marker
198
+ def trigger(cursor, group, codename)
199
+ # save any previous code that was being done for this group
200
+ store(group) if @curr_codes[group]
201
+ new_codeset = get_code(group, codename)
202
+ @curr_codes[group] = [ new_codeset, cursor ]
203
+ end
204
+ private :trigger
205
+
206
+ # returns the code name +codename+ within the group +group+,
207
+ # creating a new empty category
208
+ def get_code(group, codename)
209
+ return @codes[group][codename] if @codes[group][codename]
210
+ @codes[group][codename] = QDA::CodeSet.new()
211
+ end
212
+
213
+ # Returns the names and codesets for autocodes in group +group+
214
+ # in a series of pairs
215
+ def each_autocode(group)
216
+ @codes[group].each { | name, codeset | yield name, codeset }
217
+ end
218
+
219
+ # alters all the stored coding in this autocoder so that it refers
220
+ # to the document identified by +docid+
221
+ def apply(docid)
222
+ @codes.values.each do | group |
223
+ group.values.each do | codeset |
224
+ codeset.map! { | x | x.docid = docid; x }
225
+ end
226
+ end
227
+ end
228
+
229
+ # finish up all currently active coding in this autocoder
230
+ def terminate()
231
+ @curr_codes.each_key { | group | store(group) }
232
+ end
233
+
234
+ # finish the coding for the current code being used among +group+
235
+ def store(group)
236
+ codeset, start = @curr_codes[group]
237
+ # -1 here is a placeholder
238
+ terminus = cursor - start
239
+ codeset.add( Code.new(-1, start, terminus) )
240
+ end
241
+ private :store
242
+ end
243
+ end
data/lib/weft/wxgui.rb ADDED
@@ -0,0 +1,687 @@
1
+ require 'wxruby'
2
+ require 'stringio'
3
+
4
+ # temporary hack to turn c++ style set_foo(), get_foo() and is_foo()
5
+ # accessors into ruby style foo=() foo() and foo?()
6
+ wx_classes = Wx::constants.collect { | c | Wx::const_get(c) }.grep(Class)
7
+ wx_classes.each do | klass |
8
+ klass.instance_methods.grep(/^([gs]et|evt|is)_/).each do | meth |
9
+ case meth
10
+ when /^get_(\w+)$/
11
+ klass.class_eval("alias :#{$1} :#{meth}")
12
+ when /^set_(\w+)$/
13
+ klass.class_eval("alias :#{$1}= :#{meth}")
14
+ when /^is_(\w+)$/
15
+ klass.class_eval("alias :#{$1}? :#{meth}")
16
+ end
17
+ end
18
+ end
19
+
20
+ # one global variable holds a reference to the single instance of the
21
+ # Wx::App class. This allows widgets deep within the GUI to receive
22
+ # notifications from the top level of the GUI.
23
+ $wxapp = nil
24
+
25
+ require 'weft/wxgui/utilities'
26
+ require 'weft/wxgui/lang'
27
+
28
+ require 'weft/wxgui/dialogs'
29
+ require 'weft/wxgui/inspectors'
30
+ require 'weft/wxgui/sidebar'
31
+ require 'weft/wxgui/workarea'
32
+ require 'weft/wxgui/error_handler.rb'
33
+
34
+
35
+ module QDA
36
+ module GUI
37
+ # save some typing
38
+ DEF_POS = Wx::DEFAULT_POSITION
39
+ DEF_SIZE = Wx::DEFAULT_SIZE
40
+
41
+ # the default display font for text
42
+ GLOBAL_FONT = Wx::Font.new(10, Wx::DEFAULT, Wx::NORMAL, Wx::NORMAL,
43
+ false, 'Tahoma')
44
+ class Instance < Wx::App
45
+ attr_reader :display_font, :workarea, :sidebar
46
+ attr_accessor :app
47
+
48
+ include Wx
49
+
50
+ SUBSCRIBABLE_EVENTS = [ :document_added, :document_changed,
51
+ :document_deleted, :category_added, :category_changed,
52
+ :category_deleted, :focus_category ]
53
+
54
+ def on_init()
55
+ @display_font = GLOBAL_FONT
56
+ # see wxgui/lang.rb
57
+ Lang::set_language('En')
58
+
59
+ # see wxgui/utilities.rb
60
+ self.extend(Broadcaster)
61
+
62
+ # check if running under rubyscript2exe?
63
+ @icon_dirs = $:.dup << File.join( File.dirname( __FILE__ ), 'wxgui')
64
+ create_workarea()
65
+ create_menus()
66
+ @workarea.show()
67
+ end
68
+
69
+ # forcibly closes all the windows - does not check for project
70
+ # saved state - just ends everything
71
+ def finish()
72
+ @sidebar.close() if @sidebar
73
+ @workarea.close()
74
+ end
75
+
76
+ # create the workarea - would normally only be called once in a
77
+ # whole execution
78
+ def create_workarea()
79
+ @workarea = WorkArea.new(self)
80
+ # TODO - nice windows icon
81
+ @workarea.set_icon( fetch_icon("weft16.xpm") )
82
+ @workarea.evt_close() { | e | on_close(e) }
83
+ end
84
+
85
+ # create menus - also only created once, then manipulated as
86
+ # projects open and close.
87
+ def create_menus()
88
+ @menu_file = EasyMenu.new(@workarea)
89
+ @menu_file.add_item("&New Project", "Ctrl-N") do | e |
90
+ on_new_project(e)
91
+ end
92
+ @menu_file.add_item("&Open Project", "Ctrl-O") do | e |
93
+ on_open_project(e)
94
+ end
95
+ @menu_file.add_item("&Close Project") do | e |
96
+ on_close_project(e)
97
+ end
98
+ @menu_file.append_separator()
99
+ @menu_file.add_item("&Save Project", "Ctrl-S") do | e |
100
+ on_save_project(e)
101
+ end
102
+ @menu_file.add_item("Save Project &As", "Ctrl-Shift-S") do | e |
103
+ on_saveas_project(e)
104
+ end
105
+ @menu_file.add_item("&Import N6 Project") do | e |
106
+ on_import_n6(e)
107
+ end
108
+ @menu_file.append_separator()
109
+ @menu_file.add_item("&Exit", "Alt-F4") do | e |
110
+ @workarea.close()
111
+ end
112
+
113
+ @menu_project = EasyMenu.new(@workarea)
114
+ @menu_project.add_item("&Import Document") do | e |
115
+ on_import_document(e)
116
+ end
117
+ @menu_project.add_item("&Set Display Font") do | e |
118
+ on_set_display_font(e)
119
+ end
120
+ # menu_project.append(MENU_IMPORT_DOCUMENT_CLIPBOARD ,
121
+ # "&Import Document...\t", 'Import')
122
+ # @workarea.evt_menu(MENU_IMPORT_DOCUMENT_CLIPBOARD) do | e |
123
+ # on_paste_import_document(e)
124
+ # end
125
+
126
+ @menu_search = EasyMenu.new(@workarea)
127
+ @menu_search.add_item("&Search") { | e | on_search(e) }
128
+
129
+ @menu_search.add_item("&Query") { | e | on_query(e) }
130
+ @menu_search.add_item("&Review Coding") { | e | on_review_coding(e) }
131
+ @menu_search.append_separator()
132
+ @menu_search.add_item("Reindex &Documents") do | e |
133
+ on_search_reindex(e)
134
+ end
135
+
136
+ # hide script in release builds
137
+ if ::WEFT_TESTING
138
+ @menu_script = EasyMenu.new(@workarea)
139
+ @menu_script.add_item("&New") { | e | on_start_script(e) }
140
+ end
141
+
142
+ @menu_view = EasyMenu.new(@workarea)
143
+ @menu_view.add_item("&Documents and Categories", "",
144
+ Wx::ITEM_CHECK) { | e | on_toggle_dandc(e) }
145
+
146
+ @menu_help = EasyMenu.new(@workarea)
147
+ @menu_help.add_item("&Help") { | e | on_help(e) }
148
+ @menu_help.add_item("&About") { | e | on_help_about(e) }
149
+ menu_state_no_project()
150
+
151
+ menu_bar = Wx::MenuBar.new()
152
+ menu_bar.append(@menu_file, "&File")
153
+ menu_bar.append(@menu_project, "&Project")
154
+ menu_bar.append(@menu_search, "&Search")
155
+ menu_bar.append(@menu_script, "&Script") if ::WEFT_TESTING
156
+ menu_bar.append(@menu_view, "&View")
157
+ menu_bar.append(@menu_help, "&Help")
158
+
159
+ @workarea.menu_bar = menu_bar
160
+ end
161
+
162
+ def current_category
163
+ if @sidebar.tree_list.get_current_category()
164
+ return @app.get_category( @sidebar.tree_list.get_current_category().dbid )
165
+ end
166
+ return nil
167
+ end
168
+
169
+ # opens the document identified by +doc_id+, and, if +offset+ is
170
+ # supplied, jumps to that point in the document
171
+ def on_document_open(doc, offset = 0)
172
+ dbid = doc.is_a?(Document) ? doc.dbid : doc
173
+ doc = @app.get_document(dbid)
174
+ win = @workarea.launch_window(DocumentWindow, doc)
175
+ win.jump_to(offset)
176
+ end
177
+
178
+ def on_set_display_font(e)
179
+ font_data = Wx::FontData.new()
180
+ font_data.initial_font = @display_font
181
+ dialog = Wx::FontDialog.new(@workarea, font_data)
182
+ case dialog.show_modal()
183
+ when Wx::ID_OK
184
+ font = dialog.get_font_data.get_chosen_font()
185
+ change_display_font(font)
186
+ # font.get_native_font_info_desc would be nicer, but
187
+ # set_native_font_info doesn't appear to work in WxRuby 0.5.0
188
+ @app.save_preference('DisplayFont',
189
+ :size => font.point_size,
190
+ :family => font.family,
191
+ :style => font.style,
192
+ :weight => font.weight,
193
+ :face => font.face_name )
194
+ when Wx::ID_CANCEL
195
+ return
196
+ end
197
+ end
198
+
199
+ def on_search_reindex(e)
200
+ confirm = MessageDialog.new( nil,
201
+ Lang::REINDEX_DOCS_WARNING,
202
+ Lang::REINDEX_DOCS_WARNING_TITLE,
203
+ NO_DEFAULT|YES|NO|ICON_EXCLAMATION)
204
+ case confirm.show_modal()
205
+ when ID_NO
206
+ return true # skip
207
+ when ID_YES
208
+ end
209
+ @app.get_all_docs.each do | doc |
210
+ doc = @app.get_doc(doc.dbid)
211
+ @app.drop_reverse_indexes(doc.dbid)
212
+
213
+ Wx::BusyCursor.busy do
214
+ filter = QDA::TextFilter.new()
215
+ indexer = QDA::WordIndexer.new()
216
+ filter.add_indexer(indexer)
217
+ # filter.add_indexer(DocumentImportProgressTracker.new(@workarea))
218
+
219
+ begin
220
+ new_doc = filter.read(doc, doc.doctitle)
221
+ rescue Exception => err
222
+ MessageDialog.new(nil, err.to_s + caller.to_s,
223
+ "Cannot reindex document",
224
+ OK|ICON_ERROR).show_modal()
225
+ return()
226
+ end
227
+
228
+ begin
229
+ wordcount = indexer.words.keys.length
230
+ prog = WordIndexSaveProgressTracker.new( wordcount, @workarea,
231
+ doc.doctitle )
232
+ @app.save_reverse_index(doc.dbid, indexer.words, prog)
233
+ rescue Exception => err
234
+ MessageDialog.new(nil, err.to_s,
235
+ "Cannot reindex document",
236
+ OK|ICON_ERROR).show_modal()
237
+ ensure
238
+ prog.progbar.close()
239
+ end
240
+ end
241
+ end
242
+ end
243
+
244
+ def change_display_font(font)
245
+ @workarea.set_display_font(@display_font = font)
246
+ end
247
+
248
+ def on_help(e)
249
+ MessageDialog.new(nil, Lang::HELP_HELP_MESSAGE, 'Help',
250
+ OK|ICON_INFORMATION).show_modal()
251
+
252
+ end
253
+
254
+ def on_help_about(e)
255
+ MessageDialog.new(nil, Lang::HELP_ABOUT_MESSAGE, 'About',
256
+ OK|ICON_INFORMATION).show_modal()
257
+ end
258
+
259
+ def on_category_open(cat)
260
+ @workarea.launch_window(CategoryWindow, cat)
261
+ end
262
+
263
+ class Script
264
+ attr_accessor :dbid
265
+ end
266
+
267
+ # run a new query
268
+ def on_start_script(e)
269
+ s_id = @app.get_preference('NextScript') || 1
270
+ @app.save_preference('NextScript', s_id + 1)
271
+ s = Script.new()
272
+ s.dbid = s_id
273
+ @workarea.launch_window(ScriptWindow, s)
274
+ end
275
+
276
+ def on_import_document(e)
277
+ import_document()
278
+ end
279
+
280
+ # import a document
281
+ def import_document()
282
+ wildcard = "Text files (*.txt)|*.txt|PDF files (*.pdf)|*.pdf"
283
+ file_dialog = Wx::FileDialog.new(@workarea, "Import a Document From File",
284
+ "", "", wildcard, Wx::MULTIPLE )
285
+ if file_dialog.show_modal() == Wx::ID_OK
286
+ file_dialog.get_paths.each_with_index do | fpath, i |
287
+ # Should this be determined from the file dialogue's
288
+ # current type selection, rather than file extension, which
289
+ # is unreliable (eg lots of plain text files don't end with
290
+ # .txt, esp on non-Windows platforms.
291
+ ext = fpath[-3, 3].downcase
292
+ if ext == 'txt'
293
+ filter = QDA::TextFilter.new()
294
+ elsif ext == 'pdf'
295
+ filter = QDA::PDFFilter.new()
296
+ else
297
+ # as a last shot, try importing the file as plaing text
298
+ filter = QDA::TextFilter.new()
299
+ end
300
+
301
+ Wx::BusyCursor.busy do
302
+ doc_title = file_dialog.get_filenames[i].gsub(/\.\w+$/, '')
303
+ indexer = QDA::WordIndexer.new()
304
+ filter.add_indexer(indexer)
305
+ tracker = DocumentImportProgressTracker.new( @workarea,
306
+ doc_title )
307
+ filter.add_indexer(tracker)
308
+
309
+ # try and import it
310
+ begin
311
+ doc = filter.read(fpath, doc_title)
312
+ rescue Exception => err
313
+ p err
314
+ tracker.terminate()
315
+ MessageDialog.new(nil, err.to_s,
316
+ "Cannot import document",
317
+ OK|ICON_ERROR).show_modal()
318
+ @workarea.cursor = Wx::Cursor.new(Wx::CURSOR_ARROW)
319
+ ensure
320
+ end
321
+
322
+ # try and save it
323
+ begin
324
+ @app.save_document(doc)
325
+
326
+ wordcount = indexer.words.keys.length
327
+ if wordcount > 0
328
+ prog = WordIndexSaveProgressTracker.new( wordcount,
329
+ @workarea,
330
+ doc.title )
331
+ @app.save_reverse_index(doc.dbid, indexer.words, prog)
332
+ end
333
+ rescue Exception => err
334
+ prog.terminate() if prog
335
+ @app.delete_document(doc.dbid) if doc.dbid
336
+ MessageDialog.new(nil, err.to_s,
337
+ "Cannot import document",
338
+ OK|ICON_ERROR).show_modal()
339
+ @workarea.cursor = Wx::Cursor.new(Wx::CURSOR_ARROW)
340
+ return
341
+ ensure
342
+
343
+ end
344
+ broadcast(:document_added, doc)
345
+ end
346
+ end
347
+ end
348
+ end
349
+
350
+
351
+ # could potentially be saved in database
352
+ class Query
353
+ attr_accessor :dbid
354
+ end
355
+
356
+ # run a new query
357
+ def on_query(e)
358
+ q_id = @app.get_preference('NextQuery') || 1
359
+ @app.save_preference('NextQuery', q_id + 1)
360
+ q = Query.new()
361
+ q.dbid = q_id
362
+ @workarea.launch_window(QueryWindow, q)
363
+ end
364
+
365
+
366
+ # could potentially be saved in database
367
+ class CodeReview
368
+ attr_accessor :dbid
369
+ end
370
+
371
+ # run a new query
372
+ def on_review_coding(e)
373
+ q_id = @app.get_preference('NextReview') || 1
374
+ @app.save_preference('NextReview', q_id + 1)
375
+ q = CodeReview.new()
376
+ q.dbid = q_id
377
+ @workarea.launch_window(CodeReviewWindow, q)
378
+ end
379
+
380
+ def on_search(e)
381
+ search = SearchDialog.new(@workarea)
382
+ if search.show_modal() == Wx::ID_OK
383
+ Wx::BusyCursor.busy do
384
+ options = { :wrap_both => search.expand,
385
+ :case_sensitive => search.case_sensitive,
386
+ :whole_word => search.whole_word }
387
+
388
+ text_results = @app.get_search_fragments(search.term, options)
389
+ title = "'#{search.term}' (#{Lang::SEARCH_RESULTS}) "
390
+
391
+ # create a category from the returned fragments
392
+ search_parent = @app.get_root_category('SEARCHES')
393
+ search_cat = Category.new( title, search_parent )
394
+ text_results.each do | docid, fragments |
395
+ fragments.each do | f |
396
+ search_cat.code( f.docid, f.offset, f.length )
397
+ end
398
+ end
399
+ @app.save_category(search_cat)
400
+ broadcast(:category_added, search_cat)
401
+ @workarea.launch_window(CategoryWindow, search_cat)
402
+ end
403
+ end
404
+ end
405
+
406
+ def on_close(event)
407
+ if on_close_project(event)
408
+ # remember window position and size
409
+ @workarea.remember_size()
410
+ event.skip()
411
+ else
412
+ event.veto()
413
+ end
414
+ end
415
+
416
+ # returns true if a new project was started, false if not
417
+ def on_open_project(e)
418
+ return false unless on_close_project(e)
419
+ file_dialog = FileDialog.new(@workarea, "Open Project",
420
+ "", "", "Project Files (*.qdp)|*.qdp" )
421
+ if file_dialog.show_modal() == Wx::ID_OK
422
+ begin
423
+ Wx::BusyCursor.busy() do
424
+ @app = QDA::Application.new(self)
425
+ @app.extend( QDA::Backend::SQLite )
426
+ @app.start(:dbfile => file_dialog.get_path())
427
+ end
428
+ rescue Exception => err
429
+ MessageDialog.new(nil, err.to_s,
430
+ "Cannot open project",
431
+ OK|ICON_ERROR).show_modal()
432
+ @app = nil
433
+ return()
434
+ end
435
+ populate()
436
+ end
437
+ end
438
+
439
+ # receives notifications when the application is dirtied or cleaned
440
+ def update(state)
441
+ if state
442
+ @menu_file.enable_item(:save_project)
443
+ else
444
+ @menu_file.disable_item(:save_project)
445
+ end
446
+ broadcast(:savestate_changed, @app)
447
+ end
448
+
449
+ # Loads up metadata - eg from CSV file
450
+ def on_load_metadata(e)
451
+
452
+ end
453
+
454
+ def on_toggle_dandc(e)
455
+ if @sidebar.shown?
456
+ @menu_view.uncheck_item(:documents_and_categories)
457
+ @sidebar.hide()
458
+ else
459
+ @menu_view.check_item(:documents_and_categories)
460
+ @sidebar.show()
461
+ end
462
+ end
463
+
464
+ # should resize the toolbar window in proportion to the main
465
+ # window - seems to cause it to go corrupted looking though
466
+ def on_resize(e)
467
+ # if @tools
468
+ # @tools.set_client_size( proportional_size(0.3, 0.995) )
469
+ # end
470
+
471
+ end
472
+
473
+ # Handles requests to save the project. Do nothing if no project
474
+ # is open, prompt the user to supply a filename if this is a new
475
+ # project, otherwise just save the project.
476
+ def on_save_project(e)
477
+ return unless @app
478
+ return on_saveas_project(e) unless @app.dbfile
479
+ save_project()
480
+ end
481
+
482
+ def save_project()
483
+ @workarea.remember_layouts()
484
+ Wx::BusyCursor.busy() { @app.save }
485
+ broadcast(:savestate_changed, @app)
486
+ end
487
+
488
+ def on_saveas_project(e)
489
+ wildcard = "QDA Project Files (*.qdp)|*.qdp"
490
+ file_dialog = Wx::FileDialog.new(@workarea, "Save Project As",
491
+ "", "", wildcard, Wx::SAVE)
492
+
493
+ if file_dialog.show_modal() == Wx::ID_OK
494
+ new_file = file_dialog.get_path()
495
+
496
+ if FileTest.exists?(new_file)
497
+ confirm = MessageDialog.new(nil, Lang::FILE_ALREADY_EXISTS,
498
+ "File already exists",
499
+ NO_DEFAULT|OK|CANCEL|ICON_EXCLAMATION)
500
+ case confirm.show_modal()
501
+ when ID_OK
502
+ # ok
503
+ when ID_CANCEL
504
+ return()
505
+ else
506
+ raise "Bad Status"
507
+ end
508
+ end
509
+ Wx::BusyCursor.busy() { @app.save(new_file) }
510
+ broadcast(:savestate_changed, @app)
511
+ end
512
+ end
513
+
514
+ # Starts a new project
515
+ def on_new_project(e)
516
+ return unless on_close_project(e)
517
+ @app = QDA::Application.new_virgin(QDA::Backend::SQLite,
518
+ { :dbfile => nil }, self)
519
+ @app.set_up
520
+ populate()
521
+ end
522
+
523
+ def on_import_n6(e)
524
+ wildcard = "Project Files (*.stp)|*.stp"
525
+ file_dialog = Wx::FileDialog.new(@workarea, "Import N6 Project", "",
526
+ "", wildcard )
527
+ if file_dialog.show_modal() == Wx::ID_OK
528
+ Wx::BusyCursor.busy do
529
+ src = QDA::Application.new()
530
+ src.extend(QDA::Backend::N6)
531
+ src.start( :basedir => file_dialog.get_directory() )
532
+ on_new_project(nil)
533
+
534
+ @app.batch() do
535
+ src.get_all_docs.each do | d |
536
+ d.dbid = nil
537
+ @app._save_document(d)
538
+ broadcast(:document_added, d)
539
+ end
540
+
541
+ save_down = Proc.new do | cat |
542
+ if cat.parent.nil?
543
+ cat.name = "IMPORTED"
544
+ end
545
+ @app._save_category(cat)
546
+ broadcast(:category_added, cat)
547
+
548
+ cat.children.each { | c | save_down.call(c) }
549
+ end
550
+
551
+ save_down.call(src.get_all_categories)
552
+ end
553
+ end
554
+ end
555
+ end
556
+
557
+ # refetch here - this is when it is transmitted to the child
558
+ # windows, and may then be used for coding, so it should contain
559
+ # a list of all the codes that apply to the vector
560
+ def current_category=(category)
561
+ category = @app.get_category( category.dbid )
562
+ broadcast(:focus_category, category)
563
+ end
564
+
565
+ # unloads the currently loaded Weft instance and tidies up the GUI
566
+ def unpopulate()
567
+ return unless @app
568
+ # does this clean up searches as well?
569
+ @workarea.close_all()
570
+ @sidebar.remember_size()
571
+ @sidebar.close()
572
+ @sidebar = nil
573
+ menu_state_no_project()
574
+ @app.end()
575
+ @app = nil
576
+ broadcast(:savestate_changed, nil)
577
+ reset_subscriptions()
578
+ add_subscriber(@workarea, :savestate_changed)
579
+ end
580
+
581
+ # updates the state of the menu above the main workarea to its
582
+ # initial state with no active project
583
+ def menu_state_no_project()
584
+ @menu_file.disable_items(:close_project, :save_project,
585
+ :save_project_as)
586
+ @menu_project.disable_items(:import_document, :set_display_font)
587
+ @menu_search.disable_items( :search, :query, :review_coding,
588
+ :reindex_documents)
589
+ @menu_script.disable_items(:new) if ::WEFT_TESTING
590
+ @menu_view.disable_item(:documents_and_categories)
591
+ end
592
+
593
+ # updates the state of the menu above the main workarea to
594
+ # reflect the fact that a project has been opened.
595
+ def menu_state_open_project()
596
+ @menu_file.enable_items(:close_project, :save_project,
597
+ :save_project_as)
598
+ @menu_project.enable_items(:import_document, :set_display_font)
599
+ @menu_search.enable_items( :search, :query, :review_coding,
600
+ :reindex_documents)
601
+ @menu_script.enable_items(:new) if ::WEFT_TESTING
602
+ @menu_view.enable_item(:documents_and_categories)
603
+ @menu_view.check_item(:documents_and_categories)
604
+ end
605
+
606
+ # Tries to close the current project, checking for unsaved
607
+ # changes. Returns true if the currently open project was closed,
608
+ # or false if it was not. If there was no currently open project,
609
+ # always returns true.
610
+ def on_close_project(e)
611
+ return true unless @app
612
+ if confirm_unsaved_changes()
613
+ unpopulate()
614
+ return true
615
+ else
616
+ return false
617
+ end
618
+ end
619
+
620
+ def confirm_unsaved_changes()
621
+ unless @app and @app.dirty?
622
+ # there's no open project or there's no unsaved changes
623
+ return true
624
+ end
625
+
626
+ style = NO_DEFAULT|YES|NO|CANCEL|ICON_EXCLAMATION
627
+ confirm = MessageDialog.new( nil,
628
+ Lang::UNSAVED_CHANGES_CONFIRM,
629
+ Lang::UNSAVED_CHANGES_CONFIRM_TITLE,
630
+ style )
631
+ case confirm.show_modal()
632
+ when ID_YES
633
+ @app.dbfile ? save_project() : on_saveas_project(nil)
634
+ return true
635
+ when ID_CANCEL
636
+ return false
637
+ when ID_NO
638
+ return true
639
+ end
640
+ end
641
+
642
+ # hunt through icon directories to find an icon +icon+ - returns
643
+ # a Wx::Icon constructed from that file, or raise an exception.
644
+ def fetch_icon(icon)
645
+ @icon_dirs.each do | dir |
646
+ f = File.join(dir, icon)
647
+ if File.exist?( f )
648
+ return Wx::Icon.new( f )
649
+ end
650
+ end
651
+ raise RuntimeError, "Could not find icon #{icon}"
652
+ end
653
+
654
+ def add_icon_dir(dir)
655
+ @icon_dirs.push(dir)
656
+ end
657
+
658
+ def populate()
659
+ if ! @app
660
+ raise "Populate called without an @app set"
661
+ end
662
+ if font_pref = @app.get_preference('DisplayFont')
663
+ # @display_font.set_native_font_info(font_pref) - not impl
664
+ change_display_font(Wx::Font.new( font_pref[:size],
665
+ font_pref[:family],
666
+ font_pref[:style],
667
+ font_pref[:weight],
668
+ false,
669
+ font_pref[:face] ) )
670
+ end
671
+
672
+ frame = @workarea
673
+ @workarea.app = @app
674
+ # see sidebar.rb
675
+ @sidebar = SideBar.new( @workarea, self )
676
+ @sidebar.set_icon( fetch_icon("weft16.xpm") )
677
+ @sidebar.show()
678
+
679
+ # restore windows
680
+ @workarea.restore_layouts
681
+
682
+ menu_state_open_project()
683
+ broadcast(:savestate_changed, @app)
684
+ end
685
+ end
686
+ end
687
+ end