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,284 @@
1
+ module QDA::GUI
2
+ # a window that allows the entry of criteria (category
3
+ # intersections and unions, text search, document attributes), and
4
+ # interactively presents the text meeting the criteria, and to save
5
+ # the results as a permanent category (memo describing the rules
6
+ # used to produce the codes.
7
+ class QueryWindow < InspectorWindow
8
+ include QDA::Subscriber
9
+ attr_reader :query, :rows, :funcs, :args, :exprs
10
+ private :rows, :funcs, :args, :exprs
11
+
12
+ # obj is a simple object with a +dbid+ representing a project
13
+ # unique identifier for this query
14
+ def initialize(obj, client, workarea, layout)
15
+ @client = client
16
+
17
+
18
+ @rows, @funcs, @args, @exprs = [], [], [], []
19
+
20
+ super(workarea, "Query #{obj.dbid}", layout) do | parent |
21
+ @text_box = CompositeText.new(parent, '', 0, @client)
22
+ end
23
+ set_icon( @client.fetch_icon("query") )
24
+ construct_query_panel
25
+ # go to the query constructor panel, if query not defined, else show
26
+ # results pane
27
+ self.query = obj
28
+ if query.empty?
29
+ @notebook.selection = 1
30
+ else
31
+ if results = get_results(query)
32
+ populate(results)
33
+ end
34
+ end
35
+ end
36
+
37
+ def obj
38
+ @query
39
+ end
40
+
41
+
42
+
43
+ def query=(the_query)
44
+ @query = the_query
45
+ i = 0
46
+ query.unroll do | func, expr |
47
+ show_row(i)
48
+ case func
49
+ when QDA::Query::CodedByFunction
50
+ funcs[i].string_selection = 'CODED BY'
51
+ args[i].show( 0 )
52
+ cat = @client.app.get_category( func.identifier )
53
+ args[i].visible_item.set_active_category( cat )
54
+ when QDA::Query::WordSearchFunction
55
+ funcs[i].string_selection = 'CONTAINS WORD'
56
+ args[i].show( 1 )
57
+ args[i].visible_item.value = func.word
58
+ end
59
+
60
+
61
+ case expr
62
+ when QDA::Query::AND
63
+ exprs[i].string_selection = 'AND'
64
+ when QDA::Query::OR
65
+ exprs[i].string_selection = 'OR'
66
+ when QDA::Query::AND_NOT
67
+ exprs[i].string_selection = 'AND NOT'
68
+ end
69
+ i += 1
70
+ end
71
+ end
72
+
73
+ def associated_subscribers()
74
+ super << @args.grep(CategoryDropDown)
75
+ end
76
+
77
+ # runs a query using the currently selected query options
78
+ def get_results(query)
79
+ # remember the last query we did
80
+ @last_query = query
81
+ set_cursor( Wx::BUSY_CURSOR )
82
+ @last_results = @last_query.calculate()
83
+ set_cursor( Wx::NORMAL_CURSOR )
84
+ return @last_results
85
+ end
86
+
87
+ def get_target_argument(offset, as_string = false)
88
+ return nil unless @objects[offset].is_shown
89
+
90
+ end
91
+
92
+ def get_function(i)
93
+ arg_ctrl = args[i].visible_item
94
+ case funcs[i].string_selection
95
+ when 'IS CODED BY'
96
+ return QDA::Query::CodedByFunction.new(@client.app,
97
+ arg_ctrl.current_category)
98
+ when 'CONTAINS WORD'
99
+ return QDA::Query::WordSearchFunction.new(@client.app,
100
+ arg_ctrl.get_value(),
101
+ :wrap_both => 200 )
102
+ end
103
+ end
104
+
105
+ def get_query()
106
+ q = QDA::Query.new( get_function(0) )
107
+ 1.upto(rows.length - 1) do | i |
108
+ expr = exprs[i - 1].string_selection
109
+ break if expr.empty?
110
+ q.add_expression(expr, get_function(i))
111
+ end
112
+ return q
113
+ rescue ArgumentError => err
114
+ ErrorDialog.display("Error in query", err.to_s)
115
+ return
116
+ end
117
+
118
+ # returns a stringified representation of the query. The only
119
+ # reason this is here at the moment is because the underlying
120
+ # query code parser doesn't accept string literal paths or names
121
+ # for categories- only database ids. This should be remedied, so
122
+ # the string passed to query and this are the same.
123
+ def get_query_string(join_char = " ")
124
+ get_query.to_s
125
+ end
126
+
127
+ # run the query, display the results, view them
128
+ def on_view(e)
129
+ return unless query = get_query()
130
+ return unless results = get_results( query )
131
+
132
+ populate(results)
133
+ @notebook.selection = 0
134
+ end
135
+
136
+ def populate(results)
137
+ @text_box.populate( results )
138
+ if curr_category = @drop_down.current_category()
139
+ @text_box.highlight_codingtable(curr_category.codes)
140
+ end
141
+ end
142
+
143
+ def on_save_results(e)
144
+ # check for saved results
145
+ Wx::BusyCursor.busy do
146
+ results = get_results( get_query )
147
+ return unless results
148
+ @text_box.populate(results)
149
+ end
150
+
151
+ q_string = get_query_string
152
+ title = "'Query #{obj.dbid} results'"
153
+ search_parent = @client.app.get_root_category('SEARCHES')
154
+ title = search_parent.unique_name(title)
155
+ query_cat = QDA::Category.new( title, search_parent )
156
+ query_cat.codetable = @last_results
157
+ query_cat.memo = get_query_string("\n")
158
+
159
+ @client.app.save_category(query_cat, true)
160
+ end
161
+
162
+ def on_focus_dropdown(e)
163
+ # p "focussed #{e.event_object}"
164
+ # e.skip()
165
+ end
166
+
167
+ def on_blur_dropdown(e)
168
+ # p "blurred #{e.event_object}"
169
+ # p active?
170
+ # e.skip()
171
+ end
172
+
173
+
174
+ def show_row(idx)
175
+ if rows[idx].nil?
176
+ add_row()
177
+ else
178
+ @crit_sizer.show( rows[idx], true )
179
+ end
180
+
181
+ # show the next row below if this one ends with a valid expression
182
+ if not exprs[idx].get_string_selection.empty?
183
+ show_row(idx + 1)
184
+ end
185
+ @crit_sizer.layout()
186
+ end
187
+
188
+ def hide_row(idx)
189
+ return if rows[idx].nil?
190
+ @crit_sizer.show( rows[idx], false )
191
+ @crit_sizer.layout()
192
+ hide_row(idx + 1)
193
+ end
194
+
195
+ # handle changing the /AND/OR/AND NOT/ selector at the end of each row,
196
+ # hiding or showing rows below as appropriate
197
+ def on_alter_expression(expr_sel, row_index)
198
+ # Previously used to receive parameters as (event, row_index)
199
+ # .. but ..
200
+ # This seems to cause regular segfaults - wxruby 0.6.0
201
+ # expr_sel = event.event_object()
202
+ expr = expr_sel.string_selection()
203
+ if expr.empty?
204
+ hide_row(row_index + 1)
205
+ else
206
+ show_row(row_index + 1)
207
+ end
208
+ end
209
+
210
+ def construct_query_panel()
211
+ panel2 = Wx::Panel.new(@notebook, -1)
212
+ sizer2 = Wx::BoxSizer.new(Wx::VERTICAL)
213
+ sizer2.add( Wx::StaticText.new(panel2, -1, 'Find text that:'),
214
+ 0, Wx::ALL|Wx::ADJUST_MINSIZE, 4 )
215
+
216
+ @crit_panel = Wx::Panel.new(panel2, -1)
217
+ @crit_sizer = Wx::BoxSizer.new(Wx::VERTICAL)
218
+
219
+ add_row()
220
+ @crit_panel.set_sizer(@crit_sizer)
221
+ sizer2.add(@crit_panel, 1, Wx::GROW|Wx::ALL|Wx::ADJUST_MINSIZE, 4 )
222
+
223
+ butt_panel = Wx::Panel.new(panel2, -1)
224
+ bott_sizer = Wx::BoxSizer.new(Wx::HORIZONTAL)
225
+
226
+ button = Wx::Button.new(butt_panel, -1, 'View')
227
+ button.evt_button(button.get_id) { | e | on_view(e) }
228
+ bott_sizer.add(button, 1, Wx::ALL, 4)
229
+
230
+ button = Wx::Button.new(butt_panel, -1, 'Save Results')
231
+ button.evt_button(button.get_id) { | e | on_save_results(e) }
232
+ bott_sizer.add(button, 1, Wx::ALL, 4)
233
+
234
+ butt_panel.set_sizer(bott_sizer)
235
+ sizer2.add(butt_panel, 0,
236
+ Wx::GROW|Wx::ADJUST_MINSIZE|Wx::ALIGN_BOTTOM)
237
+ @notebook.add_page(panel2, 'query')
238
+ panel2.set_sizer_and_fit(sizer2)
239
+ end
240
+
241
+ def add_row()
242
+ row_sizer = Wx::BoxSizer.new(Wx::HORIZONTAL)
243
+ row_index = @rows.length
244
+
245
+ func_sel = Wx::Choice.new( @crit_panel, -1)
246
+ func_sel.append('IS CODED BY')
247
+ func_sel.append('CONTAINS WORD')
248
+ func_sel.selection = 0
249
+ row_sizer.add( func_sel, 2, Wx::GROW|Wx::ALL, 2)
250
+ @funcs.push( func_sel )
251
+
252
+ cat_dd = CategoryDropDown.new(@client, @crit_panel, nil, true)
253
+ arg_sel = MultiTypedControl.new(cat_dd)
254
+ arg_sel.add_control( Wx::TextCtrl.new(@crit_panel, -1, '') )
255
+ # TODO - allow currently highlighted category dropdown to
256
+ # receive global focus_category events
257
+ # type_op2.evt_set_focus() { | e | on_focus_dropdown(e) }
258
+ # type_op2.evt_kill_focus() { | e | on_blur_dropdown(e) }
259
+ row_sizer.add( arg_sel, 2, Wx::GROW|Wx::ADJUST_MINSIZE|Wx::ALL, 2)
260
+ @args.push( arg_sel )
261
+
262
+
263
+ # switch inputs
264
+ evt_choice(func_sel.get_id) { arg_sel.show( func_sel.selection() ) }
265
+
266
+ expr_sel = Wx::Choice.new(@crit_panel, -1)
267
+ expr_sel.append('')
268
+ expr_sel.append('OR')
269
+ expr_sel.append('AND')
270
+ expr_sel.append('AND NOT')
271
+ expr_sel.selection = 0
272
+ # Previously passed the event, but see on_alter_expression
273
+ # evt_choice(expr_sel.get_id) { | e | on_alter_expression(e, row_index) }
274
+ evt_choice(expr_sel.get_id) { on_alter_expression(expr_sel, row_index) }
275
+
276
+ row_sizer.add( expr_sel, 1, Wx::GROW|Wx::ALL, 2)
277
+ @exprs.push( expr_sel )
278
+ @crit_sizer.add( row_sizer, 0,
279
+ Wx::GROW|Wx::ALL|Wx::ALIGN_TOP, 1 )
280
+
281
+ @rows.push(row_sizer)
282
+ end
283
+ end
284
+ end
@@ -1,35 +1,159 @@
1
+ require 'stringio'
2
+
1
3
  module QDA::GUI
4
+ # stub class - to be moved to core weft lib?
5
+ class Script
6
+ attr_accessor :dbid
7
+ end
8
+
9
+ class CodeTextCtrl < Wx::TextCtrl
10
+ def initialize(parent, code = '')
11
+ super( parent, -1, code, DEF_POS, DEF_SIZE,
12
+ Wx::TE_MULTILINE|Wx::TE_RICH|Wx::TE_PROCESS_TAB )
13
+ set_default_style( normal_style )
14
+ evt_set_focus() { reset() }
15
+ end
16
+
17
+ def reset
18
+ set_style(0, get_last_position, normal_style)
19
+ end
20
+
21
+ def normal_font
22
+ @normal_font ||= Wx::Font.new( 10, Wx::MODERN, Wx::NORMAL, Wx::NORMAL )
23
+ end
24
+ def normal_style
25
+ @normal_style ||= Wx::TextAttr.new(Wx::BLACK, Wx::WHITE, normal_font)
26
+ end
27
+ def bold_font
28
+ @bold_font ||= Wx::Font.new( 10, Wx::MODERN, Wx::NORMAL, Wx::BOLD )
29
+ end
30
+
31
+ def bold_style
32
+ @bold_style ||= Wx::TextAttr.new(Wx::RED, Wx::WHITE, bold_font)
33
+ end
34
+
35
+ def highlight_line(lineno)
36
+ line_start = xy_to_position(0, lineno)
37
+ line_end = line_start + get_line_length(lineno)
38
+ set_style(line_start, line_end, bold_style)
39
+ end
40
+ end
41
+
2
42
  class ScriptWindow < WorkAreaWindow
3
- def initialize(obj, app, workarea, layout)
43
+ attr_reader :code, :output, :obj
44
+ private :code, :output
45
+
46
+ def initialize(obj, client, workarea, layout)
4
47
  super(workarea, "Script #{obj.dbid}", nil)
5
- @app = app
6
- code = ''
7
- sizer = Wx::BoxSizer.new(Wx::VERTICAL)
8
- @code = Wx::TextCtrl.new(self, -1, code, Wx::DEFAULT_POSITION,
9
- Wx::DEFAULT_SIZE, Wx::TE_MULTILINE)
10
- sizer.add(@code, 2, Wx::GROW|Wx::ALL, 5)
11
-
12
- button = Wx::Button.new(self, -1, 'Run')
13
- button.evt_button(button.get_id) { | e | on_run(e) }
14
- sizer.add(button, 0, Wx::ALL, 5)
15
-
16
- @output = Wx::TextCtrl.new(self, -1, '', Wx::DEFAULT_POSITION,
17
- Wx::DEFAULT_SIZE,
18
- Wx::TE_MULTILINE|Wx::TE_READONLY)
19
- sizer.add(@output, 2, Wx::GROW|Wx::ALL, 5)
20
- self.sizer = sizer
48
+ @client = client
49
+ @obj = obj
50
+ self.construct()
21
51
  end
52
+
53
+
54
+ def construct()
55
+ @splitter = Wx::SplitterWindow.new(self, -1,
56
+ Wx::DEFAULT_POSITION,
57
+ Wx::DEFAULT_SIZE,Wx::SP_3DSASH)
58
+ @splitter.minimum_pane_size = 75
59
+
60
+ # the input pane and buttons
61
+ panel_code = Wx::Panel.new(@splitter)
62
+ code_panel_sizer = Wx::BoxSizer.new(Wx::VERTICAL)
63
+
64
+ code_sizer = LabelledVSizer.new(panel_code, 'Script')
65
+
66
+ @code = CodeTextCtrl.new(panel_code)
67
+ code_sizer.add(@code, 1, Wx::GROW|Wx::ALL, 4)
68
+
69
+ b_panel = Wx::Panel.new(panel_code)
70
+ b_sizer = Wx::BoxSizer.new(Wx::HORIZONTAL)
71
+ @button_run = Wx::Button.new(b_panel, -1, 'Run')
72
+ @button_run.evt_button(@button_run.get_id) { | e | on_run(e) }
73
+ b_sizer.add(@button_run, 0, Wx::ALL, 4)
74
+
75
+ @button_load = Wx::Button.new(b_panel, -1, 'Load from file...')
76
+ @button_load.evt_button(@button_load.get_id) { | e | on_load_script(e) }
77
+ b_sizer.add(@button_load, 0, Wx::ALL|Wx::ALIGN_RIGHT, 4)
22
78
 
79
+ @button_save = Wx::Button.new(b_panel, -1, 'Save to file...')
80
+ @button_save.evt_button(@button_save.get_id) { | e | on_save_script(e) }
81
+ b_sizer.add(@button_save, 0, Wx::ALL|Wx::ALIGN_RIGHT, 4)
82
+ b_panel.set_sizer(b_sizer)
83
+
84
+ code_sizer.add(b_panel, 0, Wx::ADJUST_MINSIZE|Wx::ALL)
85
+ code_panel_sizer.add(code_sizer, 1, Wx::ALL|Wx::GROW, 4)
86
+ panel_code.set_sizer(code_panel_sizer)
87
+
88
+
89
+ # the output pane
90
+ panel_outp = Wx::Panel.new(@splitter)
91
+ outp_panel_sizer = Wx::BoxSizer.new(Wx::HORIZONTAL)
92
+ outp_sizer = LabelledVSizer.new(panel_outp, 'Output')
93
+
94
+ @output = Wx::TextCtrl.new(panel_outp, -1, '', DEF_POS, DEF_SIZE,
95
+ Wx::TE_MULTILINE|Wx::TE_READONLY)
96
+ outp_sizer.add(@output, 1, Wx::GROW|Wx::ALL, 4)
97
+ outp_panel_sizer.add(outp_sizer, 1, Wx::GROW|Wx::ALL, 4)
98
+ panel_outp.set_sizer(outp_panel_sizer)
99
+
100
+ @splitter.split_horizontally( panel_code, panel_outp,
101
+ get_client_size.height / 2)
102
+ end
103
+
23
104
  def on_run(e)
24
105
  $stdout = result = StringIO.new()
25
- @app.instance_eval( @code.get_value() )
26
- # STDERR.puts result.inspect()
106
+ $stderr = errors = StringIO.new()
107
+ output.value = ''
108
+ code.reset()
109
+
110
+ file_id = "Script #{obj.dbid}"
111
+ @button_run.disable()
112
+ @button_load.disable()
113
+ @button_save.disable()
114
+
115
+ set_cursor( Wx::BUSY_CURSOR )
116
+ client.app.instance_eval( @code.get_value(), file_id)
117
+
27
118
  result.rewind()
28
- @output.value = result.read()
29
- $stdout = STDOUT
119
+ output.value = result.read()
30
120
  rescue Exception => err
31
- @output.value = err
32
- @output.value += err.backtrace.join("\n")
121
+ script_line = /\A#{file_id}:(\d)/
122
+ trace = err.backtrace
123
+ # remove backtrace info unrelated to the specific script
124
+ trace.pop while trace[-1] !~ script_line
125
+ # find the first backtrace item from within the script
126
+ first_script_err = trace.detect { | x | x =~ script_line }
127
+ # find the script line that caused the problem
128
+ line = first_script_err.match(script_line)[1].to_i - 1
129
+ code.highlight_line(line)
130
+ # spit the error out in the output box
131
+ output.value += "#{err}\n"
132
+ output.value += trace.join("\n")
133
+ ensure
134
+ $stdout, $stderr = STDOUT, STDERR
135
+ set_cursor( Wx::NORMAL_CURSOR )
136
+ @button_run.enable()
137
+ @button_load.enable()
138
+ @button_save.enable()
139
+ end
140
+
141
+ def on_load_script(e)
142
+ file_dialog = ScriptFileDialog.new(self, false)
143
+ return unless file_dialog.show_modal == Wx::ID_OK
144
+ @code.load_file( file_dialog.get_path() )
145
+ end
146
+
147
+ def on_save_script(e)
148
+ file_dialog = ScriptFileDialog.new(self, true)
149
+ return unless file_dialog.show_modal == Wx::ID_OK
150
+ @code.save_file( file_dialog.get_path() )
151
+ end
152
+
153
+ def on_save_output(e)
154
+ file_dialog = ScriptOutputFileDialog.new(self)
155
+ return unless file_dialog.show_modal == Wx::ID_OK
156
+ @output.save_file( file_dialog.get_path() )
33
157
  end
34
158
  end
35
159
  end