ruber 0.0.1.1

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 (166) hide show
  1. data/COPYING +339 -0
  2. data/INSTALL +137 -0
  3. data/LICENSE +8 -0
  4. data/bin/ruber +65 -0
  5. data/data/share/apps/ruber/core_components.yaml +31 -0
  6. data/data/share/apps/ruber/ruberui.rc +109 -0
  7. data/data/share/icons/ruber.png +0 -0
  8. data/data/share/pixmaps/ruby.png +0 -0
  9. data/icons/ruber-16.png +0 -0
  10. data/icons/ruber-32.png +0 -0
  11. data/icons/ruber-48.png +0 -0
  12. data/icons/ruber-8.png +0 -0
  13. data/lib/ruber/application/application.rb +288 -0
  14. data/lib/ruber/application/plugin.yaml +11 -0
  15. data/lib/ruber/component_manager.rb +899 -0
  16. data/lib/ruber/config/config.rb +82 -0
  17. data/lib/ruber/config/plugin.yaml +3 -0
  18. data/lib/ruber/document_project.rb +209 -0
  19. data/lib/ruber/documents/document_list.rb +416 -0
  20. data/lib/ruber/documents/plugin.yaml +4 -0
  21. data/lib/ruber/editor/document.rb +506 -0
  22. data/lib/ruber/editor/editor_view.rb +167 -0
  23. data/lib/ruber/editor/ktexteditor_wrapper.rb +202 -0
  24. data/lib/ruber/exception_widgets.rb +245 -0
  25. data/lib/ruber/external_program_plugin.rb +397 -0
  26. data/lib/ruber/filtered_output_widget.rb +342 -0
  27. data/lib/ruber/gui_states_handler.rb +231 -0
  28. data/lib/ruber/kde_config_option_backend.rb +167 -0
  29. data/lib/ruber/kde_sugar.rb +249 -0
  30. data/lib/ruber/main_window/choose_plugins_dlg.rb +353 -0
  31. data/lib/ruber/main_window/main_window.rb +524 -0
  32. data/lib/ruber/main_window/main_window_actions.rb +537 -0
  33. data/lib/ruber/main_window/main_window_internal.rb +239 -0
  34. data/lib/ruber/main_window/open_file_in_project_dlg.rb +212 -0
  35. data/lib/ruber/main_window/output_color_widget.rb +35 -0
  36. data/lib/ruber/main_window/plugin.yaml +58 -0
  37. data/lib/ruber/main_window/save_modified_files_dlg.rb +89 -0
  38. data/lib/ruber/main_window/status_bar.rb +156 -0
  39. data/lib/ruber/main_window/ui/choose_plugins_widget.rb +90 -0
  40. data/lib/ruber/main_window/ui/choose_plugins_widget.ui +77 -0
  41. data/lib/ruber/main_window/ui/main_window_settings_widget.rb +108 -0
  42. data/lib/ruber/main_window/ui/main_window_settings_widget.ui +89 -0
  43. data/lib/ruber/main_window/ui/new_project_widget.rb +119 -0
  44. data/lib/ruber/main_window/ui/new_project_widget.ui +178 -0
  45. data/lib/ruber/main_window/ui/open_file_in_project_dlg.rb +109 -0
  46. data/lib/ruber/main_window/ui/open_file_in_project_dlg.ui +168 -0
  47. data/lib/ruber/main_window/ui/output_color_widget.rb +241 -0
  48. data/lib/ruber/main_window/ui/output_color_widget.ui +204 -0
  49. data/lib/ruber/main_window/workspace.rb +442 -0
  50. data/lib/ruber/output_widget.rb +1093 -0
  51. data/lib/ruber/plugin.rb +264 -0
  52. data/lib/ruber/plugin_like.rb +589 -0
  53. data/lib/ruber/plugin_specification.rb +106 -0
  54. data/lib/ruber/plugin_specification_reader.rb +451 -0
  55. data/lib/ruber/project.rb +493 -0
  56. data/lib/ruber/project_backend.rb +105 -0
  57. data/lib/ruber/projects/plugin.yaml +11 -0
  58. data/lib/ruber/projects/project_files_list.rb +314 -0
  59. data/lib/ruber/projects/project_files_widget.rb +301 -0
  60. data/lib/ruber/projects/project_list.rb +314 -0
  61. data/lib/ruber/projects/ui/project_files_rule_chooser_widget.rb +74 -0
  62. data/lib/ruber/projects/ui/project_files_rule_chooser_widget.ui +61 -0
  63. data/lib/ruber/projects/ui/project_files_widget.rb +117 -0
  64. data/lib/ruber/projects/ui/project_files_widget.ui +123 -0
  65. data/lib/ruber/qt_sugar.rb +673 -0
  66. data/lib/ruber/settings_container.rb +515 -0
  67. data/lib/ruber/settings_dialog.rb +244 -0
  68. data/lib/ruber/settings_dialog_manager.rb +503 -0
  69. data/lib/ruber/utils.rb +414 -0
  70. data/lib/ruber/yaml_option_backend.rb +159 -0
  71. data/outsider_files +15 -0
  72. data/plugins/autosave/autosave.rb +404 -0
  73. data/plugins/autosave/plugin.yaml +16 -0
  74. data/plugins/autosave/ui/autosave_config_widget.rb +83 -0
  75. data/plugins/autosave/ui/autosave_config_widget.ui +68 -0
  76. data/plugins/command/command.png +0 -0
  77. data/plugins/command/command.rb +74 -0
  78. data/plugins/command/plugin.yaml +11 -0
  79. data/plugins/find_in_files/find_in_files.rb +337 -0
  80. data/plugins/find_in_files/find_in_files_dlg.rb +411 -0
  81. data/plugins/find_in_files/find_in_files_ui.rc +11 -0
  82. data/plugins/find_in_files/find_in_files_widgets.rb +485 -0
  83. data/plugins/find_in_files/plugin.yaml +23 -0
  84. data/plugins/find_in_files/ui/config_widget.rb +58 -0
  85. data/plugins/find_in_files/ui/config_widget.ui +41 -0
  86. data/plugins/find_in_files/ui/find_in_files_widget.rb +260 -0
  87. data/plugins/find_in_files/ui/find_in_files_widget.ui +324 -0
  88. data/plugins/project_browser/plugin.yaml +10 -0
  89. data/plugins/project_browser/project_browser.rb +245 -0
  90. data/plugins/rake/plugin.yaml +39 -0
  91. data/plugins/rake/rake.png +0 -0
  92. data/plugins/rake/rake.rb +567 -0
  93. data/plugins/rake/rake_extension.rb +153 -0
  94. data/plugins/rake/rake_widgets.rb +615 -0
  95. data/plugins/rake/rakeui.rc +27 -0
  96. data/plugins/rake/ui/add_quick_task_widget.rb +71 -0
  97. data/plugins/rake/ui/add_quick_task_widget.ui +59 -0
  98. data/plugins/rake/ui/choose_task_widget.rb +77 -0
  99. data/plugins/rake/ui/choose_task_widget.ui +72 -0
  100. data/plugins/rake/ui/config_widget.rb +127 -0
  101. data/plugins/rake/ui/config_widget.ui +123 -0
  102. data/plugins/rake/ui/project_widget.rb +217 -0
  103. data/plugins/rake/ui/project_widget.ui +246 -0
  104. data/plugins/rspec/plugin.yaml +30 -0
  105. data/plugins/rspec/rspec.png +0 -0
  106. data/plugins/rspec/rspec.rb +945 -0
  107. data/plugins/rspec/rspec.svg +90 -0
  108. data/plugins/rspec/rspecui.rc +20 -0
  109. data/plugins/rspec/ruber_rspec_formatter.rb +312 -0
  110. data/plugins/rspec/ui/rspec_project_widget.rb +170 -0
  111. data/plugins/rspec/ui/rspec_project_widget.ui +193 -0
  112. data/plugins/ruby_development/plugin.yaml +27 -0
  113. data/plugins/ruby_development/ruby_development.png +0 -0
  114. data/plugins/ruby_development/ruby_development.rb +453 -0
  115. data/plugins/ruby_development/ruby_developmentui.rc +19 -0
  116. data/plugins/ruby_development/ui/project_widget.rb +112 -0
  117. data/plugins/ruby_development/ui/project_widget.ui +108 -0
  118. data/plugins/ruby_runner/config_widget.rb +116 -0
  119. data/plugins/ruby_runner/plugin.yaml +26 -0
  120. data/plugins/ruby_runner/project_widget.rb +62 -0
  121. data/plugins/ruby_runner/ruby.png +0 -0
  122. data/plugins/ruby_runner/ruby_interpretersui.rc +26 -0
  123. data/plugins/ruby_runner/ruby_runner.rb +411 -0
  124. data/plugins/ruby_runner/ui/config_widget.rb +92 -0
  125. data/plugins/ruby_runner/ui/config_widget.ui +91 -0
  126. data/plugins/ruby_runner/ui/project_widget.rb +60 -0
  127. data/plugins/ruby_runner/ui/project_widget.ui +48 -0
  128. data/plugins/ruby_runner/ui/ruby_runnner_plugin_option_widget.rb +59 -0
  129. data/plugins/ruby_runner/ui/ruby_runnner_plugin_option_widget.ui +44 -0
  130. data/plugins/state/plugin.yaml +28 -0
  131. data/plugins/state/state.rb +520 -0
  132. data/plugins/state/ui/config_widget.rb +92 -0
  133. data/plugins/state/ui/config_widget.ui +89 -0
  134. data/plugins/syntax_checker/plugin.yaml +18 -0
  135. data/plugins/syntax_checker/syntax_checker.rb +662 -0
  136. data/ruber.desktop +10 -0
  137. data/spec/annotation_model_spec.rb +174 -0
  138. data/spec/common.rb +119 -0
  139. data/spec/component_manager_spec.rb +1259 -0
  140. data/spec/document_list_spec.rb +626 -0
  141. data/spec/document_project_spec.rb +373 -0
  142. data/spec/document_spec.rb +779 -0
  143. data/spec/editor_view_spec.rb +167 -0
  144. data/spec/external_program_plugin_spec.rb +676 -0
  145. data/spec/filtered_output_widget_spec.rb +642 -0
  146. data/spec/gui_states_handler_spec.rb +304 -0
  147. data/spec/kde_config_option_backend_spec.rb +214 -0
  148. data/spec/kde_sugar_spec.rb +101 -0
  149. data/spec/ktexteditor_wrapper_spec.rb +305 -0
  150. data/spec/output_widget_spec.rb +1703 -0
  151. data/spec/plugin_spec.rb +1393 -0
  152. data/spec/plugin_specification_reader_spec.rb +1765 -0
  153. data/spec/plugin_specification_spec.rb +401 -0
  154. data/spec/project_backend_spec.rb +172 -0
  155. data/spec/project_files_list_spec.rb +401 -0
  156. data/spec/project_list_spec.rb +511 -0
  157. data/spec/project_spec.rb +990 -0
  158. data/spec/qt_sugar_spec.rb +328 -0
  159. data/spec/settings_container_spec.rb +617 -0
  160. data/spec/settings_dialog_manager_spec.rb +773 -0
  161. data/spec/settings_dialog_spec.rb +419 -0
  162. data/spec/state_spec.rb +991 -0
  163. data/spec/utils_spec.rb +406 -0
  164. data/spec/workspace_spec.rb +869 -0
  165. data/spec/yaml_option_backend_spec.rb +246 -0
  166. metadata +284 -0
@@ -0,0 +1,11 @@
1
+ name: app
2
+ description: The application itself
3
+ require: application
4
+ class: 'Ruber::Application'
5
+ config_options:
6
+ :general:
7
+ :plugin_dirs: {:default: Ruber::Application::DEFAULT_PLUGIN_PATHS}
8
+ :plugins: {:default: 'Ruber::Application::DEFAULT_PLUGINS'}
9
+ :auto_load_project: {:default: false }
10
+ config_widgets:
11
+ - {:caption: General, :pixmap: configure, :code: "w=Qt::CheckBox.new('&Open last project at startup');w.object_name='kcfg_general_auto_load_project';w"}
@@ -0,0 +1,899 @@
1
+ =begin
2
+ Copyright (C) 2010 by Stefano Crocco
3
+ stefano.crocco@alice.it
4
+
5
+ This program is free software; you can redistribute it andor modify
6
+ it under the terms of the GNU General Public License as published by
7
+ the Free Software Foundation; either version 2 of the License, or
8
+ (at your option) any later version.
9
+
10
+ This program is distributed in the hope that it will be useful,
11
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
12
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13
+ GNU General Public License for more details.
14
+
15
+ You should have received a copy of the GNU General Public License
16
+ along with this program; if not, write to the
17
+ Free Software Foundation, Inc.,
18
+ 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
19
+ =end
20
+
21
+ require 'forwardable'
22
+
23
+ require 'dictionary'
24
+ require 'facets/kernel/deep_copy'
25
+ require 'facets/boolean'
26
+
27
+ require 'ruber/plugin'
28
+ require 'ruber/plugin_specification'
29
+
30
+ =begin
31
+ Workflow:
32
+
33
+ * bin/ruber creates an instance of the component manager
34
+ * the component manager adds itself to the list of components (in initialize)
35
+ * bin/ruber calls the component manager's load_core_components method
36
+ * load_core_components calls load_component for the application object
37
+ * Ruber::Application.new creates the application data and command line objects,
38
+ before creating the object itself
39
+ * the application's setup method starts a single shot timer which will cause
40
+ the user plugins to be loaded when the main window is displayed
41
+ * load_core_components calls load_component for the other core components:
42
+ configuration, document keeper, project keeper, main window
43
+ * the setup method of the config object calls application#read_settings
44
+ * bin/ruber displays the main window and starts the application
45
+ * user plugins are loaded
46
+ =end
47
+
48
+ module Ruber
49
+
50
+ class ComponentManager < Qt::Object
51
+
52
+ =begin rdoc
53
+ Helper class used to resolve dependencies among plugins. Most likely you don't
54
+ need to use it, but simply call <tt>Ruber::ComponentManager.sort_plugins</tt>.
55
+ =end
56
+ class PluginSorter
57
+
58
+ =begin rdoc
59
+ Creates a new +PluginSorter+. _pdfs_ is an array of the plugin descriptions to
60
+ sort. _ignored_ is an array containing dependencies to be ignored(maybe because
61
+ they're already loaded). _ignored_ can be either an array of symbols, where each
62
+ symbol is the name of a feature, or an array of +PluginSpecification+s.
63
+
64
+ <b>Note:</b> _pdfs_ should contain dependencies in terms of actual plugins, not
65
+ of features.
66
+ =end
67
+ def initialize pdfs, ignored = []
68
+ @pdfs = {}
69
+ @plugins = {}
70
+ pdfs.each do |i|
71
+ @pdfs[i.name] = i
72
+ @plugins[i.name] = i.deps
73
+ end
74
+ @ignored = ignored.map{|i| i.is_a?(OpenStruct) ? i.name : i}
75
+ @ready = []
76
+ @deps = {}
77
+ end
78
+
79
+ =begin rdoc
80
+ Sorts the plugins associated with the object, according with their dependencies
81
+ and returns an array containing the plugin descriptions sorted in dependence order,
82
+ from the dependence to the dependent.
83
+
84
+ If some of the plugins have dependency which doesn't correspond neither to another
85
+ plugin nor to one of the plugins to ignore, <tt>Ruber::ComponentManager::UnresolvedDep</tt>
86
+ will be raised.
87
+
88
+ If there's a circular dependency among the plugins, <tt>Ruber::ComponentManager::CircularDep</tt>
89
+ will be raised.
90
+ =end
91
+ def sort_plugins
92
+ @plugins.each_value do |v|
93
+ v.reject!{|d| @ignored.include? d}
94
+ end
95
+ unknown = find_unknown_deps
96
+ raise ComponentManager::UnresolvedDep.new unknown unless unknown.empty?
97
+ circular = @plugins.keys.inject([]){ |res, plug| res + find_dep( plug ) }
98
+ raise ComponentManager::CircularDep.new(circular.uniq) unless circular.empty?
99
+ deps = @deps.reject{|k, v| v.nil? }
100
+ res = []
101
+ old_size = deps.size
102
+ until deps.empty?
103
+ ready = deps.select{|k, v| v.empty?}.map{|i| i[0]}.sort_by{|i| i.to_s}
104
+ res += ready
105
+ ready.each do |i|
106
+ deps.each{|d| d[1].delete i}
107
+ deps.delete i
108
+ end
109
+ raise "Circular deps (this shouldn't happen)" unless old_size > deps.size
110
+ old_size = deps.size
111
+ end
112
+ res.map{|i| @pdfs[i]}
113
+ end
114
+
115
+ private
116
+
117
+ =begin rdoc
118
+ Checks whether all the dependencies among the plugins are satisifed either by
119
+ another plugin or by a plugin in the _ignore_ list. Returns a hash which is
120
+ empty if all the dependencies were satisifed and otherwise has for keys the
121
+ names of plugins whose dependencies couldn't be found and for values arrays
122
+ containing the names of the missing dependencies for that plugin.
123
+ =end
124
+ def find_unknown_deps
125
+ known = @plugins.keys
126
+ res = Hash.new{|h, k| h[k] = []}
127
+ known.each do |i|
128
+ missing_deps = @plugins[i] - known
129
+ missing_deps.each{|d| res[d] << i}
130
+ end
131
+ res
132
+ end
133
+
134
+ =begin rdoc
135
+ Finds the dependencies of the plugin _plug_. To do this, it calls itself
136
+ recursively for each of the direct dependencies of the plugin. The dependencies
137
+ found are stored in the <tt>@deps</tt> hash.
138
+
139
+ To avoid an endless loop or a SystemStackError in case of circular dependencies,
140
+ each time the method is called, it is also passed a second argument, an array
141
+ containing the names of the plugins whose dependencies have lead to that call.
142
+
143
+ If circular dependencies are found, the entry in <tt>@deps</tt> corresponding
144
+ to the plugin is set to *nil*, and an array containing the pairs of plugins with
145
+ circular dependencies is returned. If no circular dependencies exist, the returned
146
+ array is empty.
147
+ =end
148
+ def find_dep plug, stack = []
149
+ direct_deps = @plugins[plug] || []
150
+ circ = []
151
+ deps = []
152
+ circ << plug if stack.include? plug
153
+ direct_deps.each{|d| circ << plug << d if stack.include? d}
154
+ if circ.empty?
155
+ deps = []
156
+ res = direct_deps.each do |d|
157
+ circ += find_dep d, stack + [plug] unless @deps.has_key? d
158
+ deps += @deps[d] + [d] if @deps[d]
159
+ end
160
+ end
161
+ @deps[plug] = circ.empty? ? deps : nil
162
+ circ
163
+ end
164
+
165
+ =begin
166
+ Old implementation of sort_plugins (just in case it turns out to be necessary)
167
+ def sort_plugins
168
+ res = []
169
+ until @remaining.empty?
170
+ @ready = @remaining.select{|k, v| v.empty?}.map{|k, v| k}.sort_by{|i| i.to_s}
171
+ report_problem if @ready.empty?
172
+ res += @ready.map{|i| @descriptions[i]}
173
+ @remaining.delete_if{|k, v| @ready.include? k}
174
+ @remaining.each_key{|k| @remaining[k] -= @ready}
175
+ end
176
+ res
177
+ end
178
+
179
+ =end
180
+
181
+
182
+ end
183
+
184
+ =begin rdoc
185
+ Helper class which contains the methods needed to find all the plugins needed
186
+ to satisfy the dependencies of a given set of plugins.
187
+
188
+ The difference between this class and PluginSorter is that the latter needs
189
+ to know all the plugins which should be loaded, while this class has the job of
190
+ finding out which ones need to be loaded.
191
+ =end
192
+ class DepsSolver
193
+
194
+ =begin rdoc
195
+ Creates a new +DepsSolver+.
196
+
197
+ <i>to_load</i> is an array containing the PluginSpecification corresponding describing
198
+ the plugins to load, while <i>availlable</i> is an array containing the
199
+ PluginSpecification which can be used to resolve dependencies.
200
+ =end
201
+ def initialize to_load, availlable
202
+ @to_load = to_load.map{|i| [i.name, i]}.to_h
203
+ @availlable = availlable.map{|i| [i.name, i]}.to_h
204
+ @loaded_features = to_load.inject({}) do |res, i|
205
+ i.features.each{|f| (res[f] ||= []) << i}
206
+ res
207
+ end
208
+ @availlable_plugins = availlable.map{|i| [i.name, i]}.to_h
209
+ @availlable_features = availlable.inject({}) do |res, i|
210
+ i.features.each{|f| (res[f] ||= []) << i}
211
+ res
212
+ end
213
+ # @res is an array containing the result, that is the list of names of
214
+ # the plugins to load to satisfy all dependencies.
215
+ # @deps is a hash containing the reasons for which a given plugin should
216
+ # be loaded. Each key is the name of a plugin, while each value is an array
217
+ # containing the list of plugins directly depending on the key. If the key
218
+ # is in the list of plugins to load (that is in the first argument passed
219
+ # to the constructor), the array contains nil.
220
+ @res = []
221
+ @deps = Hash.new{|h, k| h[k] = []}
222
+ end
223
+
224
+ =begin rdoc
225
+ Tries to resolve the dependencies for the given plugins, returning an array
226
+ containing the plugins needed to satisfy all the dependencies. When a plugin
227
+ depends on a feature _f_, then _f_ is included in the list of needed plugins,
228
+ together with its dependencies, unless another required plugin already provides
229
+ that feature.
230
+
231
+ If some dependencies can't be satisfied, UnresolvedDep is raised. If there are
232
+ circular dependencies, CircularDep is raised.
233
+ =end
234
+ def solve
235
+ errors = {:missing => {}, :circular => []}
236
+ @res = @to_load.values.inject([]) do |r, i|
237
+ @deps[i.name] << nil
238
+ r + solve_for(i, errors, [])
239
+ end
240
+ if !errors[:missing].empty? then raise UnresolvedDep.new errors[:missing]
241
+ elsif !errors[:circular].empty? then raise CircularDep.new errors[:circular]
242
+ end
243
+ remove_unneeded_deps
244
+ @res
245
+ end
246
+
247
+ private
248
+
249
+ =begin rdoc
250
+ Recursively finds all the dependencies for the plugin described by the PluginSpecification
251
+ _pl_.
252
+
253
+ _errors_ is a hash used to store missing dependencies and circular dependencies.
254
+ It should have a <tt>:circular</tt> and a <tt>:missing</tt> key. The corresponding
255
+ values should be an array and a hash.
256
+
257
+ _stack_ is an array containing the names of the plugins whose dependencies are
258
+ being solved and is used to detect circular dependencies. For example, if _stack_
259
+ is: <tt>[:a, :b, :c]</tt>, it would mean that we're resolving the dependencies
260
+ of the plugin :c, which is a dependency of the plugin :b, which is a dependency
261
+ of the plugin :a. If this array contains <tt>pl.name</tt>, then there's a circular
262
+ dependency.
263
+
264
+ <b>Note:</b> this method doesn't raise exceptions if there are circular or missing
265
+ dependencies. Rather, it adds them to _errors_ and goes on (this means that it
266
+ skips both missing and circular dependencies).
267
+ =end
268
+ def solve_for pl, errors, stack
269
+ deps = []
270
+ if stack.include? pl.name
271
+ errors[:circular] << [stack.at(stack.index(pl.name + 1)), pl.name]
272
+ return deps
273
+ end
274
+ stack << pl.name
275
+ unless pl.deps.empty?
276
+ pl.deps.each do |dep|
277
+ next if @loaded_features.include? dep
278
+ new_pl = @availlable_plugins[dep]
279
+ if new_pl
280
+ deps << dep
281
+ @deps[dep] << pl.name
282
+ deps += solve_for new_pl, errors, stack
283
+ else
284
+ (errors[:missing][dep] ||= []) << pl.name
285
+ return []
286
+ end
287
+ end
288
+ end
289
+ stack.pop
290
+ deps
291
+ end
292
+
293
+ =begin rdoc
294
+ Attempts to remove from the list of needed dependencies all those dependencies
295
+ which are there only to provide features already provided by other plugins.
296
+
297
+ For example, if the list of needed plugins includes both the plugin <tt>:a</tt>
298
+ and the plugin <tt>:b</tt>, which provides the feature <tt>:a</tt>, then the
299
+ plugin <tt>:a</tt> whould be removed from the list.
300
+
301
+ If after removing plugins as described above, the list contains plugins which
302
+ aren't needed anymore, because they were there only to satisfy the dependencies
303
+ of plugins which have already been removed, they're also removed.
304
+ =end
305
+ def remove_unneeded_deps
306
+ h = Hash.new{|hash, k| hash[k] = []}
307
+ #A hash having the features as keys and the plugins providing them
308
+ #as values
309
+ deps_features = @res.inject(h) do |res, i|
310
+ @availlable_plugins[i].features.each{|f| res[f] << i}
311
+ res
312
+ end
313
+ to_delete = @res.find{|i| !@deps[i].include?(nil) and !deps_features[i].uniq.only? i}
314
+ until to_delete.nil?
315
+ @res.delete to_delete
316
+ deps_features.each_value{|i| i.delete to_delete}
317
+ new = deps_features[to_delete]
318
+ @deps[new] += @deps[to_delete]
319
+ @deps.delete to_delete
320
+ @deps.each_value{|i| i.delete to_delete}
321
+ to_delete = @res.find{|i| !@deps.include?(nil) and !deps_features[i].only? i}
322
+ to_delete = @deps.find{|k, v| v.empty?}[0] rescue nil unless to_delete
323
+ end
324
+ end
325
+
326
+ end
327
+
328
+ =begin rdoc
329
+ Base class for UnresolvedDep and CircularDep exceptions. It represents all the
330
+ possible kinds of dependency errors.
331
+ =end
332
+ class DependencyError < RuntimeError
333
+ end
334
+
335
+ =begin rdoc
336
+ Exception raised by <tt>Ruber::ComponentManager</tt> when some dependencies of
337
+ the plugins can't be found.
338
+ =end
339
+ class UnresolvedDep < DependencyError
340
+
341
+ =begin rdoc
342
+ a hash containing the missing dependencies. The keys are the names of the plugins
343
+ who have unknown dependencies, while the values are arrays with the name of the
344
+ missing dependencies
345
+ =end
346
+ attr_reader :missing
347
+
348
+ =begin rdoc
349
+ Creates a new <tt>Ruber::ComponentManager::UnresolvedDep</tt>. _missing_ is a
350
+ hash containing the missing dependencies. It must have the same format as
351
+ the +missing+ attribute.
352
+ =end
353
+ def initialize missing
354
+ @missing = Hash[missing]
355
+ text = @missing.map do |k, v|
356
+ "#{k} (needed by #{v.join ','})"
357
+ end
358
+ super "The following plugins couldn't be found: #{text.join(', ')}"
359
+ end
360
+
361
+ end
362
+
363
+ =begin rdoc
364
+ Exception raised by <tt>Ruber::ComponentManager</tt> when circular dependencies
365
+ among plugins are detected.
366
+ =end
367
+ class CircularDep < DependencyError
368
+
369
+ =begin rdoc
370
+ The plugins among which the circular dependencies exist. It's an array of
371
+ arrays. Each inner array contains the name of the two plugins depending
372
+ (perhaps indirectly) on each other.
373
+ =end
374
+ attr_reader :circular_deps
375
+
376
+ =begin rdoc
377
+ Creates a new <tt>Ruber::ComponentManager::CircularDep</tt>. _circular_ is an
378
+ array containing the plugins among which the circular dependencies exist, whith
379
+ the format described for <tt>Ruber::ComponentManager::CircularDep#circular_deps</tt>
380
+ =end
381
+ def initialize circular
382
+ @circular_deps = circular.deep_copy
383
+ super "There were circular dependencies among the following pairs of plugins: #{circular.map{|i| "#{i[0]} and #{i[1]}"}.join ', '}"
384
+ end
385
+ end
386
+
387
+ =begin rdoc
388
+ Exception raised when some plugins couldn't be found. _plugins_ is an array containing
389
+ the names of the plugins which couldn't be found, while _dirs_ is an array of
390
+ the directories searched for those plugins
391
+ =end
392
+ class MissingPlugins < StandardError
393
+ attr_reader :plugins, :dirs
394
+ def initialize plugins, dirs
395
+ @plugins = plugins.dup
396
+ @dirs = dirs.dup
397
+ super "The plugins #{@plugins.join ' '} couldn't be found in the directories #{@dirs.join ' '}"
398
+ end
399
+ end
400
+
401
+ =begin rdoc
402
+ Exception raised when some of the PDFs for the plugins to be loaded contain error.
403
+ It differs from <tt>Ruber::ComponentManager::PluginSpecification::PSFError</tt> only
404
+ in the fact that it contains the list of files which produced an error.
405
+ =end
406
+ class InvalidPDF < StandardError
407
+
408
+ # An array containing the files which produced errors
409
+ attr_reader :files
410
+
411
+ # Creates a new instance. _files_ is an array containing the names of the files
412
+ # which produced errors.
413
+ def initialize files
414
+ @files = files.dup
415
+ super "The following plugin description files contained errors: #{files.join ' '}"
416
+ end
417
+ end
418
+
419
+ =begin rdoc
420
+ Looks in the directories specified in the _dirs_ array for plugins and returns
421
+ a hash having the directory of each found plugin as keys and either the name
422
+ or the PluginSpecification for each plugin as values, depending on the value of the
423
+ _info_ parameter.
424
+
425
+ <b>Note:</b> if more than one directory contains a plugin with the given name,
426
+ only the first (according to the order in which directories are in _dirs_) will
427
+ be taken into account.
428
+ =end
429
+ def self.find_plugins dirs, info = false
430
+ res = {}
431
+ dirs.each do |dir|
432
+ Dir.entries(dir)[2..-1].each do |name|
433
+ next if res[name.to_sym]
434
+ d = File.join dir, name
435
+ if File.directory?(d) and File.exist?(File.join d, 'plugin.yaml')
436
+ if info then
437
+ res[name.to_sym] = PluginSpecification.intro(File.join d, 'plugin.yaml')
438
+ else res[name.to_sym] = d
439
+ end
440
+ end
441
+ end
442
+ end
443
+ res
444
+ end
445
+
446
+
447
+ =begin rdoc
448
+ Replaces features in plugin dependencies with the names of the plugin providing
449
+ them. _pdfs_ is an array containing the <tt>Ruber::PluginSpecification</tt>s of plugins whose dependencies should
450
+ be changed, while _extra_ is an array containing the <tt>PluginSpecification</tt>s of plugins
451
+ which should be used to look up features, but which should not be changed. For
452
+ example, _extra_ may contain descriptions for plugins which are already loaded.
453
+
454
+ It returns an array containing a copy of the <tt>Ruber::PluginSpecification</tt>s whith the dependencies
455
+ correctly changed. If a dependency is unknown, <tt>Ruber::ComponentManager::UnresolvedDep</tt>
456
+ will be raised.
457
+ =end
458
+ def self.resolve_features pdfs, extra = []
459
+ features = (pdfs+extra).inject({}) do |res, pl|
460
+ pl.features.each{|f| res[f] = pl.name}
461
+ res
462
+ end
463
+ missing = Hash.new{|h, k| h[k] = []}
464
+ new_pdfs = pdfs.map do |pl|
465
+ res = pl.deep_copy
466
+ res.deps = pl.deps.map do |d|
467
+ f = features[d]
468
+ missing[pl.name] << d unless f
469
+ f
470
+ end.uniq.compact
471
+ res
472
+ end
473
+ raise UnresolvedDep.new Hash[missing] unless missing.empty?
474
+ new_pdfs
475
+ end
476
+
477
+ =begin rdoc
478
+ Finds all the dependencies for the given plugins choosing among a list.
479
+ <i>to_load</i> is an array containing the +PluginSpecification+ for the plugins to load,
480
+ while _availlable_ is an array containing the plugins which can be used to satisfy
481
+ the dependencies.
482
+
483
+ This method uses <tt>DepsSolver#solve</tt>, so see the documentation for it for
484
+ a more complete description.
485
+ =end
486
+ def self.fill_dependencies to_load, availlable
487
+ solver = DepsSolver.new to_load, availlable
488
+ solver.solve
489
+ end
490
+
491
+ =begin rdoc
492
+ Sorts the plugins in the _pdfs_ array, according with their dependencies
493
+ and returns an array containing the plugin descriptions sorted in dependence order,
494
+ from the dependence to the dependent.
495
+
496
+ _known_ is an array of either symbols or <tt>Ruber::PluginSpecification</tt>s corresponding
497
+ to plugins which can be depended upon but which shouldn't be sorted with the
498
+ others (for example, because they're already loaded).
499
+
500
+ If some of the plugins have dependency which doesn't correspond neither to another
501
+ plugin nor to one of the knonw plugins, <tt>Ruber::ComponentManager::UnresolvedDep</tt>
502
+ will be raised.
503
+
504
+ If there's a circular dependency among the plugins, <tt>Ruber::ComponentManager::CircularDep</tt>
505
+ will be raised.
506
+ =end
507
+ def self.sort_plugins pdfs, known = []
508
+ PluginSorter.new( pdfs, known ).sort_plugins
509
+ end
510
+
511
+ extend Forwardable
512
+
513
+ include Enumerable
514
+
515
+ signals 'loading_component(QObject*)', 'component_loaded(QObject*)',
516
+ 'feature_loaded(QString, QObject*)', 'unloading_component(QObject*)'
517
+
518
+ def_delegators :@features, :[]
519
+
520
+ # Returns the <tt>PluginSpecification</tt> describing the +ComponentManager+
521
+ attr_reader :plugin_description
522
+
523
+ # Creates a new +ComponentManager+
524
+ def initialize
525
+ super
526
+ @components = Dictionary[:components, self]
527
+ @features = {:components => self}
528
+ @plugin_description = PluginSpecification.full({:name => :components, :class => self.class})
529
+ end
530
+
531
+ # Returns <tt>:components</tt>
532
+ def component_name
533
+ @plugin_description.name
534
+ end
535
+ alias plugin_name component_name
536
+
537
+ =begin rdoc
538
+ Method required for the Plugin interface. Does nothing
539
+ =end
540
+ def register_with_project prj
541
+ end
542
+
543
+ =begin rdoc
544
+ Method required for the Plugin interface. Does nothing
545
+ =end
546
+ def remove_from_project prj
547
+ end
548
+
549
+ =begin rdoc
550
+ Method required for the Plugin interface. Does nothing
551
+ =end
552
+ def update_project prj
553
+ end
554
+
555
+ =begin rdoc
556
+ Returns an array containing all the loaded plugins (but not the components), in loading order
557
+ =end
558
+ def plugins
559
+ @components.inject([]) do |res, i|
560
+ res << i[1] if i[1].is_a? Ruber::Plugin
561
+ res
562
+ end
563
+ end
564
+
565
+ =begin rdoc
566
+ Returns an array containing all the loaded components, in loading order
567
+ =end
568
+ def components
569
+ @components.inject([]){|res, i| res << i[1]}
570
+ end
571
+
572
+ =begin rdoc
573
+ Calls the block for each component, passing it as argument to the block. The
574
+ components are passed in reverse loading order (i.e., the last loaded component
575
+ will be the first passed to the block.)
576
+ =end
577
+ def each_component #:yields: comp
578
+ @components.reverse_each{|k, v| yield v}
579
+ end
580
+
581
+ =begin rdoc
582
+ Calls the block for each plugin (that is, for every component of class
583
+ <tt>Ruber::Plugin</tt> or derived), passing it as argument to the block. The
584
+ plugins are passed in reverse loading order (i.e., the last loaded plugin
585
+ will be the first passed to the block.)
586
+ =end
587
+ def each_plugin #:yields: plug
588
+ @components.reverse_each do |k, v|
589
+ yield v if v.is_a?(Ruber::Plugin)
590
+ end
591
+ end
592
+ alias each each_plugin
593
+
594
+ =begin rdoc
595
+ <b>For internal use only</b>
596
+
597
+ Adds the given component to the list of components and at the end of the list
598
+ of sorted components.
599
+ =end
600
+ def add comp
601
+ @components<< [comp.component_name, comp]
602
+ comp.plugin_description.features.each{|f| @features[f] = comp}
603
+ end
604
+
605
+ =begin rdoc
606
+ Loads the component with name _name_.
607
+
608
+ _name_ is the name of a subdirectory (called the <i>component directory</i>
609
+ in the directory where <tt>component_manager.rb</tt>
610
+ is. That directory should contain the PDF file for the component to load.
611
+
612
+ The loading process works as follows:
613
+ * the component directory is added to the KDE resource dirs for the +pixmap+,
614
+ +data+ and +appdata+ resource types.
615
+ * A full <tt>Ruber::PluginSpecification</tt> is generated from the PDF
616
+ (see <tt>Ruber::PluginSpecification.full</tt>). If the file can't
617
+ be read, +SystemCallError+ is raised; if it isn't a valid PDF,
618
+ <tt>Ruber::PluginSpecification::PSFError</tt> is raised. In both cases, a message box
619
+ warning the user is shown.
620
+ * the component object (that is, an instance of the class specified in the +class+
621
+ entry of the PDF) is created
622
+ * the <tt>component_loaded(QObject*)</tt> signal is emitted, passing the component
623
+ object as argument
624
+ * the component object is returned.
625
+
626
+ <b>Note:</b> this method doesn't insert the component object in the components
627
+ list: the component should take care to do it itself, using the +add+ method.
628
+ =end
629
+ def load_component name
630
+ dir = File.expand_path File.join(File.dirname(__FILE__), name)
631
+ if KDE::Application.instance
632
+ KDE::Global.dirs.add_resource_dir 'pixmap', dir
633
+ KDE::Global.dirs.add_resource_dir 'data', dir
634
+ KDE::Global.dirs.add_resource_dir 'appdata', dir
635
+ end
636
+ file = File.join dir, 'plugin.yaml'
637
+ pdf = PluginSpecification.full file
638
+ parent = Ruber[:app] rescue self
639
+ comp = pdf.class_obj.new parent, pdf
640
+ emit component_loaded(comp)
641
+ comp
642
+ end
643
+
644
+ =begin rdoc
645
+ Loads the plugin in the directory _dir_.
646
+
647
+ The directory _dir_ should contain the PDF for the plugin, and its last part
648
+ should correspond to the plugin name.
649
+
650
+ The loading process works as follows:
651
+ * the plugin directory is added to the KDE resource dirs for the +pixmap+,
652
+ +data+ and +appdata+ resource types.
653
+ * A full <tt>Ruber::PluginSpecification</tt> is generated from the PDF
654
+ (see <tt>Ruber::PluginSpecification.full</tt>). If the file can't
655
+ be read, +SystemCallError+ is raised; if it isn't a valid PDF,
656
+ <tt>Ruber::PluginSpecification::PSFError</tt> is raised.
657
+ * the plugin object (that is, an instance of the class specified in the +class+
658
+ entry of the PDF) is created
659
+ * the <tt>component_loaded(QObject*)</tt> signal is emitted, passing the component
660
+ object as argument
661
+ * for each feature provided by the plugin, the signal <tt>feature_loaded(QString, QObject*)</tt>
662
+ is emitted, passing the name of the feature (as string) and the plugin object
663
+ as arguments
664
+ * for each feature _f_ provided by the plugin, a signal "unloading_f(QObject*)"
665
+ is defined
666
+ * the plugin object is returned.
667
+
668
+ <b>Note:</b> this method doesn't insert the plugin object in the components
669
+ list: the plugin should take care to do it itself, using the +add+ method.
670
+ =end
671
+ def load_plugin dir
672
+ KDE::Global.dirs.add_resource_dir 'pixmap', dir
673
+ KDE::Global.dirs.add_resource_dir 'data', dir
674
+ KDE::Global.dirs.add_resource_dir 'appdata', dir
675
+ file = File.join dir, 'plugin.yaml'
676
+ pdf = PluginSpecification.full YAML.load(File.read(file)), dir
677
+ pdf.directory = dir
678
+ plug = pdf.class_obj.new pdf
679
+ emit component_loaded(plug)
680
+ pdf.features.each do |f|
681
+ self.class.class_eval{signals "unloading_#{f}(QObject*)"}
682
+ emit feature_loaded(f.to_s, plug)
683
+ end
684
+ plug.send :delayed_initialize
685
+ plug
686
+ end
687
+
688
+ =begin rdoc
689
+ Makes the +ComponentManager+ load the given plugins. It is the standard method
690
+ to load plugins, because it takes into account dependency order and features.
691
+
692
+ For each plugin, a directory with the same name and containing a file
693
+ <tt>plugin.yaml</tt> is searched in the directories in the _dirs_ array.
694
+ Directories near the beginning of the array have the precedence with respect
695
+ to those near the end of the array (that is, if a plugin is found both in the
696
+ second and in the fourth directories of _dir_, the one in the second directory
697
+ is used). If the directory for some plugins can't be found, +MissingPlugins+ is
698
+ raised.
699
+
700
+ This method attempts to resolve the features for the plugins (see
701
+ <tt>Ruber::ComponentManager.resolve_features</tt>) and to sort them, using also
702
+ the already loaded plugins, if any. If it fails, it raises +UnresolvedDep+ or
703
+ +CircularDep+.
704
+
705
+ Once the plugins have been sorted, it attempts to load each one, according to
706
+ the dependency order. The order in which independent plugins are loaded is
707
+ arbitrary (but consistent: the order will be the same every time). If a plugin
708
+ fails to load, there are several behaviours:
709
+ * if no block has been given, the exception raised by the plugin is propagated
710
+ otherwise, the block is passed with the exception as argument. Depending on the
711
+ value returned by the block, the following happens:
712
+ * if the block returns <tt>:skip</tt>, all remaining plugins are skipped and
713
+ the method returns *true*
714
+ * if the block returns <tt>:silent</tt>, an attempt to load the remaining plugins
715
+ is made. Other loading failures will be ignored
716
+ * if the block any other true value, then the failed plugin is ignored and an
717
+ attempt to load the remaining plugins is made.
718
+ * if the block returns *false* or *nil*, the method immediately returns *false*
719
+
720
+ <tt>load_plugins</tt> returns *true* if all the plugins were successfully loaded
721
+ (or if some failed but the block always returned a true value) and false otherwise.
722
+
723
+ ===== Notes
724
+ * After a failure, dependencies aren't recomputed. This means that most likely
725
+ all the plugins dependent on the failed one will fail, too
726
+ * This method can be conceptually divided into two phases: plugin ordering and
727
+ plugin loading. The first part doesn't change any state. This means that, if
728
+ it fails, the caller is free to attempt to solve the problem (for example,
729
+ to remove the missing plugins and the ones with invalid PDFs from the list)
730
+ and call again <tt>load_plugins</tt>. The part which actually _does_ something
731
+ is the second. If called twice with the same arguments, it can cause trouble,
732
+ since no attempt to skip already-loaded plugins is made. If the caller wants
733
+ to correct errors caused in the second phase, it should put the logic to do
734
+ so in the block.
735
+ =end
736
+ def load_plugins( plugins, dirs ) #:yields: ex
737
+ plugins = plugins.map(&:to_s)
738
+ plugin_files = locate_plugins dirs
739
+ plugins = create_plugins_info plugins, plugin_files, dirs
740
+ plugins = ComponentManager.resolve_features plugins, self.plugins.map{|pl| pl.plugin_description}
741
+ plugins = ComponentManager.sort_plugins plugins, @features.keys
742
+ silent = false
743
+ plugins.each do |pl|
744
+ begin load_plugin File.dirname(plugin_files[pl.name.to_s])
745
+ rescue Exception => e
746
+ @components.delete pl.name
747
+ if silent then next
748
+ elsif block_given?
749
+ res = yield pl, e
750
+ if res == :skip then break
751
+ elsif res == :silent then silent = true
752
+ elsif !res then return false
753
+ end
754
+ else raise
755
+ end
756
+ end
757
+ end
758
+ true
759
+ end
760
+
761
+ =begin rdoc
762
+ Prepares the application for being cleanly closed. To do so, it:
763
+ * asks each plugin to save its settings
764
+ * emits the signal <tt>unloading_component(QObject*)</tt> for each component,
765
+ in reverse loading order
766
+ * calls the shutdown method for each component (in their shutdown methods,
767
+ plugins should emit the "closing(QObject*)" signal)
768
+ * calls the delete_later method of the plugins (not of the components)
769
+ * deletes all the features provided by plugins from the list of features
770
+ * delete all the plugins from the list of loaded components.
771
+ =end
772
+ def shutdown
773
+ each_component{|c| c.save_settings unless c.equal?(self)}
774
+ @components[:config].write
775
+ each_component{|c| c.shutdown unless c.equal? self}
776
+ # @components[:config].write
777
+ # each_component do |c|
778
+ # unless c.equal? self
779
+ # if c.is_a? Plugin
780
+ # c.plugin_description.features.each{|f| emit method("unloading_#{f}").call( c)}
781
+ # end
782
+ # emit unloading_component(c)
783
+ # c.shutdown
784
+ # end
785
+ # end
786
+ # each_plugin {|pl| pl.delete_later}
787
+ # @features.delete_if{|f, pl| pl.is_a? Plugin}
788
+ # @components.delete_if{|_, pl| pl.is_a?(Plugin)}
789
+ end
790
+
791
+ =begin rdoc
792
+ Unloads the plugin called _name_ (_name_ must be a symbol) by doing the following:
793
+ * emit the signal "unloading_*(QObject*)" for each feature provided by the plugin
794
+ * emit the signal "unloading_component(QObject*)"
795
+ * call the +shutdown+ method of the plugin
796
+ * call the <tt>delete_later</tt> method of the plugin
797
+ * remove the features provided by the plugin from the list of features
798
+ * remove the plugin from the list of components
799
+
800
+ If _name_ corresponds to a basic component and not to a plugin, +ArgumentError+
801
+ will be raised (you can't unload a basic component).
802
+ =end
803
+ def unload_plugin name
804
+ plug = @components[name]
805
+ raise ArgumentError, "A component can't be unloaded" unless plug.is_a?(Plugin)
806
+ # plug.save_settings
807
+ plug.plugin_description.features.each do |f|
808
+ emit method("unloading_#{f}").call( plug )
809
+ end
810
+ emit unloading_component plug
811
+ plug.unload
812
+ plug.delete_later
813
+ plug.plugin_description.features.each{|f| @features.delete f}
814
+ @components.delete plug.plugin_name
815
+ end
816
+
817
+ =begin rdoc
818
+ Calls the <tt>query_close</tt> method of all the components (in arbitrary order).
819
+ As soon as one of them returns a false value, it stops and returns *false*. If all
820
+ the calls to <tt>query_close</tt> return a true value, *true* is returned.
821
+
822
+ This method is intented to be called from <tt>MainWindow#queryClose</tt>.
823
+ =end
824
+ def query_close
825
+ res = each_component do |c|
826
+ unless c.equal? self
827
+ break false unless c.query_close
828
+ end
829
+ end
830
+ res.to_bool
831
+ end
832
+
833
+ def session_data
834
+ res = {}
835
+ each_component do |c|
836
+ res.merge! c.session_data unless c.same? self
837
+ end
838
+ res
839
+ end
840
+
841
+ def restore_session data
842
+ each_component do |c|
843
+ c.restore_session data unless c.same? self
844
+ end
845
+ end
846
+
847
+
848
+ private
849
+
850
+ =begin rdoc
851
+ Searches the directories in the _dirs_ array for all the subdirectories containing
852
+ a plugin.yaml file and returns the paths of the files. Returns a hash with keys
853
+ corresponding to plugin names and values corresponding to the path of the PDF
854
+ for the plugin.
855
+ =end
856
+ def locate_plugins dirs
857
+ plugin_files = {}
858
+ dirs.reverse.each do |d|
859
+ Dir.entries(d)[2..-1].each do |f|
860
+ full_dir = File.join d, f
861
+ if File.directory?(full_dir) and File.exist?(File.join(full_dir, 'plugin.yaml'))
862
+ plugin_files[f] = File.join full_dir, 'plugin.yaml'
863
+ end
864
+ end
865
+ end
866
+ plugin_files
867
+ end
868
+
869
+
870
+ =begin rdoc
871
+ Attempts to create <tt>Ruber::PluginSpecification</tt>s for each plugin in the _plugins_
872
+ array. The path for the PDFs is taken from _files_, which is an hash with the
873
+ plugin names as keys and the PDFs paths as values.
874
+
875
+ If some PDFs are missing, <tt>MissingPlugins</tt> is raised. If some PDFs are
876
+ invalid, +InvalidPDF+ is raised. Otherwise, an array containing the <tt>PluginSpecification</tt>s
877
+ for the plugins is returned.
878
+ =end
879
+ def create_plugins_info plugins, files, dirs
880
+ missing = []
881
+ errors = []
882
+ res = plugins.map do |pl|
883
+ file = files[pl]
884
+ if file
885
+ begin PluginSpecification.new file
886
+ rescue ArgumentError, PluginSpecification::PSFError
887
+ errors << file
888
+ end
889
+ else missing << pl
890
+ end
891
+ end
892
+ raise MissingPlugins.new missing, dirs unless missing.empty?
893
+ raise InvalidPDF.new errors unless errors.empty?
894
+ res
895
+ end
896
+
897
+ end
898
+
899
+ end