weft-qda 0.9.6 → 0.9.8

Sign up to get free protection for your applications and to get access to all the features.
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