weft-qda 0.9.6

Sign up to get free protection for your applications and to get access to all the features.
Files changed (55) hide show
  1. data/lib/weft.rb +21 -0
  2. data/lib/weft/WEFT-VERSION-STRING.rb +1 -0
  3. data/lib/weft/application.rb +130 -0
  4. data/lib/weft/backend.rb +39 -0
  5. data/lib/weft/backend/marshal.rb +26 -0
  6. data/lib/weft/backend/mysql.rb +267 -0
  7. data/lib/weft/backend/n6.rb +366 -0
  8. data/lib/weft/backend/sqlite.rb +633 -0
  9. data/lib/weft/backend/sqlite/category_tree.rb +104 -0
  10. data/lib/weft/backend/sqlite/schema.rb +152 -0
  11. data/lib/weft/backend/sqlite/upgradeable.rb +55 -0
  12. data/lib/weft/category.rb +157 -0
  13. data/lib/weft/coding.rb +355 -0
  14. data/lib/weft/document.rb +118 -0
  15. data/lib/weft/filters.rb +243 -0
  16. data/lib/weft/wxgui.rb +687 -0
  17. data/lib/weft/wxgui/category.xpm +26 -0
  18. data/lib/weft/wxgui/dialogs.rb +128 -0
  19. data/lib/weft/wxgui/document.xpm +25 -0
  20. data/lib/weft/wxgui/error_handler.rb +52 -0
  21. data/lib/weft/wxgui/inspectors.rb +361 -0
  22. data/lib/weft/wxgui/inspectors/category.rb +165 -0
  23. data/lib/weft/wxgui/inspectors/codereview.rb +275 -0
  24. data/lib/weft/wxgui/inspectors/document.rb +139 -0
  25. data/lib/weft/wxgui/inspectors/imagedocument.rb +56 -0
  26. data/lib/weft/wxgui/inspectors/script.rb +35 -0
  27. data/lib/weft/wxgui/inspectors/search.rb +265 -0
  28. data/lib/weft/wxgui/inspectors/textcontrols.rb +304 -0
  29. data/lib/weft/wxgui/lang.rb +17 -0
  30. data/lib/weft/wxgui/lang/en.rb +45 -0
  31. data/lib/weft/wxgui/mondrian.xpm +44 -0
  32. data/lib/weft/wxgui/search.xpm +25 -0
  33. data/lib/weft/wxgui/sidebar.rb +498 -0
  34. data/lib/weft/wxgui/utilities.rb +148 -0
  35. data/lib/weft/wxgui/weft16.xpm +31 -0
  36. data/lib/weft/wxgui/workarea.rb +249 -0
  37. data/test/001-document.rb +196 -0
  38. data/test/002-category.rb +138 -0
  39. data/test/003-code.rb +370 -0
  40. data/test/004-application.rb +52 -0
  41. data/test/006-filters.rb +139 -0
  42. data/test/009a-backend_sqlite_basic.rb +280 -0
  43. data/test/009b-backend_sqlite_complex.rb +175 -0
  44. data/test/009c_backend_sqlite_bench.rb +81 -0
  45. data/test/010-backend_nudist.rb +5 -0
  46. data/test/all-tests.rb +1 -0
  47. data/test/manual-gui-script.txt +24 -0
  48. data/test/testdata/autocoding-test.txt +15 -0
  49. data/test/testdata/iso-8859-1.txt +5 -0
  50. data/test/testdata/sample_doc.txt +19 -0
  51. data/test/testdata/search_results.txt +1254 -0
  52. data/test/testdata/text1-dos-ascii.txt +2 -0
  53. data/test/testdata/text1-unix-utf8.txt +2 -0
  54. data/weft-qda.rb +28 -0
  55. metadata +96 -0
@@ -0,0 +1,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