ruber 0.0.1.1

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