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,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