ruber 0.0.5 → 0.0.7

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 (83) hide show
  1. data/CHANGES +25 -0
  2. data/bin/ruber +0 -0
  3. data/data/share/apps/ruber/ruberui.rc +15 -1
  4. data/data/share/icons/{ruber.png → ruber-old.pgn} +0 -0
  5. data/lib/ruber/application/application.rb +216 -73
  6. data/lib/ruber/application/plugin.yaml +2 -2
  7. data/lib/ruber/document_project.rb +25 -5
  8. data/lib/ruber/documents/document_list.rb +11 -15
  9. data/lib/ruber/editor/document.rb +106 -50
  10. data/lib/ruber/editor/editor_view.rb +4 -2
  11. data/lib/ruber/external_program_plugin.rb +8 -0
  12. data/lib/ruber/kde_config_option_backend.rb +12 -4
  13. data/lib/ruber/kde_sugar.rb +35 -1
  14. data/lib/ruber/main_window/choose_plugins_dlg.rb +10 -10
  15. data/lib/ruber/main_window/hint_solver.rb +263 -0
  16. data/lib/ruber/main_window/main_window.rb +462 -206
  17. data/lib/ruber/main_window/main_window_actions.rb +228 -62
  18. data/lib/ruber/main_window/main_window_internal.rb +169 -115
  19. data/lib/ruber/main_window/plugin.yaml +13 -3
  20. data/lib/ruber/main_window/save_modified_files_dlg.rb +1 -1
  21. data/lib/ruber/main_window/ui/choose_plugins_widget.rb +1 -1
  22. data/lib/ruber/main_window/ui/main_window_settings_widget.rb +1 -1
  23. data/lib/ruber/main_window/ui/new_project_widget.rb +1 -1
  24. data/lib/ruber/main_window/ui/open_file_in_project_dlg.rb +1 -1
  25. data/lib/ruber/main_window/ui/output_color_widget.rb +1 -1
  26. data/lib/ruber/main_window/ui/workspace_settings_widget.rb +51 -0
  27. data/lib/ruber/main_window/ui/workspace_settings_widget.ui +28 -0
  28. data/lib/ruber/main_window/view_manager.rb +418 -0
  29. data/lib/ruber/main_window/workspace.png +0 -0
  30. data/lib/ruber/output_widget.rb +43 -37
  31. data/lib/ruber/pane.rb +621 -0
  32. data/lib/ruber/plugin_specification_reader.rb +8 -1
  33. data/lib/ruber/projects/project_files_list.rb +6 -0
  34. data/lib/ruber/projects/ui/project_files_rule_chooser_widget.rb +1 -1
  35. data/lib/ruber/projects/ui/project_files_widget.rb +1 -1
  36. data/lib/ruber/qt_sugar.rb +94 -4
  37. data/lib/ruber/utils.rb +16 -7
  38. data/lib/ruber/version.rb +2 -2
  39. data/plugins/autosave/autosave.rb +62 -1
  40. data/plugins/autosave/plugin.yaml +1 -0
  41. data/plugins/autosave/ui/autosave_config_widget.rb +37 -14
  42. data/plugins/autosave/ui/autosave_config_widget.ui +62 -12
  43. data/plugins/find_in_files/find_in_files_widgets.rb +1 -3
  44. data/plugins/find_in_files/ui/config_widget.rb +1 -1
  45. data/plugins/find_in_files/ui/find_in_files_widget.rb +1 -1
  46. data/plugins/rake/plugin.yaml +1 -1
  47. data/plugins/rake/ui/add_quick_task_widget.rb +1 -1
  48. data/plugins/rake/ui/choose_task_widget.rb +1 -1
  49. data/plugins/rake/ui/config_widget.rb +1 -1
  50. data/plugins/rake/ui/project_widget.rb +1 -1
  51. data/plugins/rspec/rspec.rb +14 -22
  52. data/plugins/rspec/ruber_rspec_formatter.rb +4 -1
  53. data/plugins/rspec/ui/rspec_project_widget.rb +1 -1
  54. data/plugins/ruby_development/plugin.yaml +7 -2
  55. data/plugins/ruby_development/ruby_development.rb +134 -13
  56. data/plugins/ruby_development/ui/config_widget.rb +66 -0
  57. data/plugins/ruby_development/ui/config_widget.ui +58 -0
  58. data/plugins/ruby_development/ui/project_widget.rb +1 -1
  59. data/plugins/ruby_runner/plugin.yaml +2 -2
  60. data/plugins/ruby_runner/ruby_runner.rb +15 -3
  61. data/plugins/ruby_runner/ui/config_widget.rb +1 -1
  62. data/plugins/ruby_runner/ui/project_widget.rb +1 -1
  63. data/plugins/ruby_runner/ui/ruby_runnner_plugin_option_widget.rb +1 -1
  64. data/plugins/state/plugin.yaml +6 -2
  65. data/plugins/state/state.rb +305 -81
  66. data/plugins/state/ui/config_widget.rb +1 -1
  67. data/spec/common.rb +11 -3
  68. data/spec/document_list_spec.rb +8 -8
  69. data/spec/document_project_spec.rb +98 -25
  70. data/spec/document_spec.rb +178 -152
  71. data/spec/editor_view_spec.rb +26 -5
  72. data/spec/framework.rb +5 -0
  73. data/spec/hint_solver_spec.rb +450 -0
  74. data/spec/kde_sugar_spec.rb +73 -6
  75. data/spec/output_widget_spec.rb +172 -156
  76. data/spec/pane_spec.rb +1165 -0
  77. data/spec/plugin_specification_reader_spec.rb +37 -1
  78. data/spec/project_files_list_spec.rb +30 -20
  79. data/spec/qt_sugar_spec.rb +269 -0
  80. data/spec/state_spec.rb +566 -353
  81. data/spec/utils_spec.rb +1 -1
  82. data/spec/view_manager_spec.rb +71 -0
  83. metadata +16 -4
@@ -663,7 +663,7 @@ likely doing that, not requesting to open a file
663
663
  return unless file
664
664
  line = file[1]
665
665
  line -= 1 if line > 0
666
- Ruber[:main_window].display_document file[0], line
666
+ Ruber[:main_window].display_document file[0], line, 0
667
667
  Ruber[:main_window].hide_tool self if (Qt::MetaModifier & modifiers) == 0
668
668
  end
669
669
 
@@ -720,14 +720,21 @@ method with one which always returns *nil*.
720
720
  else idx.data.to_string
721
721
  end
722
722
  res = find_filename_in_string str
723
+ d res
723
724
  return unless res
724
725
  res = Array res
725
726
  res << 0 if res.size == 1
726
- unless Pathname.new(res[0]).absolute?
727
- res[0] = File.join @working_dir, res[0]
727
+ #if res[0] is an url with scheme file:, transform it into a regular file
728
+ #name by removing the scheme and the two following slash
729
+ res[0].sub! %r{^file://}, ''
730
+ if KDE::Url.file_url?(res[0]) then res
731
+ else
732
+ res[0] = File.join @working_dir, res[0] unless Pathname.new(res[0]).absolute?
733
+ if File.exist?(res[0]) and !File.directory?(res[0])
734
+ res
735
+ else nil
736
+ end
728
737
  end
729
- return nil unless File.exist?(res[0]) and !File.directory?(res[0])
730
- res
731
738
  end
732
739
 
733
740
  =begin rdoc
@@ -739,50 +746,49 @@ What is a file name and what isn't is a bit arbitrary. Here's what this method
739
746
  recognizes as a filename:
740
747
  * an absolute path not containing spaces and colons starting with '/'
741
748
  * an absolute path not containing spaces and colons starting with '~' or '~user'
742
- (they're expanded using <tt>File.expand_path</tt>)
743
- * a relative path starting with . or .. (either followed by a slash or not)
749
+ (they're expanded using @File.expand_path@)
750
+ * a relative path starting with @./@ or @../@ (either followed by a slash or not)
751
+ * a relative path of the form @.filename@ or @.dirname/dir/file@
744
752
  * any string not containing spaces or colons followed by a colon and a line number
753
+ * absolute URLs with an authority component
754
+
755
+ File names enclosed in quotes or parentheses are recognized.
745
756
 
746
757
  The first three entries of the previous list can be followed by a colon and a line
747
758
  number; for the last one they're mandatory
748
759
  =end
749
- def find_filename_in_string str\
760
+ def find_filename_in_string str
750
761
  #This ensures that file names inside quotes or brackets are found. It's
751
762
  #easier replacing quotes and brackets with spaces than to modify the main
752
763
  #regexp to take them into account
753
764
  str = str.gsub %r|['"`<>\(\)\[\]\{\}]|, ' '
754
- reg = %r{(?: #This is the grouping for the big or operator
755
- (?:\s|^) #Here starts the first alternative. The filename must either
756
- #come after a whitespace or at the beginning of the string
757
- ( #Here starts the capturing group for the filename
758
- (?: #We have to consider separately strings with a known start (the
759
- # first branch of the |) and files without a definite start
760
- # containing a slash (the second branch)
761
- (?:/|~|\.\.?) # This non-capturing group is for the beginning of
762
- # the file. It may start with a slash (absolute path),
763
- # a tilde (absolute path for the home directory)
764
- # or a dot (relative path)
765
- [^\s:]* # The start of the filename is followed by any number of
766
- # non-space, non-colon character
767
- |[^\s:/]+/[^\s:/]*) #here we deal with files starting with any other
768
- #character and containing at least one slash
769
- [^\s:/]) # and by at least one non-colon, non-space and non-slash
770
- # character (to attempt to avoid directories, which may
771
- # end with a slash)
772
- (?:\s|$|(?::(\d+)))) # the file name can be followed by either a
773
- # space, the end of the string or a colon and
774
- # some digits (the line number)
775
- |(?:([^\s]+):(\d+) # Here's the second alternative: anything except
776
- # a space followed by a colon and at least one
777
- # digit (the line number)
778
- ) #The end of the large group}x
779
- match = reg.match str
765
+ matches = []
766
+ attempts = [
767
+ %r{(?:^|\s)([\w+.-]+:/{1,2}(?:/[^/:\s]+)+)(?::(\d+))?(?:$|[,.;:\s])}, #URLS
768
+ %r{(?:^|\s)((?:/[^/\s:]+)+)(?::(\d+))?(?:$|[,.;:\s])}, #absolute files
769
+ #absolute files referring to user home directory: ~/xyz or ~user/xyz
770
+ %r{(?:^|\s)(~[\w_-]*(?:/[^/\s:]+)+)(?::(\d+))?(?:$|[,.;:\s])},
771
+ #relative files starting with ./ and ../
772
+ %r{(?:^|\s)(\.{1,2}(?:/[^/\s:]+)+)(?::(\d+))?(?:$|[,.;:\s])},
773
+ #hidden files or directories (.filename or .dir/filename)
774
+ %r{(?:^|\s)(\.[^/\s:]+(?:/[^/\s:]+)*)(?::(\d+))?(?:$|[,.;:\s])},
775
+ #relative files containing, but not ending with a slash
776
+ %r{(?:^|\s)([^/\s:]+/[^\s:]*[^\s/:])(?::(\d+))?(?:$|[,.;:\s])},
777
+ #relative files not containing slashes but ending with the line number
778
+ %r{(?:^|\s)([^/\s:]+):(\d+)(?:$|[,.;:\s])}
779
+ ]
780
+ attempts.each do |a|
781
+ m = str.match a
782
+ matches << [m.begin(0),[$1,$2]] if m
783
+ end
784
+ d str
785
+ d matches
786
+ match = matches.sort_by{|i| i[0]}[0]
780
787
  return unless match
781
- file = match[1] || match[3]
782
- ln = match[1] ? match[2] : match[4]
788
+ file, line = *match[1]
783
789
  file = File.expand_path(file) if file.start_with? '~'
784
790
  res = [file]
785
- res << ln.to_i if ln
791
+ res << line.to_i if line
786
792
  res
787
793
  end
788
794
 
@@ -0,0 +1,621 @@
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 'facets/boolean'
22
+
23
+ module Ruber
24
+
25
+ =begin rdoc
26
+ Container used to organize multiple editor views in a tab
27
+
28
+ A pane can either contain a single non-{Pane} widget (usually an {EditorView})
29
+ or multiple {Pane}s in a @Qt::Splitter@. In the first case, the {Pane} is said
30
+ to be in _single view mode_, while in the second it's said to be in _multiple view
31
+ mode_.
32
+
33
+ A {Pane} is said to be a direct child of (or directly contained in) another {Pane}
34
+ if it is one of the widgets contained in the second {Pane}'s splitter. A non-{Pane}
35
+ widget is said to be a direct child of (or directly contained in) a {Pane} if the
36
+ pane is in single view mode and contains that widget or if it is in multiple view
37
+ mode and one of the widgets in the splitter is in single view mode and contains
38
+ that widget.
39
+
40
+ The most important method provided by this class is {#split}, which allows a view
41
+ to be split either horizontally or vertically.
42
+
43
+ Whenever a view is closed, it is automatically removed from the pane, and panes
44
+ are automatically rearranged. The only situation you should care for this is when
45
+ the last view of a top-level pane is closed. In this case, the pane emits the {#closing_last_view}
46
+ signal.
47
+
48
+ *Note*: the pane containing a given view (or pane) can change without warning, so
49
+ never store them. To access the pane directly containing another pane, use {#parent_pane}
50
+ on the child pane; to access the pane directly containing another widget, call
51
+ the widget's @parent@ method.
52
+
53
+ *Note:* this class allows to access the splitter widget. This is only meant to
54
+ allow to resize it. It *must not* be used to add or remove widgets to or from it.
55
+ =end
56
+ class Pane < Qt::Widget
57
+
58
+ include Enumerable
59
+
60
+ =begin rdoc
61
+ @overload initialize(view, parent = nil)
62
+ Creates a {Pane} containing a single view
63
+ @param [EditorView] view the view to insert in the pane
64
+ @param [Qt::Widget,nil] parent the parent widget
65
+ @return [Pane]
66
+ @overload initialize(orientation, pane1, pane2, parent = nil)
67
+ Creates a {Pane} containing two other panes in a splitter widget
68
+ @param [Integer] orientation the orientation of the splitter. It can be @Qt::Horizontal@
69
+ or @Qt::Vertical@
70
+ @param [Pane] pane1 the pane to put in the first sector of the splitter
71
+ @param [Pane] pane2 the pane to put in the second sector of the splitter
72
+ @param [Qt::Widget,nil] parent the parent widget
73
+ @return [Pane]
74
+ =end
75
+ def initialize *args
76
+ case args.size
77
+ when 1..2
78
+ super args[1]
79
+ @view = args[0]
80
+ @view.parent = self
81
+ self.layout = Qt::VBoxLayout.new self
82
+ layout.add_widget @view
83
+ @splitter = nil
84
+ connect view, SIGNAL('closing(QWidget*)'), self, SLOT('remove_view(QWidget*)')
85
+ when 3..4
86
+ super args[3]
87
+ self.layout = Qt::VBoxLayout.new self
88
+ orientation, pane1, pane2 = args[0..3]
89
+ @splitter = Qt::Splitter.new orientation, self
90
+ layout.add_widget @splitter
91
+ insert_widget 0, pane1
92
+ insert_widget 1, pane2
93
+ @view = nil
94
+ end
95
+ margins = layout.contents_margins
96
+ margins.top = margins.left = margins.bottom = margins.right = 0
97
+ layout.contents_margins = margins
98
+ @label = Qt::Label.new '', self
99
+ @label.hide unless parent_pane
100
+ layout.add_widget @label
101
+ end
102
+
103
+ =begin rdoc
104
+ @return [Pane] this pane's containing pane
105
+ =end
106
+ def parent_pane
107
+ if parent.is_a?(Qt::Splitter)
108
+ pane = parent.parent
109
+ pane.is_a?(Pane) ? pane : nil
110
+ end
111
+ end
112
+
113
+ =begin rdoc
114
+ Whether the pane contains another pane or widget
115
+
116
+ @overload contain? widget
117
+ Whether the pane contains, directly or indirectly, another pane or widget
118
+ @param [Pane,Qt::Widget] widget the {Pane} or widget to look for
119
+ @return [Boolean] *true* if _widget_ is directly or indirectly contained in this
120
+ pane and *false* if it isn't contained in it
121
+
122
+ @overload contain? widget, :directly
123
+ Whether the pane directly contains another pane or widget
124
+ @param [Pane,Qt::Widget] widget the {Pane} or widget to look for
125
+ @return [Boolean] *true* if _widget_ is directly contained in this
126
+ pane and *false* if it isn't contained in it or it's contained indirectly
127
+ =end
128
+ def contain? widget, mode = nil
129
+ if mode
130
+ if @splitter then @splitter.any?{|w| w == widget}
131
+ else @view == widget
132
+ end
133
+ else find_children(Pane).include? widget
134
+ end
135
+ end
136
+
137
+ =begin rdoc
138
+ Signal emitted whenever the single view associated with the paned is about to be closed
139
+ @param [Pane] pane the pane which emitted the signal
140
+ =end
141
+ signals 'closing_last_view(QWidget*)'
142
+
143
+ =begin rdoc
144
+ Signal emitted whenever a view in the pane or one of its children has been removed
145
+
146
+ @param [Pane] pane the pane the view was child of
147
+ @param [Qt::Widget] view the view which was removed from the pane. Note that when
148
+ this signal is emitted, the view hasn't as yet been destroyed, but it has already
149
+ been removed from the pane (@view.parent@ returns *nil*)
150
+ =end
151
+ signals 'removing_view(QWidget*, QWidget*)'
152
+
153
+ =begin rdoc
154
+ Signal emitted after the pane has been split
155
+
156
+ @param [Pane] pane the pane which has been split
157
+ @param [Qt::Widget] old_view the view which has been split
158
+ @param [Qt::Widget] new_view the view which has been inserted
159
+ =end
160
+ signals 'pane_split(QWidget*, QWidget*, QWidget*)'
161
+
162
+ =begin rdoc
163
+ Signal emitted after a view has been replaced
164
+
165
+ @param [Pane] pane the pane the original view was child of
166
+ @param [Qt::Widget] old_view the view which was replaced. Note that when
167
+ this signal is emitted, the view hasn't been destroyed, but it has
168
+ been removed from the pane (@old_view.parent@ returns *nil*)
169
+ @param [Qt::Widget] replacement the view which replaced _old_view_. Note that at
170
+ the time this signal is emitted, the new view has already been made a child of
171
+ the pane
172
+ =end
173
+ signals 'view_replaced(QWidget*, QWidget*, QWidget*)'
174
+
175
+ =begin rdoc
176
+ @return [Qt::Splitter,nil] the splitter used by the pane or *nil* if the pane
177
+ contains a single view
178
+ =end
179
+ attr_reader :splitter
180
+
181
+ =begin rdoc
182
+ Whether the pane contains a single view or not
183
+
184
+ @return [Boolean] *true* if the pane contains a single view an *false* if it contains
185
+ a splitter with multiple panes
186
+ =end
187
+ def single_view?
188
+ @view.to_b
189
+ end
190
+
191
+ =begin rdoc
192
+ The view contained in the pane
193
+ @return [EditorView, nil] the view contained in the pane or *nil* if the pane contains
194
+ multiple panes
195
+ =end
196
+ def view
197
+ @view
198
+ end
199
+
200
+ =begin rdoc
201
+ The orientation in which the pane is split
202
+ @return [Integer,nil] @Qt::Horizontal@ or @Qt::Vertical@ according to the orientation
203
+ of the splitter or *nil* if the pane contains a single view
204
+ =end
205
+ def orientation
206
+ @splitter ? @splitter.orientation : nil
207
+ end
208
+
209
+ =begin rdoc
210
+ Splits the pane in two in correspondence to the given view
211
+
212
+ The place previously occupated by the view is divided bewtween it and another view,
213
+ _new_view_. If needed, other panes are created to accomodate them.
214
+
215
+ The view to split must already be contained in the pane. It can be contained directly,
216
+ as the only view of the pane or inserted in the splitter contained in the pane,
217
+ or indirectly, contained in one of the panes contained by this pane.
218
+
219
+ If _view_ is contained indirectly, the method call will be redirected to the correct
220
+ pane (not the one associated with the view but the pane containing the latter).
221
+
222
+ If _view_ is not contained in this pane, nothing is done.
223
+
224
+ _new_view_ must not be associated with a pane. When this method returns, a new
225
+ pane for it will have been created.
226
+
227
+ *Note:* after calling this method, the pane associated with _view_ may be changed.
228
+ @param [Ruber::EditorView] view the view to split
229
+ @param [Ruber::EditorView] new_view the view to insert alongside _view_
230
+ @param [Integer] orientation whether the view should be split horizontally or
231
+ vertically. It can be @Qt::Horizontal@ or @Qt::Vertical@
232
+ @param [Symbol] pos whether _new_view_ should be put after or before _view_. It
233
+ can be @:after@ or @:before@
234
+ @return [Array(Pane, Pane), nil] an array containing the panes associated with _view_
235
+ and with _new_view_. If _view_ wasn't contained in the pane, *nil* is returned
236
+ =end
237
+ def split view, new_view, orientation, pos = :after
238
+ idx = index_of_contained_view view
239
+ return split_recursive view, new_view, orientation, pos unless idx
240
+ new_pane = Pane.new(new_view)
241
+ multiple_view_mode orientation
242
+ old_pane = @splitter.widget idx
243
+ keeping_focus old_pane do
244
+ if @splitter.orientation == orientation
245
+ idx += 1 if pos == :after
246
+ insert_widget idx, new_pane
247
+ else
248
+ pane = old_pane.multiple_view_mode orientation
249
+ new_idx = pos == :after ? 1 : 0
250
+ old_pane.insert_widget new_idx, new_pane
251
+ old_pane = pane
252
+ end
253
+ end
254
+ emit pane_split self, view, new_view
255
+ [old_pane, new_pane]
256
+ end
257
+
258
+ =begin rdoc
259
+ Replaces a view with another
260
+
261
+ If the pane is in single view mode and its view is the same as _old_, the view is
262
+ replaced with _replacement_. If the view contained in the pane is different from
263
+ _old_, nothing is done.
264
+
265
+ If the pane is in multiple view mode, the method call will be propagated to all
266
+ child panes, until one is able to carry out the replacement.
267
+
268
+ @param [Qt::Widget] old the view to replace
269
+ @param [Qt::Widget] replacement the view to insert in the pane in place of _old_
270
+ @return [Boolean] *true* if the replacement was performed (that is, if the pane
271
+ or one of its children contained _old_) and *false* otherwise
272
+ =end
273
+ def replace_view old, replacement
274
+ if @view
275
+ return false unless @view == old
276
+ @view = replacement
277
+ replacement.parent = self
278
+ layout.insert_widget 0, replacement
279
+ disconnect old, SIGNAL('closing(QWidget*)'), self, SLOT('remove_view(QWidget*)')
280
+ connect replacement, SIGNAL('closing(QWidget*)'), self, SLOT('remove_view(QWidget*)')
281
+ layout.remove_widget old
282
+ old.parent = nil
283
+ emit view_replaced(self, old, replacement)
284
+ true
285
+ else each_pane.any?{|pn| pn.replace_view old, replacement}
286
+ end
287
+ end
288
+
289
+ =begin rdoc
290
+ Iterates on child panes
291
+
292
+ @overload each_pane
293
+ Iterates only on panes which are directly contained in this pane. Does nothing
294
+ if the pane is in single view mode.
295
+ @yieldparam [Pane] pane a child pane
296
+ @overload each_pane(:recursive)
297
+ Iterates on all contained panes (recursively). Does nothing if the pane is in
298
+ single view mode.
299
+ @yieldparam [Pane] pane a child pane
300
+ @return [Pane,Enumerator] if called with a block returns *self*, otherwise an Enumerator
301
+ which iterates on the contained panes, acting recursively or not according to
302
+ which of the two forms is called
303
+ =end
304
+ def each_pane mode = :flat, &blk
305
+ return to_enum(:each_pane, mode) unless block_given?
306
+ return self unless @splitter
307
+ if mode == :flat then @splitter.each{|w| yield w}
308
+ else
309
+ @splitter.each do |w|
310
+ yield w
311
+ w.each_pane :recursive, &blk
312
+ end
313
+ end
314
+ self
315
+ end
316
+
317
+ =begin rdoc
318
+ Changes the text of the label for the given view
319
+
320
+ If the pane is in single view mode and the view given as argument is the same
321
+ contained in it, the text of the label will be changed, otherwise nothing will be
322
+ done. If the pane is not a toplevel pane the label will be also made visible. If
323
+ the pane is a toplevel pane, the label won't be shown. The rationale for this
324
+ behaviour is that the label can be used to distinguish different widgets in the
325
+ same pane. If a top-level pane is in single view mode, however, it contains no
326
+ other views, so there's no need for a label to distinguish them.
327
+
328
+ If the text is an empty string, the label will be hidden.
329
+
330
+ If the pane is in multiple view mode, the method call will be propagated recursively
331
+ to child panes, until a pane containing the given view is found.
332
+
333
+ @param [Qt::Widget] view the view to change the label for
334
+ @param [String] text the new label
335
+ @return [Boolean] *true* if the label was changed either in this pane or in one
336
+ of its children and *false* if neither this pane nor any of its children contain
337
+ _view_
338
+ =end
339
+ def set_view_label view, text
340
+ if single_view?
341
+ return false unless @view == view
342
+ @label.text = text
343
+ @label.visible = !text.empty? if parent_pane
344
+ true
345
+ else each_pane.any? {|pn| pn.set_view_label view, text}
346
+ end
347
+ end
348
+
349
+ =begin rdoc
350
+ Changes the label of the view in the pane
351
+
352
+ If the pane is in multiple view mode, nothing is done.
353
+
354
+ If the text is empty the label is hidden. If the text is not empty, the label
355
+ is made visible, unless the pane is top-level.
356
+
357
+ This method is similar to {#set_view_label}, except that it doesn't allow to specify
358
+ the view to change the label for and always acts on the view contained in this pane.
359
+
360
+ @param [String] text the new label
361
+ =end
362
+ def label= text
363
+ set_view_label @view, text if @view
364
+ end
365
+
366
+ =begin rdoc
367
+ The text of label associated with the pane
368
+
369
+ @return [String,nil] the text of the label associated with the pane or *nil* if
370
+ the pane is in multiple view mode
371
+ =end
372
+ def label
373
+ # For some reason, Qt::Label#text returns nil if the text hasn't been set
374
+ # or is set to ''
375
+ @view ? (@label.text || '') : nil
376
+ end
377
+
378
+ =begin rdoc
379
+ Iterates on all views contained in the pane
380
+
381
+ This method always acts recursively, meaning that views indirectly contained in
382
+ the pane are returned.
383
+
384
+ If the pane is in single view mode, that only view is passed to the block.
385
+
386
+ @yieldparam [EditorView] view a view contained (directly) in the pane
387
+ @return [Pane,Enumerator] if a block is given then *self*, otherwise an Enumerator
388
+ which iterates on all the views
389
+ =end
390
+ def each_view &blk
391
+ return to_enum(:each_view) unless block_given?
392
+ if single_view? then yield @view
393
+ else
394
+ each_pane(:recursive) do |pn|
395
+ yield pn.view if pn.single_view?
396
+ end
397
+ end
398
+ self
399
+ end
400
+ alias_method :each, :each_view
401
+
402
+ =begin rdoc
403
+ @return [Array<Qt::Widget>] a list of all the views contained (directly or not)
404
+ in the pane
405
+ =end
406
+ def views
407
+ to_a
408
+ end
409
+
410
+ protected
411
+
412
+ =begin rdoc
413
+ Prepares the pane for a contained pane to be moved elsewhere
414
+
415
+ In practice, this makes the pane to remove parentless and disconnects the {#closing_last_view}
416
+ signal from *self*.
417
+ @param [Pane] the pane which will be removed
418
+ @return [nil]
419
+ =end
420
+ def take_pane pane
421
+ pane.parent = nil
422
+ pane.disconnect SIGNAL('closing_last_view(QWidget*)'), self, SLOT('remove_pane(QWidget*)')
423
+ pane.disconnect SIGNAL('pane_split(QWidget*, QWidget*, QWidget*)'), self, SIGNAL('pane_split(QWidget*, QWidget*, QWidget*)')
424
+ pane.disconnect SIGNAL('removing_view(QWidget*, QWidget*)'), self, SIGNAL('removing_view(QWidget*, QWidget*)')
425
+ pane.disconnect SIGNAL('view_replaced(QWidget*,QWidget*,QWidget*)'), self, SIGNAL('view_replaced(QWidget*,QWidget*,QWidget*)')
426
+ nil
427
+ end
428
+
429
+ =begin rdoc
430
+ Inserts a widget in the splitter
431
+
432
+ If the widget is not a pane, it'll be enclosed in the pane (and become a child of
433
+ it). The {#closing_last_view} signal of the pane will be connected with the {#remove_pane}
434
+ slot of *self*.
435
+
436
+ This method assumes that the pane is not in single view mode
437
+ @param [Integer] idx the index to insert the widget at
438
+ @param [Qt::Widget] widget the widget to insert. If it's not a {Pane}, it'll be
439
+ enclosed in a new pane
440
+ @return [Pane] the pane inserted in the splitter. It'll be _widget_ if it's already
441
+ a {Pane} or the new pane enclosing it if it isn't
442
+ =end
443
+ def insert_widget idx, widget
444
+ widget = Pane.new widget, @splitter unless widget.is_a? Pane
445
+ @splitter.insert_widget idx, widget
446
+ connect widget, SIGNAL('closing_last_view(QWidget*)'), self, SLOT('remove_pane(QWidget*)')
447
+ connect widget, SIGNAL('pane_split(QWidget*,QWidget*,QWidget*)'), self, SIGNAL('pane_split(QWidget*,QWidget*,QWidget*)')
448
+ connect widget, SIGNAL('removing_view(QWidget*, QWidget*)'), self, SIGNAL('removing_view(QWidget*, QWidget*)')
449
+ connect widget, SIGNAL('view_replaced(QWidget*,QWidget*,QWidget*)'), self, SIGNAL('view_replaced(QWidget*,QWidget*,QWidget*)')
450
+ widget
451
+ end
452
+
453
+ =begin rdoc
454
+ Switches the pane to multiple view mode
455
+
456
+ It disconnects the view's {#closing} signal from *self*, creates a new splitter
457
+ and inserts a new pane for the view in it.
458
+
459
+ It does nothing if the pane is already in multiple view mode
460
+ @param [Integer] orientation the orientation of the new splitter. It can be
461
+ @Qt::Vertical@ or @Qt::Horizontal@
462
+ @return [Pane] the new pane associated with the view
463
+ =end
464
+ def multiple_view_mode orientation
465
+ return if @splitter
466
+ keeping_focus @view do
467
+ layout.remove_widget @view
468
+ @label.hide
469
+ @view.disconnect SIGNAL('closing(QWidget*)'), self, SLOT('remove_view(QWidget*)')
470
+ @splitter = Qt::Splitter.new orientation, self
471
+ layout.insert_widget 0, @splitter
472
+ pane = insert_widget 0, @view
473
+ # For some reason, Qt::Label#text returns nil if the text hasn't been set
474
+ # or is set to ''
475
+ pane.label = @label.text || ''
476
+ @view = nil
477
+ pane
478
+ end
479
+ end
480
+
481
+ =begin rdoc
482
+ Switches the pane to single view mode
483
+
484
+ It disconnects the {#closing_last_view} signal of each pane in the splitter from
485
+ self, deletes the splitter then sets the given view as single view for *self*.
486
+
487
+ It does nothing if the splitter is already in single view mode
488
+ @param [EditorView] view the single view to insert in the pane
489
+ @param [String] label the label to assign to the pane
490
+ @return [Pane] self
491
+ =end
492
+ def single_view_mode view, label = ''
493
+ return unless @splitter
494
+ keeping_focus view do
495
+ @view = view
496
+ @view.parent = self unless @view.parent == self
497
+ @splitter.each{|w| w.disconnect SIGNAL('closing_last_view(QWidget*)'), self, SLOT('remove_pane(QWidget*)')}
498
+ layout.remove_widget @splitter
499
+ layout.insert_widget 0, @view
500
+ self.label = label
501
+ @label.visible = false unless parent_pane
502
+ connect @view, SIGNAL('closing(QWidget*)'), self, SLOT('remove_view(QWidget*)')
503
+ self
504
+ end
505
+ end
506
+
507
+
508
+ private
509
+
510
+ def keeping_focus *widgets
511
+ active_window = widgets.find{|w| w.is_active_window}
512
+ focus_widget = Ruber::Application.focus_widget if active_window
513
+ begin yield
514
+ ensure
515
+ if focus_widget and !focus_widget.disposed?
516
+ focus_widget.set_focus
517
+ end
518
+ end
519
+ end
520
+
521
+ =begin rdoc
522
+ Returns the index of the pane containing given view in the splitter
523
+
524
+ If the pane is in single view mode, it returns 0 if the single view is _view_
525
+ and *nil* otherwise.
526
+
527
+ If the pane is in multiple view mode, it returns the index of the pane directly
528
+ containing the view in the splitter or *nil* none of the panes in the splitter
529
+ directly contain _view_.
530
+
531
+ *Note:* this method is not recursive. This means that if the splitter of *self*
532
+ contains the pane @A@, whose splitter contains the pane @B@ which contains _view_,
533
+ this method returns *nil*.
534
+
535
+ @param [EditorView] view the view whose index should be returned
536
+ @return [Integer,nil] the index in the splitter corresponding to the pane containing
537
+ _view_ or *nil* if none of the panes contain it
538
+ =end
539
+ def index_of_contained_view view
540
+ if @splitter then @splitter.find_index{|w| w.view == view}
541
+ elsif @view == view then 0
542
+ else nil
543
+ end
544
+ end
545
+
546
+ =begin rdoc
547
+ Redirects the call to {#split} to the pane which actually contains the pane
548
+ containing the given view
549
+
550
+ It calls {#split} with the arguments passed to it to all panes contained in the splitter
551
+ until one of them returns non-nil, then stops
552
+ @param (see #split)
553
+ @return [Array(Pane, Pane),nil] an array containing the panes associated with _view_
554
+ and with _new_view_. If _view_ wasn't contained in any of the children panes, *nil*
555
+ is returned
556
+ =end
557
+ def split_recursive view, new_view, orientation, pos
558
+ return nil unless @splitter
559
+ @splitter.each do |w|
560
+ res = w.split view, new_view, orientation, pos
561
+ return res if res
562
+ end
563
+ nil
564
+ end
565
+
566
+ =begin rdoc
567
+ Slot called when the single view contained in the pane is closed
568
+
569
+ It emis the {#closing_last_view} signal passing *self* as argument, makes the
570
+ view parentless and schedules *self* for deletion.
571
+
572
+ *Note:* this method assumes the {Pane} is in single view mode
573
+ @param [EditorView] view the view which is being closed
574
+ @return [nil]
575
+ =end
576
+ def remove_view view
577
+ emit closing_last_view(self)
578
+ @view.parent = nil
579
+ emit removing_view self, view
580
+ delete_later
581
+ nil
582
+ end
583
+ slots 'remove_view(QWidget*)'
584
+
585
+ =begin rdoc
586
+ Removes the given pane from the pane
587
+
588
+ Depending on the situation, removing a pane causes different steps to be taken:
589
+ * the pane is always removed from the splitter
590
+ * if only one pane remains and it's in single view mode, it'll be deleted and the
591
+ widget it contain will be displayed in this pane, which will be switched to single
592
+ view mode
593
+ * if only one pane remains and it's in multiple view mode, the pane it contains
594
+ will be moved to this pane instead. The remaining one will be removed
595
+ *Note:* this method is usually called in response to the {#closing_last_view} signal
596
+ emitted by a chid pane, so it assumes the pane is in single view mode.
597
+ @param [Pane] the pane to remove
598
+ @return [nil]
599
+ =end
600
+ def remove_pane pane
601
+ pane.parent = nil
602
+ if @splitter.count == 1
603
+ remaining_pane = @splitter.widget(0)
604
+ if remaining_pane.single_view?
605
+ single_view_mode remaining_pane.view, remaining_pane.label
606
+ else
607
+ take_pane remaining_pane
608
+ remaining_pane.splitter.to_a.each_with_index do |w, i|
609
+ remaining_pane.take_pane w
610
+ insert_widget i, w
611
+ end
612
+ end
613
+ remaining_pane.delete_later
614
+ end
615
+ nil
616
+ end
617
+ slots 'remove_pane(QWidget*)'
618
+
619
+ end
620
+
621
+ end