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
data/lib/weft/filters.rb
ADDED
@@ -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
|