weft-qda 0.9.6 → 0.9.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (86) hide show
  1. data/lib/weft.rb +16 -1
  2. data/lib/weft/WEFT-VERSION-STRING.rb +1 -1
  3. data/lib/weft/application.rb +17 -74
  4. data/lib/weft/backend.rb +6 -32
  5. data/lib/weft/backend/sqlite.rb +222 -164
  6. data/lib/weft/backend/sqlite/category_tree.rb +52 -48
  7. data/lib/weft/backend/sqlite/database.rb +57 -0
  8. data/lib/weft/backend/sqlite/upgradeable.rb +7 -0
  9. data/lib/weft/broadcaster.rb +90 -0
  10. data/lib/weft/category.rb +139 -47
  11. data/lib/weft/codereview.rb +160 -0
  12. data/lib/weft/coding.rb +74 -23
  13. data/lib/weft/document.rb +23 -10
  14. data/lib/weft/exceptions.rb +10 -0
  15. data/lib/weft/filters.rb +47 -224
  16. data/lib/weft/filters/indexers.rb +137 -0
  17. data/lib/weft/filters/input.rb +118 -0
  18. data/lib/weft/filters/output.rb +101 -0
  19. data/lib/weft/filters/templates.rb +80 -0
  20. data/lib/weft/filters/win32backtick.rb +246 -0
  21. data/lib/weft/query.rb +169 -0
  22. data/lib/weft/wxgui.rb +349 -294
  23. data/lib/weft/wxgui/constants.rb +43 -0
  24. data/lib/weft/wxgui/controls.rb +6 -0
  25. data/lib/weft/wxgui/controls/category_dropdown.rb +192 -0
  26. data/lib/weft/wxgui/controls/category_tree.rb +314 -0
  27. data/lib/weft/wxgui/controls/document_list.rb +97 -0
  28. data/lib/weft/wxgui/controls/multitype_control.rb +37 -0
  29. data/lib/weft/wxgui/{inspectors → controls}/textcontrols.rb +235 -64
  30. data/lib/weft/wxgui/dialogs.rb +144 -41
  31. data/lib/weft/wxgui/error_handler.rb +116 -36
  32. data/lib/weft/wxgui/exceptions.rb +7 -0
  33. data/lib/weft/wxgui/inspectors.rb +61 -208
  34. data/lib/weft/wxgui/inspectors/category.rb +19 -16
  35. data/lib/weft/wxgui/inspectors/codereview.rb +90 -132
  36. data/lib/weft/wxgui/inspectors/document.rb +12 -8
  37. data/lib/weft/wxgui/inspectors/imagedocument.rb +56 -56
  38. data/lib/weft/wxgui/inspectors/query.rb +284 -0
  39. data/lib/weft/wxgui/inspectors/script.rb +147 -23
  40. data/lib/weft/wxgui/lang/en.rb +69 -0
  41. data/lib/weft/wxgui/sidebar.rb +90 -432
  42. data/lib/weft/wxgui/utilities.rb +70 -91
  43. data/lib/weft/wxgui/workarea.rb +150 -43
  44. data/share/icons/category.ico +0 -0
  45. data/share/icons/category.xpm +109 -0
  46. data/share/icons/codereview.ico +0 -0
  47. data/share/icons/codereview.xpm +54 -0
  48. data/share/icons/d_and_c.xpm +126 -0
  49. data/share/icons/document.ico +0 -0
  50. data/share/icons/document.xpm +70 -0
  51. data/share/icons/project.ico +0 -0
  52. data/share/icons/query.ico +0 -0
  53. data/share/icons/query.xpm +56 -0
  54. data/{lib/weft/wxgui → share/icons}/search.xpm +0 -0
  55. data/share/icons/weft.ico +0 -0
  56. data/share/icons/weft.xpm +62 -0
  57. data/share/icons/weft16.ico +0 -0
  58. data/share/icons/weft32.ico +0 -0
  59. data/share/templates/category_plain.html +18 -0
  60. data/share/templates/codereview_plain.html +18 -0
  61. data/share/templates/document_plain.html +13 -0
  62. data/share/templates/document_plain.txt +7 -0
  63. data/test/001-document.rb +55 -36
  64. data/test/002-category.rb +81 -6
  65. data/test/003-code.rb +8 -4
  66. data/test/004-application.rb +13 -34
  67. data/test/005-query_review.rb +139 -0
  68. data/test/006-filters.rb +54 -42
  69. data/test/007-output_filters.rb +113 -0
  70. data/test/009a-backend_sqlite_basic.rb +95 -24
  71. data/test/009b-backend_sqlite_complex.rb +43 -62
  72. data/test/009c_backend_sqlite_bench.rb +5 -10
  73. data/test/053-doc_inspector.rb +46 -0
  74. data/test/055-query_window.rb +50 -0
  75. data/test/all-tests.rb +1 -0
  76. data/test/test-common.rb +19 -0
  77. data/test/testdata/empty.qdp +0 -0
  78. data/test/testdata/simple with space.pdf +0 -0
  79. data/test/testdata/simple.pdf +0 -0
  80. data/weft-qda.rb +40 -7
  81. metadata +74 -14
  82. data/lib/weft/wxgui/category.xpm +0 -26
  83. data/lib/weft/wxgui/document.xpm +0 -25
  84. data/lib/weft/wxgui/inspectors/search.rb +0 -265
  85. data/lib/weft/wxgui/mondrian.xpm +0 -44
  86. data/lib/weft/wxgui/weft16.xpm +0 -31
@@ -0,0 +1,169 @@
1
+ # A stub class modelling a boolean query expression
2
+ module QDA
3
+ class Query
4
+ attr_accessor :dbid, :root
5
+
6
+ def initialize( func = nil )
7
+ @root = func
8
+ end
9
+
10
+ def add_expression(expr, arg_2 = nil)
11
+ begin
12
+ expr_class = self.class.const_get( expr.upcase.gsub(/\s+/, '_') )
13
+ rescue NameError
14
+ raise ArgumentError.new("Unknown expression '#{expr}'")
15
+ end
16
+ @root = expr_class.new(@root, arg_2)
17
+ end
18
+
19
+ def empty?()
20
+ @root.nil?
21
+ end
22
+
23
+ def items(entry = @root)
24
+ entry.to_a.flatten
25
+ end
26
+
27
+ def traverse(entry = @root)
28
+ items(entry).each { | expr | yield(expr) }
29
+ end
30
+
31
+ # Yields a series of pairs of functions and expressions, in the evaluating
32
+ # left-to-right order of the query. Note that in the last iteration, the
33
+ # +expr+ will be nil.
34
+ #
35
+ # query.unroll { | func, expr | ... }
36
+ #
37
+ def unroll(entry = @root)
38
+ things = items(entry)
39
+ while func = things.shift
40
+ yield func, things.shift
41
+ end
42
+ end
43
+
44
+ def num_functions()
45
+ items.grep(Function).length
46
+ end
47
+
48
+ def num_expressions()
49
+ items.grep(LogicalExpression).length
50
+ end
51
+
52
+ def calculate()
53
+ @root.calculate()
54
+ end
55
+
56
+ def to_s()
57
+ @root.to_s()
58
+ end
59
+ class Function
60
+ def initialize(app = nil)
61
+ @app = app
62
+ end
63
+
64
+ def to_a
65
+ [ self ]
66
+ end
67
+
68
+ def calculated?()
69
+ true
70
+ end
71
+ end
72
+
73
+ class CodedByFunction < Function
74
+ attr_reader :identifier
75
+ def initialize(app, identifier)
76
+ super(app)
77
+ case identifier
78
+ when Category # a category specified directly
79
+ @identifier = identifier.path
80
+ when String # a path to a category
81
+ @identifier = identifier
82
+ when Fixnum # a category id
83
+ @identifier = @app.get_category(identifier, false).path
84
+ when NilClass
85
+ raise ArgumentError.new("Unspecified category for CODED BY expression")
86
+ else
87
+ raise ArgumentError.new("Invalid value '#{identifier}' for CODED BY function")
88
+ end
89
+ end
90
+
91
+ def to_s()
92
+ "( CODED BY(#{@identifier.inspect}) )"
93
+ end
94
+
95
+ def calculate()
96
+ cat = @app.get_category(@identifier, false)
97
+ @app.get_text_at_category( cat )
98
+ end
99
+ end
100
+
101
+ class WordSearchFunction < Function
102
+ attr_reader :word
103
+ def initialize(app, word, options = {})
104
+ super(app)
105
+ if word.empty?
106
+ raise ArgumentError.new('No word supplied for CONTAINS WORD function')
107
+ end
108
+ @word = word
109
+ @options = options
110
+ end
111
+
112
+ def to_s()
113
+ "( SEARCH(#{@word.inspect}) )"
114
+ end
115
+
116
+ def calculate()
117
+ @app.get_search_fragments(@word, @options)
118
+ end
119
+ end
120
+
121
+ class LogicalExpression
122
+ attr_reader :arg_1, :arg_2
123
+ private :arg_1, :arg_2
124
+ def initialize(arg_1, arg_2)
125
+ @arg_1, @arg_2 = arg_1, arg_2
126
+ end
127
+
128
+ def to_a()
129
+ [ @arg_1.to_a, self, @arg_2.to_a ]
130
+ end
131
+
132
+ def val_1()
133
+ @arg_1.respond_to?(:calculate) ? @arg_1.calculate : @arg_1
134
+ end
135
+
136
+ def val_2()
137
+ @arg_2.respond_to?(:calculate) ? @arg_2.calculate : @arg_2
138
+ end
139
+ end
140
+
141
+ class OR < LogicalExpression
142
+ def calculate()
143
+ val_1.dup.merge(val_2)
144
+ end
145
+
146
+ def to_s
147
+ "( #{arg_1} OR #{@arg_2} )"
148
+ end
149
+ end
150
+
151
+ class AND < LogicalExpression
152
+ def calculate()
153
+ val_1.dup.join(val_2)
154
+ end
155
+ def to_s
156
+ "( #{arg_1} AND #{arg_2} )"
157
+ end
158
+ end
159
+
160
+ class AND_NOT < LogicalExpression
161
+ def calculate()
162
+ val_1.dup.remove(val_2)
163
+ end
164
+ def to_s
165
+ "( #{arg_1} AND NOT #{arg_2} )"
166
+ end
167
+ end
168
+ end
169
+ end
@@ -1,30 +1,11 @@
1
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
2
 
3
+ require 'weft/wxgui/constants'
25
4
  require 'weft/wxgui/utilities'
5
+ require 'weft/wxgui/exceptions'
26
6
  require 'weft/wxgui/lang'
27
7
 
8
+ require 'weft/wxgui/controls'
28
9
  require 'weft/wxgui/dialogs'
29
10
  require 'weft/wxgui/inspectors'
30
11
  require 'weft/wxgui/sidebar'
@@ -34,42 +15,62 @@ require 'weft/wxgui/error_handler.rb'
34
15
 
35
16
  module QDA
36
17
  module GUI
37
- # save some typing
38
- DEF_POS = Wx::DEFAULT_POSITION
39
- DEF_SIZE = Wx::DEFAULT_SIZE
40
-
41
18
  # 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
-
19
+ DEFAULT_FONT = Wx::Font.new(10, Wx::DEFAULT, Wx::NORMAL, Wx::NORMAL)
20
+ class WeftClient < Wx::App
21
+ attr_reader :display_font, :workarea, :sidebar, :app
22
+ attr_reader :current_category, :current_document
23
+
48
24
  include Wx
49
-
50
- SUBSCRIBABLE_EVENTS = [ :document_added, :document_changed,
51
- :document_deleted, :category_added, :category_changed,
52
- :category_deleted, :focus_category ]
25
+ include Broadcaster
26
+ include Subscriber
27
+ SUBSCRIBABLE_EVENTS = QDA::Application::SUBSCRIBABLE_EVENTS +
28
+ [ :focus_category, :focus_document, :text_font_changed ]
53
29
 
54
30
  def on_init()
55
- @display_font = GLOBAL_FONT
31
+ self.app_name = 'WeftQDA'
32
+ # retrieve font
33
+ conf = Wx::ConfigBase.get()
34
+ conf.path = '/DisplayFont'
35
+ @display_font = Wx::Font.new( conf.read_int('size', 12),
36
+ conf.read_int('family', Wx::DEFAULT),
37
+ conf.read_int('style', Wx::NORMAL),
38
+ conf.read_int('weight', Wx::NORMAL),
39
+ false,
40
+ conf.read('face', '') )
56
41
  # see wxgui/lang.rb
57
42
  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')
43
+ @icon_dirs = []
44
+ add_icon_dir( File.join( WEFT_SHAREDIR, 'icons') )
45
+ add_icon_dir( File.join( File.dirname( __FILE__ ), 'wxgui') )
64
46
  create_workarea()
65
47
  create_menus()
66
48
  @workarea.show()
67
49
  end
68
-
50
+
51
+
52
+ def main_loop()
53
+ super()
54
+ rescue Exception => err
55
+ if REPORT_CRASHES and not err.kind_of? SystemExit
56
+ workarea.hide_all()
57
+ CrashReportDialog.display(self, err)
58
+ end
59
+ Kernel.raise(err)
60
+ end
61
+
62
+ def app=(the_app)
63
+ if the_app
64
+ subscribe(the_app, :all)
65
+ else
66
+ @app.end()
67
+ end
68
+ @app = the_app
69
+ end
70
+
69
71
  # forcibly closes all the windows - does not check for project
70
72
  # saved state - just ends everything
71
73
  def finish()
72
- @sidebar.close() if @sidebar
73
74
  @workarea.close()
74
75
  end
75
76
 
@@ -78,8 +79,8 @@ module QDA
78
79
  def create_workarea()
79
80
  @workarea = WorkArea.new(self)
80
81
  # TODO - nice windows icon
81
- @workarea.set_icon( fetch_icon("weft16.xpm") )
82
- @workarea.evt_close() { | e | on_close(e) }
82
+ @workarea.set_icon( fetch_icon("weft") )
83
+ @workarea.evt_close() { | e | on_quit(e) }
83
84
  end
84
85
 
85
86
  # create menus - also only created once, then manipulated as
@@ -102,21 +103,44 @@ module QDA
102
103
  @menu_file.add_item("Save Project &As", "Ctrl-Shift-S") do | e |
103
104
  on_saveas_project(e)
104
105
  end
105
- @menu_file.add_item("&Import N6 Project") do | e |
106
- on_import_n6(e)
106
+ @menu_file.add_item("&Revert", "Ctrl-Shift-R") do | e |
107
+ on_revert_project(e)
107
108
  end
108
109
  @menu_file.append_separator()
109
110
  @menu_file.add_item("&Exit", "Alt-F4") do | e |
110
- @workarea.close()
111
+ @workarea.close() # this will trigger on_quit
112
+ end
113
+
114
+
115
+ @menu_view = EasyMenu.new(@workarea)
116
+ if WINDOW_MODE == :MDI
117
+ @menu_view.add_item("&Documents and Categories", "",
118
+ Wx::ITEM_CHECK) { | e | on_toggle_dandc(e) }
111
119
  end
112
120
 
121
+ @menu_view.add_item("&Set Display Font") do | e |
122
+ on_set_display_font(e)
123
+ end
124
+
113
125
  @menu_project = EasyMenu.new(@workarea)
114
126
  @menu_project.add_item("&Import Document") do | e |
115
127
  on_import_document(e)
116
128
  end
117
- @menu_project.add_item("&Set Display Font") do | e |
118
- on_set_display_font(e)
129
+ @menu_project.add_item("Delete Document") do | e |
130
+ on_delete_document(e)
119
131
  end
132
+ @menu_project.append_separator()
133
+ @menu_project.add_item("&Add Category") do | e |
134
+ on_add_category()
135
+ end
136
+ @menu_project.add_item("Delete Category") do | e |
137
+ on_delete_category(e)
138
+ end
139
+ @menu_project.append_separator()
140
+ @menu_project.add_item("&Export") do | e |
141
+ on_export(e)
142
+ end
143
+
120
144
  # menu_project.append(MENU_IMPORT_DOCUMENT_CLIPBOARD ,
121
145
  # "&Import Document...\t", 'Import')
122
146
  # @workarea.evt_menu(MENU_IMPORT_DOCUMENT_CLIPBOARD) do | e |
@@ -139,9 +163,6 @@ module QDA
139
163
  @menu_script.add_item("&New") { | e | on_start_script(e) }
140
164
  end
141
165
 
142
- @menu_view = EasyMenu.new(@workarea)
143
- @menu_view.add_item("&Documents and Categories", "",
144
- Wx::ITEM_CHECK) { | e | on_toggle_dandc(e) }
145
166
 
146
167
  @menu_help = EasyMenu.new(@workarea)
147
168
  @menu_help.add_item("&Help") { | e | on_help(e) }
@@ -150,21 +171,14 @@ module QDA
150
171
 
151
172
  menu_bar = Wx::MenuBar.new()
152
173
  menu_bar.append(@menu_file, "&File")
174
+ menu_bar.append(@menu_view, "&View")
153
175
  menu_bar.append(@menu_project, "&Project")
154
176
  menu_bar.append(@menu_search, "&Search")
155
177
  menu_bar.append(@menu_script, "&Script") if ::WEFT_TESTING
156
- menu_bar.append(@menu_view, "&View")
157
178
  menu_bar.append(@menu_help, "&Help")
158
179
 
159
180
  @workarea.menu_bar = menu_bar
160
181
  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
182
 
169
183
  # opens the document identified by +doc_id+, and, if +offset+ is
170
184
  # supplied, jumps to that point in the document
@@ -175,95 +189,75 @@ module QDA
175
189
  win.jump_to(offset)
176
190
  end
177
191
 
192
+
193
+ def display_font=(font)
194
+ broadcast(:text_font_changed, font)
195
+ @display_font = font
196
+ # font.get_native_font_info_desc would be nicer, but
197
+ # set_native_font_info doesn't appear to work in WxRuby 0.6.0
198
+ conf = Wx::ConfigBase.get()
199
+ conf.path = '/DisplayFont'
200
+ conf.write("size", font.point_size)
201
+ conf.write("family", font.family)
202
+ conf.write("style", font.style)
203
+ conf.write("weight", font.weight)
204
+ conf.write("face", font.face_name)
205
+ end
206
+
178
207
  def on_set_display_font(e)
179
208
  font_data = Wx::FontData.new()
180
- font_data.initial_font = @display_font
209
+ font_data.initial_font = display_font
181
210
  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
211
+ return unless dialog.show_modal() == Wx::ID_OK
212
+
213
+ # actually find the newly selected font and communicate new choice
214
+ self.display_font = dialog.font_data.chosen_font()
197
215
  end
198
216
 
199
217
  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()
218
+ confirm = WarningDialog.display( Lang::REINDEX_DOCS_WARNING_TITLE,
219
+ Lang::REINDEX_DOCS_WARNING )
220
+ return true unless confirm == Wx::ID_YES
221
+
222
+ Wx::BusyCursor.busy do
223
+ begin
224
+ docs = @app.get_all_docs()
225
+ prog = StageProgressDialog.new(@workarea,
226
+ "Reindexing documents", '')
227
+
228
+ prog.retarget(docs.length + 2)
229
+ docs.each do | doc |
230
+ prog.step("Indexing document #{doc.title}")
231
+ doc = @app.get_doc(doc.dbid)
232
+ @app.drop_reverse_indexes(doc.dbid)
233
+ indexer = QDA::WordIndexer.new()
234
+ indexer.feed(doc)
235
+ @app.save_reverse_index(doc.dbid, indexer.words)
239
236
  end
240
- end
237
+ prog.finish()
238
+ rescue UserAbortedException => err
239
+ prog.finish() if prog
240
+ ErrorDialog.display("Reindex halted", err.to_s())
241
+ @workarea.cursor = Wx::NORMAL_CURSOR
242
+ end
241
243
  end
242
244
  end
243
245
 
244
- def change_display_font(font)
245
- @workarea.set_display_font(@display_font = font)
246
- end
247
-
248
246
  def on_help(e)
249
- MessageDialog.new(nil, Lang::HELP_HELP_MESSAGE, 'Help',
250
- OK|ICON_INFORMATION).show_modal()
251
-
247
+ InformationDialog.display('Help', Lang::HELP_HELP_MESSAGE)
252
248
  end
253
249
 
254
250
  def on_help_about(e)
255
- MessageDialog.new(nil, Lang::HELP_ABOUT_MESSAGE, 'About',
256
- OK|ICON_INFORMATION).show_modal()
251
+ InformationDialog.display('About', Lang::HELP_ABOUT_MESSAGE)
257
252
  end
258
253
 
259
254
  def on_category_open(cat)
255
+ if not cat.codes
256
+ cat = @app.get_category(cat.dbid)
257
+ end
260
258
  @workarea.launch_window(CategoryWindow, cat)
261
259
  end
262
260
 
263
- class Script
264
- attr_accessor :dbid
265
- end
266
-
267
261
  # run a new query
268
262
  def on_start_script(e)
269
263
  s_id = @app.get_preference('NextScript') || 1
@@ -279,99 +273,133 @@ module QDA
279
273
 
280
274
  # import a document
281
275
  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
276
+ file_dialog = ImportFileDialog.new(@workarea, QDA::Document)
277
+ return unless file_dialog.show_modal() == Wx::ID_OK
278
+ file_dialog.each_with_path do | fpath, fname |
279
+ Wx::BusyCursor.busy do
280
+ prog = StageProgressDialog.new(@workarea, "Importing document",
281
+ "Reading file #{fname}")
282
+ begin
283
+ doc = Filters.import_file(Document, fpath) do | doc, filter |
284
+ doc.title = fname.gsub(/\.\w+$/, '')
320
285
  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
-
286
+ prog.step('Saving document')
287
+ @app.save_document(doc, true)
288
+ prog.step('Indexing document')
289
+ indexer = QDA::WordIndexer.new()
290
+ indexer.feed(doc)
291
+ prog.step('Saving indexes')
292
+ wordcount = indexer.words.keys.length
293
+ if wordcount > 0
294
+ prog.retarget(wordcount)
295
+ @app.save_reverse_index(doc.dbid, indexer.words, prog)
343
296
  end
344
- broadcast(:document_added, doc)
297
+ prog.finish()
298
+ rescue UserAbortedException, IOError => err
299
+ prog.finish() if prog
300
+ app.delete_document(doc) if doc && doc.dbid
301
+ MessageDialog.new(nil, err.to_s(),
302
+ "Document not imported",
303
+ OK|ICON_ERROR).show_modal()
304
+ @workarea.cursor = Wx::Cursor.new(Wx::CURSOR_ARROW)
345
305
  end
346
306
  end
347
307
  end
348
308
  end
349
309
 
350
-
351
- # could potentially be saved in database
352
- class Query
353
- attr_accessor :dbid
310
+ def on_delete_document(e = nil)
311
+ return unless current_document
312
+ msg = Lang::DELETE_DOCUMENT_WARNING % current_document.title
313
+ confirm = SuppressibleConfirmDialog.display('DeleteDocument',
314
+ Lang::DELETE_DOCUMENT_TITLE,
315
+ msg )
316
+ return unless confirm == Wx::ID_YES
317
+ Wx::BusyCursor.busy do
318
+ @app.delete_document( current_document )
319
+ end
354
320
  end
321
+
322
+ def on_add_category()
323
+ dialog = Wx::TextEntryDialog.new(@workarea.d_and_c, "Add Category\n",
324
+ "Enter the new category name",
325
+ "", Wx::OK | Wx::CANCEL)
326
+ return unless dialog.show_modal() == Wx::ID_OK
327
+
328
+ # go ahead and make the new category
329
+ parent = self.current_category || @app.get_root_category('CATEGORIES')
330
+ name = dialog.get_value()
331
+ begin
332
+ cat = QDA::Category.new(name, parent )
333
+ @app.save_category(cat)
334
+ rescue QDA::BadNameError
335
+ ErrorDialog.display( Lang::BAD_CATEGORY_NAME_TITLE,
336
+ Lang::BAD_CATEGORY_NAME_WARNING )
337
+ return false
338
+ rescue QDA::NotUniqueNameError
339
+ ErrorDialog.display( Lang::DUPLICATE_CATEGORY_NAME_TITLE,
340
+ Lang::DUPLICATE_CATEGORY_NAME_WARNING )
341
+ return false
342
+
343
+ end
344
+ end
345
+
346
+ # deletes the currently active category
347
+ def on_delete_category(e = nil)
348
+ return false unless current_category
349
+ msg = Lang::DELETE_CATEGORY_WARNING % current_category.name
350
+ confirm = SuppressibleConfirmDialog.display( 'DeleteCategory',
351
+ Lang::DELETE_CATEGORY_TITLE,
352
+ msg )
353
+
354
+ return false unless confirm == Wx::ID_YES
355
+ Wx::BusyCursor.busy() do
356
+ @app.delete_category(current_category)
357
+ end
358
+ end
359
+
355
360
 
361
+ def on_export(e)
362
+ a_win = @workarea.active_child
363
+ return unless a_win.respond_to?(:object)
364
+ obj = a_win.object
365
+ file_dialog = ExportFileDialog.new(@workarea, obj)
366
+ return unless file_dialog.show_modal() == Wx::ID_OK
367
+ Wx::BusyCursor.busy do
368
+ filter = file_dialog.filter.new(app)
369
+ File.open(file_dialog.get_path, 'w') do | outfile |
370
+ filter.write(obj, outfile)
371
+ end
372
+ end
373
+ end
374
+
375
+ def on_print(e)
376
+ a_win = @workarea.active_child
377
+ return unless a_win.respond_to?(:object)
378
+ obj = a_win.object
379
+ htmlprint = Wx::HtmlEasyPrinting.new('Weft print', @workarea)
380
+ htmlprint.page_setup()
381
+ file_dialog = ExportFileDialog.new(@workarea, obj)
382
+ return unless file_dialog.show_modal() == Wx::ID_OK
383
+ Wx::BusyCursor.busy do
384
+ filter = file_dialog.filter.new(app)
385
+ File.open(file_dialog.get_path, 'w') do | outfile |
386
+ filter.write(obj, outfile)
387
+ end
388
+ end
389
+ end
356
390
  # run a new query
357
391
  def on_query(e)
358
- q_id = @app.get_preference('NextQuery') || 1
359
- @app.save_preference('NextQuery', q_id + 1)
392
+ q_id = app.get_preference('NextQuery') || 1
393
+ app.save_preference('NextQuery', q_id + 1)
360
394
  q = Query.new()
361
395
  q.dbid = q_id
362
396
  @workarea.launch_window(QueryWindow, q)
363
397
  end
364
398
 
365
-
366
- # could potentially be saved in database
367
- class CodeReview
368
- attr_accessor :dbid
369
- end
370
-
371
399
  # run a new query
372
400
  def on_review_coding(e)
373
401
  q_id = @app.get_preference('NextReview') || 1
374
- @app.save_preference('NextReview', q_id + 1)
402
+ app.save_preference('NextReview', q_id + 1)
375
403
  q = CodeReview.new()
376
404
  q.dbid = q_id
377
405
  @workarea.launch_window(CodeReviewWindow, q)
@@ -385,79 +413,94 @@ module QDA
385
413
  :case_sensitive => search.case_sensitive,
386
414
  :whole_word => search.whole_word }
387
415
 
388
- text_results = @app.get_search_fragments(search.term, options)
416
+ text_results = app.get_search_fragments(search.term, options)
389
417
  title = "'#{search.term}' (#{Lang::SEARCH_RESULTS}) "
390
418
 
391
419
  # create a category from the returned fragments
392
420
  search_parent = @app.get_root_category('SEARCHES')
393
421
  search_cat = Category.new( title, search_parent )
422
+ search_cat.codetable_init() # urgh
394
423
  text_results.each do | docid, fragments |
395
424
  fragments.each do | f |
396
425
  search_cat.code( f.docid, f.offset, f.length )
397
426
  end
398
427
  end
399
- @app.save_category(search_cat)
400
- broadcast(:category_added, search_cat)
428
+ app.save_category(search_cat, true)
401
429
  @workarea.launch_window(CategoryWindow, search_cat)
402
430
  end
403
431
  end
404
432
  end
405
433
 
406
- def on_close(event)
434
+ def on_quit(event)
407
435
  if on_close_project(event)
408
436
  # remember window position and size
409
437
  @workarea.remember_size()
410
438
  event.skip()
439
+ exit() # required to exit reliably on Linux
411
440
  else
412
441
  event.veto()
413
442
  end
414
443
  end
415
-
444
+
445
+ def on_revert_project(e)
446
+ result = WarningDialog.display( Lang::REVERT_WARNING_TITLE,
447
+ Lang::REVERT_WARNING_MESSAGE )
448
+ return unless result == Wx::ID_OK
449
+
450
+ the_file = @app.dbfile
451
+ unpopulate()
452
+ open_project(the_file)
453
+ end
454
+
416
455
  # returns true if a new project was started, false if not
417
456
  def on_open_project(e)
418
457
  return false unless on_close_project(e)
419
458
  file_dialog = FileDialog.new(@workarea, "Open Project",
420
459
  "", "", "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
460
+ return false unless file_dialog.show_modal() == Wx::ID_OK
461
+
462
+ open_project( file_dialog.get_path() )
437
463
  end
464
+
465
+ def open_project(pfile)
466
+ @workarea.set_cursor( Wx::BUSY_CURSOR )
467
+ self.app = QDA::Application.new()
468
+ app.extend( QDA::Backend::SQLite )
469
+ app.start(:dbfile => pfile)
470
+ populate()
471
+ rescue ArgumentError, NotImplementedError => err
472
+ ErrorDialog.display(Lang::OPEN_PROJECT_ERROR_TITLE, err.to_s)
473
+ self.app = nil
474
+ return false
475
+ ensure
476
+ @workarea.set_cursor( Wx::NORMAL_CURSOR )
477
+ end
438
478
 
439
- # receives notifications when the application is dirtied or cleaned
440
- def update(state)
441
- if state
442
- @menu_file.enable_item(:save_project)
479
+ def add_subscriber(subscriber, *events)
480
+ super
481
+ subscriber.evt_close do | e |
482
+ delete_subscriber(subscriber)
483
+ e.skip()
484
+ end
485
+ end
486
+
487
+ def notify(ev, content = nil)
488
+ if ev == :saved or ev == :started
489
+ @menu_file.disable_items(:save_project, :revert)
443
490
  else
444
- @menu_file.disable_item(:save_project)
491
+ @menu_file.enable_items(:save_project)
492
+ @menu_file.enable_items(:revert) if app.dbfile
445
493
  end
446
- broadcast(:savestate_changed, @app)
494
+ broadcast(ev, content)
447
495
  end
448
-
449
- # Loads up metadata - eg from CSV file
450
- def on_load_metadata(e)
451
-
452
- end
453
496
 
454
497
  def on_toggle_dandc(e)
455
- if @sidebar.shown?
456
- @menu_view.uncheck_item(:documents_and_categories)
457
- @sidebar.hide()
498
+ if @workarea.d_and_c_visible?
499
+ @workarea.hide_d_and_c()
500
+ @menu_view.uncheck_items(:documents_and_categories)
458
501
  else
459
- @menu_view.check_item(:documents_and_categories)
460
- @sidebar.show()
502
+ @workarea.show_d_and_c()
503
+ @menu_view.check_items(:documents_and_categories)
461
504
  end
462
505
  end
463
506
 
@@ -474,15 +517,14 @@ module QDA
474
517
  # is open, prompt the user to supply a filename if this is a new
475
518
  # project, otherwise just save the project.
476
519
  def on_save_project(e)
477
- return unless @app
478
- return on_saveas_project(e) unless @app.dbfile
520
+ return unless app
521
+ return on_saveas_project(e) unless app.dbfile
479
522
  save_project()
480
523
  end
481
524
 
482
525
  def save_project()
483
526
  @workarea.remember_layouts()
484
- Wx::BusyCursor.busy() { @app.save }
485
- broadcast(:savestate_changed, @app)
527
+ Wx::BusyCursor.busy() { app.save }
486
528
  end
487
529
 
488
530
  def on_saveas_project(e)
@@ -506,17 +548,19 @@ module QDA
506
548
  raise "Bad Status"
507
549
  end
508
550
  end
509
- Wx::BusyCursor.busy() { @app.save(new_file) }
510
- broadcast(:savestate_changed, @app)
551
+ Wx::BusyCursor.busy() { app.save(new_file) }
511
552
  end
512
553
  end
513
554
 
555
+
514
556
  # Starts a new project
515
557
  def on_new_project(e)
516
558
  return unless on_close_project(e)
517
- @app = QDA::Application.new_virgin(QDA::Backend::SQLite,
518
- { :dbfile => nil }, self)
519
- @app.set_up
559
+ # TODO - this process is too protracted
560
+ self.app = QDA::Application.new(QDA::Backend::SQLite)
561
+ app.start(:dbfile => nil)
562
+ app.install_clean()
563
+ app.set_up()
520
564
  populate()
521
565
  end
522
566
 
@@ -554,53 +598,74 @@ module QDA
554
598
  end
555
599
  end
556
600
 
601
+
602
+
557
603
  # refetch here - this is when it is transmitted to the child
558
604
  # windows, and may then be used for coding, so it should contain
559
605
  # a list of all the codes that apply to the vector
560
606
  def current_category=(category)
561
- category = @app.get_category( category.dbid )
562
- broadcast(:focus_category, category)
607
+ if category
608
+ category = @app.get_category( category.dbid )
609
+ broadcast(:focus_category, category)
610
+ end
611
+ @current_category = category
612
+ end
613
+
614
+ def current_document=(document)
615
+ if document
616
+ document = @app.get_document( document.dbid )
617
+ broadcast(:focus_document, document)
618
+ end
619
+ @current_document = document
563
620
  end
564
621
 
565
622
  # unloads the currently loaded Weft instance and tidies up the GUI
566
623
  def unpopulate()
567
- return unless @app
568
- # does this clean up searches as well?
624
+ return unless app
625
+
569
626
  @workarea.close_all()
570
- @sidebar.remember_size()
571
- @sidebar.close()
572
- @sidebar = nil
627
+
573
628
  menu_state_no_project()
574
- @app.end()
575
- @app = nil
576
- broadcast(:savestate_changed, nil)
629
+ self.app = nil
577
630
  reset_subscriptions()
578
- add_subscriber(@workarea, :savestate_changed)
631
+ @workarea.subscribe(self, :all)
579
632
  end
580
633
 
581
634
  # updates the state of the menu above the main workarea to its
582
635
  # initial state with no active project
583
636
  def menu_state_no_project()
584
637
  @menu_file.disable_items(:close_project, :save_project,
585
- :save_project_as)
586
- @menu_project.disable_items(:import_document, :set_display_font)
638
+ :save_project_as, :revert)
639
+ @menu_project.disable_items(:import_document, :delete_document,
640
+ :add_category, :delete_category,
641
+ :export)
587
642
  @menu_search.disable_items( :search, :query, :review_coding,
588
643
  :reindex_documents)
589
644
  @menu_script.disable_items(:new) if ::WEFT_TESTING
590
- @menu_view.disable_item(:documents_and_categories)
645
+ if WINDOW_MODE == :MDI
646
+ @menu_view.disable_item(:documents_and_categories, :set_display_font)
647
+ else
648
+ @menu_view.disable_item(:set_display_font)
649
+ end
591
650
  end
592
651
 
593
652
  # updates the state of the menu above the main workarea to
594
653
  # reflect the fact that a project has been opened.
595
654
  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)
655
+ # :revert and :save_project are enabled when a change has been made
656
+ @menu_file.enable_items(:close_project, :save_project_as)
657
+ @menu_project.enable_items( :import_document, :delete_document,
658
+ :add_category, :delete_category,
659
+ :export)
599
660
  @menu_search.enable_items( :search, :query, :review_coding,
600
661
  :reindex_documents)
601
662
  @menu_script.enable_items(:new) if ::WEFT_TESTING
602
- @menu_view.enable_item(:documents_and_categories)
603
- @menu_view.check_item(:documents_and_categories)
663
+ if WINDOW_MODE == :MDI
664
+ @menu_view.enable_item(:documents_and_categories, :set_display_font)
665
+ @menu_view.check_item(:documents_and_categories)
666
+ else
667
+ @menu_view.enable_item(:set_display_font)
668
+ end
604
669
  end
605
670
 
606
671
  # Tries to close the current project, checking for unsaved
@@ -639,15 +704,22 @@ module QDA
639
704
  end
640
705
  end
641
706
 
707
+
642
708
  # hunt through icon directories to find an icon +icon+ - returns
643
709
  # a Wx::Icon constructed from that file, or raise an exception.
710
+ # +icon+ should be the name of an icon file *without* the extension.
711
+ # This method will search only for icons appropriate to the current
712
+ # platform. Raises a RuntimeError if no icon is found.
644
713
  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 )
714
+ ICON_EXTS.each do | type |
715
+ maybe_icon_file = "%s.%s" % [ icon, type.extension ]
716
+ @icon_dirs.each do | dir |
717
+ if File.exist?( f = File.join(dir, maybe_icon_file) )
718
+ return Wx::Icon.new( f, type.constant )
719
+ end
649
720
  end
650
721
  end
722
+
651
723
  raise RuntimeError, "Could not find icon #{icon}"
652
724
  end
653
725
 
@@ -656,31 +728,14 @@ module QDA
656
728
  end
657
729
 
658
730
  def populate()
659
- if ! @app
731
+ unless app
660
732
  raise "Populate called without an @app set"
661
733
  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
-
734
+ @workarea.show_d_and_c()
679
735
  # restore windows
680
736
  @workarea.restore_layouts
681
737
 
682
738
  menu_state_open_project()
683
- broadcast(:savestate_changed, @app)
684
739
  end
685
740
  end
686
741
  end