*** empty log message ***
[booh] / lib / booh / GtkAutoTable.rb
1 #                         *  BOOH  *
2 #
3 # A.k.a `Best web-album Of the world, Or your money back, Humerus'.
4 #
5 # The acronyn sucks, however this is a tribute to Dragon Ball by
6 # Akira Toriyama, where the last enemy beaten by heroes of Dragon
7 # Ball is named "Boo". But there was already a free software project
8 # called Boo, so this one will be it "Booh". Or whatever.
9 #
10 #
11 # Copyright (c) 2004 Guillaume Cottenceau <gc3 at bluewin.ch>
12 #
13 # This software may be freely redistributed under the terms of the GNU
14 # public license version 2.
15 #
16 # You should have received a copy of the GNU General Public License
17 # along with this program; if not, write to the Free Software
18 # Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
19
20 require 'gtk2'
21
22 require 'booh/booh-lib'
23 include Booh
24
25 class Gtk::Allocation
26     def to_s
27         "width:#{width} heigth:#{height}"
28     end
29 end
30
31 class Gtk::AutoTable < Gtk::VBox
32
33     def initialize(row_spacings)
34         @children = []
35         @containers = []
36         @width = -1
37         @old_widths = []
38         @row_spacings = row_spacings
39         @table = nil
40         super
41         recreate_table
42         signal_connect('size-allocate') { |w, allocation|
43             msg 3, "got self allocation: #{allocation}"
44             if @width != allocation.width
45                 if !@old_widths.include?(allocation.width)
46                     @width = allocation.width
47                     @old_widths.unshift(@width)
48                     @old_widths = @old_widths[0..2]
49                     redistribute(false)
50                 else
51                     msg 3, "\tDISABLING: #{allocation.width} - #{@old_widths.join(',')}"
52                 end
53             end
54         }
55         @timeout = Gtk.timeout_add(100) {
56             if @queue_draw
57                 queue_draw
58                 @queue_draw = false
59             end
60             true
61         }
62     end
63
64
65     def destroy
66         Gtk.timeout_remove(@timeout)
67         super.destroy
68     end
69
70     #- add (append) a widget to the list of automatically handled widgets
71     def append(widget, name)
72         #- create my child hash
73         child = { :widget => widget, :name => name }
74         #- put it in the table if last container's widget has been allocated
75         last_container = @containers[-1]
76         if !last_container || last_container[:contained_element] && last_container[:contained_element][:allocation]
77             put(child, (last_container ? last_container[:x] : -1) + 1, last_container ? last_container[:y] : 0)
78         end
79         #- add it to the internal children array
80         @children << child
81
82         #- connect 'size-allocate' signal to be sure to update allocation when received
83         widget.signal_connect('size-allocate') { |w, allocation|
84             msg 3, "got allocation for #{w.hash}: #{allocation} (#{allocation.hash})"
85             chld = @children.find { |e| e[:widget] == w }
86             if chld
87                 old = chld[:allocation]
88                 #- need to copy values because the object will be magically exploded when widget is removed
89                 chld[:allocation] = { :width => allocation.width, :height => allocation.height }
90                 if !old #|| old[:width] != allocation.width # || old[:height] != allocation.height
91                     msg 3, "redistribute!"
92                     chld == @children[0] && old and msg 3, "************ old was #{old[:width]} #{old[:height]}"
93                     redistribute(true)
94                 end
95             else
96                 warn "Critical: child not found!"
97             end
98         }
99     end
100
101     #- remove a widget from the list of automatically handled widgets
102     def remove(widget)
103         @children.each_with_index { |chld, index|
104             if chld[:widget] == widget
105                 @children.delete_at(index)
106                 redistribute(true)
107                 return true
108             end
109         }
110         return false
111     end
112
113     #- re-insert a widget at a given pos
114     def reinsert(pos, widget, name)
115         child = { :widget => widget, :name => name }
116         @children[pos, 0] = child
117         redistribute(true)
118     end
119
120     #- remove all widgets
121     def clear
122         @children = []
123         redistribute(false)
124     end
125
126     #- get current order of widget
127     def current_order
128         return @children.collect { |chld| chld[:name] }
129     end
130
131     #- get current [x, y] position of widget within automatically handled table
132     def get_current_pos(widget)
133         chld = @children.find { |e| e[:widget] == widget }
134         if chld
135             return [ chld[:x], chld[:y] ]
136         else
137             return nil
138         end
139     end
140
141     #- get current number (rank in current ordering) of widget within automatically handled table
142     def get_current_number(widget)
143         @children.each_with_index { |chld, index|
144             if chld[:widget] == widget
145                 return index
146             end
147         }
148         return -1
149     end
150
151     #- move widgets by numbers
152     def move(src, dst)
153         if src != dst
154             chld = @children.delete_at(src)
155             @children[dst > src ? dst - 1 : dst, 0] = chld
156             redistribute(true)
157         end
158     end
159
160     #- get widget at [x, y] position of automatically handled table
161     def get_widget_at_pos(x, y)
162         @children.each { |chld|
163             if chld[:x] == x && chld[:y] == y
164                 return chld[:widget]
165             end
166         }
167         return nil
168     end
169
170     #- get maximum `y' position within the automatically handled table
171     def get_max_y
172         return @children[-1][:y]
173     end
174
175     #- get the current `previous' widget (table-wise); important since widgets can be reordered with mouse drags
176     def get_previous_widget(widget)
177         @children.each_with_index { |chld, index|
178             if chld[:widget] == widget
179                 if index == 0
180                     return nil
181                 else
182                     return @children[index - 1][:widget]
183                 end
184             end
185         }
186         return nil
187     end
188
189     #- get the current `next' widget (table-wise); important since widgets can be reordered with mouse drags
190     def get_next_widget(widget)
191         @children.each_with_index { |chld, index|
192             if chld[:widget] == widget
193                 if index == @children.size - 1
194                     return nil
195                 else
196                     return @children[index + 1][:widget]
197                 end
198             end
199         }
200         return nil
201     end
202
203     #- move specified widget `up' in the table
204     def move_up(widget)
205         @children.each_with_index { |chld, index|
206             if chld[:widget] == widget && chld[:y] > 0
207                 @children.each_with_index { |chld2, index2|
208                     if chld2[:x] == chld[:x] && chld[:y] == chld2[:y] + 1
209                         @children[index], @children[index2] = chld2, chld
210                         redistribute(true)
211                         return true
212                     end
213                 }
214             end
215         }
216         return false
217     end
218
219     #- move specified widget `down' in the table
220     def move_down(widget)
221         @children.each_with_index { |chld, index|
222             if chld[:widget] == widget && chld[:y] < get_max_y
223                 @children.each_with_index { |chld2, index2|
224                     if chld2[:x] == chld[:x] && chld[:y] == chld2[:y] - 1
225                         @children[index], @children[index2] = chld2, chld
226                         redistribute(true)
227                         return true
228                     end
229                 }
230             end
231         }
232         return false
233     end
234
235     #- move specified widget `left' in the table
236     def move_left(widget)
237         @children.each_with_index { |chld, index|
238             if chld[:widget] == widget && chld[:x] > 0
239                 @children[index], @children[index - 1] = @children[index - 1], @children[index]
240                 redistribute(true)
241                 return true
242             end
243         }
244         return false
245     end
246
247     #- move specified widget `right' in the table
248     def move_right(widget)
249         @children.each_with_index { |chld, index|
250             if chld[:widget] == widget && @children[index + 1] && chld[:x] < @children[index + 1][:x]
251                 @children[index], @children[index + 1] = @children[index + 1], @children[index]
252                 redistribute(true)
253                 return true
254             end
255         }
256         return false
257     end
258
259
260     private
261
262     def put(element, x, y)
263         msg 3, "putting #{element[:widget].hash} at #{x},#{y}"
264         element[:x] = x
265         element[:y] = y
266         container = @containers.find { |e| e[:x] == x && e[:y] == y }
267         if !container
268             container = { :x => x, :y => y, :widget => Gtk::VBox.new, :fake => Gtk::Label.new('fake') }
269             msg 3, "attaching at #{x},#{y}"
270             @table.attach(container[:widget], x, x + 1, y, y + 1, Gtk::FILL, Gtk::FILL, 5, 0)
271             @containers << container
272         end
273         if container[:contained_element]
274             container[:widget].remove(container[:contained_element][:widget])
275         end
276         container[:contained_element] = element
277         container[:widget].add(element[:widget])
278         @table.show_all
279     end
280
281     def recreate_table
282         @containers.each { |e|
283             if e[:contained_element]
284                 e[:widget].remove(e[:contained_element][:widget])
285             end
286         }
287         @containers = []
288         if @table
289             remove(@table)
290             @table.destroy
291         end
292         add(@table = Gtk::Table.new(0, 0, true))
293         @table.set_row_spacings(@row_spacings)
294     end
295
296     def redistribute(force)
297         msg 3, "redistribute: "
298         @children.each { |e| msg_ 3, e[:allocation] ? 'O' : '.' }; msg 3, ''
299         if unallocated = @children.find { |e| !e[:allocation] }
300             #- waiting for allocations. replace last displayed widget with first unallocated.
301             last_container = @containers[-1]
302             put(unallocated, last_container ? last_container[:x] : 0, last_container ? last_container[:y]: 0)
303
304         else
305             if @children.size == 0
306                 recreate_table
307                 return
308
309             else
310                 totalwidth = allocation.width
311                 maxwidth = @children.collect { |chld| chld[:allocation][:width] }.max
312                 xpix = 5 + maxwidth + 5
313                 x = 1
314                 y = 0
315                 @children[1..-1].each { |e|
316                     if xpix + 5 + maxwidth + 5 > totalwidth - 1
317                         x = 0
318                         y += 1
319                         xpix = 0
320                     end
321                     e[:xnew] = x
322                     e[:ynew] = y
323                     x += 1
324                     xpix += 5 + maxwidth + 5
325                 }
326                 if @children[1..-1].find { |e| e[:xnew] != e[:x] || e[:ynew] != e[:y] } || force
327                     msg 3, "I can proceed with #{allocation}"
328                     recreate_table
329                     put(@children[0], 0, 0)
330                     @children[1..-1].each { |e|
331                         put(e, e[:xnew], e[:ynew])
332                     }
333                     show_all
334                     @queue_draw = true
335                 end
336             end
337         end
338     end
339
340 end