weft-qda 0.9.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- data/lib/weft.rb +21 -0
- data/lib/weft/WEFT-VERSION-STRING.rb +1 -0
- data/lib/weft/application.rb +130 -0
- data/lib/weft/backend.rb +39 -0
- data/lib/weft/backend/marshal.rb +26 -0
- data/lib/weft/backend/mysql.rb +267 -0
- data/lib/weft/backend/n6.rb +366 -0
- data/lib/weft/backend/sqlite.rb +633 -0
- data/lib/weft/backend/sqlite/category_tree.rb +104 -0
- data/lib/weft/backend/sqlite/schema.rb +152 -0
- data/lib/weft/backend/sqlite/upgradeable.rb +55 -0
- data/lib/weft/category.rb +157 -0
- data/lib/weft/coding.rb +355 -0
- data/lib/weft/document.rb +118 -0
- data/lib/weft/filters.rb +243 -0
- data/lib/weft/wxgui.rb +687 -0
- data/lib/weft/wxgui/category.xpm +26 -0
- data/lib/weft/wxgui/dialogs.rb +128 -0
- data/lib/weft/wxgui/document.xpm +25 -0
- data/lib/weft/wxgui/error_handler.rb +52 -0
- data/lib/weft/wxgui/inspectors.rb +361 -0
- data/lib/weft/wxgui/inspectors/category.rb +165 -0
- data/lib/weft/wxgui/inspectors/codereview.rb +275 -0
- data/lib/weft/wxgui/inspectors/document.rb +139 -0
- data/lib/weft/wxgui/inspectors/imagedocument.rb +56 -0
- data/lib/weft/wxgui/inspectors/script.rb +35 -0
- data/lib/weft/wxgui/inspectors/search.rb +265 -0
- data/lib/weft/wxgui/inspectors/textcontrols.rb +304 -0
- data/lib/weft/wxgui/lang.rb +17 -0
- data/lib/weft/wxgui/lang/en.rb +45 -0
- data/lib/weft/wxgui/mondrian.xpm +44 -0
- data/lib/weft/wxgui/search.xpm +25 -0
- data/lib/weft/wxgui/sidebar.rb +498 -0
- data/lib/weft/wxgui/utilities.rb +148 -0
- data/lib/weft/wxgui/weft16.xpm +31 -0
- data/lib/weft/wxgui/workarea.rb +249 -0
- data/test/001-document.rb +196 -0
- data/test/002-category.rb +138 -0
- data/test/003-code.rb +370 -0
- data/test/004-application.rb +52 -0
- data/test/006-filters.rb +139 -0
- data/test/009a-backend_sqlite_basic.rb +280 -0
- data/test/009b-backend_sqlite_complex.rb +175 -0
- data/test/009c_backend_sqlite_bench.rb +81 -0
- data/test/010-backend_nudist.rb +5 -0
- data/test/all-tests.rb +1 -0
- data/test/manual-gui-script.txt +24 -0
- data/test/testdata/autocoding-test.txt +15 -0
- data/test/testdata/iso-8859-1.txt +5 -0
- data/test/testdata/sample_doc.txt +19 -0
- data/test/testdata/search_results.txt +1254 -0
- data/test/testdata/text1-dos-ascii.txt +2 -0
- data/test/testdata/text1-unix-utf8.txt +2 -0
- data/weft-qda.rb +28 -0
- metadata +96 -0
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
|