remove display of debug messages from gui by default
[booh] / lib / booh / GtkAutoTable.rb
1 require 'gtk2'
2
3 require 'booh/booh-lib'
4 include Booh
5
6 class Gtk::AutoTable < Gtk::VBox
7
8     def initialize(row_spacings)
9         @children = []
10         @containers = []
11         @width = -1
12         @old_widths = []
13         @row_spacings = row_spacings
14         @table = nil
15         super
16         recreate_table
17         signal_connect('size-allocate') { |w, allocation|
18             msg 3, "got self allocation: #{allocation}"
19             if @width != allocation.width
20                 if !@old_widths.include?(allocation.width)
21                     @width = allocation.width
22                     @old_widths.unshift(@width)
23                     @old_widths = @old_widths[0..2]
24                     redistribute(false)
25                 else
26                     msg 3, "\tDISABLING: #{allocation.width} - #{@old_widths.join(',')}"
27                 end
28             end
29         }
30         @timeout = Gtk.timeout_add(100) {
31             if @queue_draw
32                 queue_draw
33                 @queue_draw = false
34             end
35             true
36         }
37     end
38
39
40     def destroy
41         Gtk.timeout_remove(@timeout)
42         super.destroy
43     end
44
45     #- add (append) a widget to the list of automatically handled widgets
46     def append(widget, name)
47         #- create my child hash
48         child = { :widget => widget, :name => name }
49         #- put it in the table if last container's widget has been allocated
50         last_container = @containers[-1]
51         if !last_container || last_container[:contained_element] && last_container[:contained_element][:allocation]
52             put(child, (last_container ? last_container[:x] : -1) + 1, last_container ? last_container[:y] : 0)
53         end
54         #- add it to the internal children array
55         @children << child
56
57         #- connect 'size-allocate' signal to be sure to update allocation when received
58         widget.signal_connect('size-allocate') { |w, allocation|
59             msg 3, "got allocation for #{w.hash}: #{allocation} (#{allocation.hash})"
60             chld = @children.find { |e| e[:widget] == w }
61             if chld
62                 old = chld[:allocation]
63                 #- need to copy values because the object will be magically exploded when widget is removed
64                 chld[:allocation] = { :width => allocation.width, :height => allocation.height }
65                 if !old #|| old[:width] != allocation.width # || old[:height] != allocation.height
66                     msg 3, "redistribute!"
67                     chld == @children[0] && old and msg 3, "************ old was #{old[:width]} #{old[:height]}"
68                     redistribute(true)
69                 end
70             else
71                 warn "Critical: child not found!"
72             end
73         }
74
75         #- handle reordering with drag and drop
76         Gtk::Drag.source_set(widget, Gdk::Window::BUTTON1_MASK, [['reorder-elements', Gtk::Drag::TARGET_SAME_APP, 1]], Gdk::DragContext::ACTION_MOVE)
77         Gtk::Drag.dest_set(widget, Gtk::Drag::DEST_DEFAULT_ALL, [['reorder-elements', Gtk::Drag::TARGET_SAME_APP, 1]], Gdk::DragContext::ACTION_MOVE)
78         widget.signal_connect('drag-data-get') { |w, ctxt, selection_data, info, time|
79             selection_data.set(Gdk::Selection::TYPE_STRING, get_current_number(widget).to_s)
80         }
81         widget.signal_connect('drag-data-received') { |w, ctxt, x, y, selection_data, info, time|
82             ctxt.targets.each { |target|
83                 if target.name == 'reorder-elements'
84                     insert(selection_data.data.to_i, get_current_number(widget))
85                 end
86             }
87         }
88     end
89
90     #- remove a widget from the list of automatically handled widgets
91     def remove(widget)
92         @children.each_with_index { |chld, index|
93             if chld[:widget] == widget
94                 @children.delete_at(index)
95                 redistribute(true)
96                 return true
97             end
98         }
99         return false
100     end
101
102     #- remove all widgets
103     def clear
104         @children = []
105         redistribute(false)
106     end
107
108     #- get current order of widget
109     def current_order
110         return @children.collect { |chld| chld[:name] }
111     end
112
113     #- get current [x, y] position of widget within automatically handled table
114     def get_current_pos(widget)
115         chld = @children.find { |e| e[:widget] == widget }
116         if chld
117             return [ chld[:x], chld[:y] ]
118         else
119             return nil
120         end
121     end
122
123     #- get current number (rank in current ordering) of widget within automatically handled table
124     def get_current_number(widget)
125         @children.each_with_index { |chld, index|
126             if chld[:widget] == widget
127                 return index
128             end
129         }
130         return -1
131     end
132
133     #- insert a widget before another by numbers
134     def insert(src, dst)
135         chld = @children.delete_at(src)
136         @children[dst > src ? dst - 1 : dst, 0] = chld
137         redistribute(true)
138     end
139
140     #- get widget at [x, y] position of automatically handled table
141     def get_widget_at_pos(x, y)
142         @children.each { |chld|
143             if chld[:x] == x && chld[:y] == y
144                 return chld[:widget]
145             end
146         }
147         return nil
148     end
149
150     #- get maximum `y' position within the automatically handled table
151     def get_max_y
152         return @children[-1][:y]
153     end
154
155     #- get the current `previous' widget (table-wise); important since widgets can be reordered with mouse drags
156     def get_previous_widget(widget)
157         @children.each_with_index { |chld, index|
158             if chld[:widget] == widget
159                 if index == 0
160                     return nil
161                 else
162                     return @children[index - 1][:widget]
163                 end
164             end
165         }
166         return nil
167     end
168
169     #- get the current `next' widget (table-wise); important since widgets can be reordered with mouse drags
170     def get_next_widget(widget)
171         @children.each_with_index { |chld, index|
172             if chld[:widget] == widget
173                 if index == @children.size - 1
174                     return nil
175                 else
176                     return @children[index + 1][:widget]
177                 end
178             end
179         }
180         return nil
181     end
182
183     #- move specified widget `up' in the table
184     def move_up(widget)
185         @children.each_with_index { |chld, index|
186             if chld[:widget] == widget && chld[:y] > 0
187                 @children.each_with_index { |chld2, index2|
188                     if chld2[:x] == chld[:x] && chld[:y] == chld2[:y] + 1
189                         @children[index], @children[index2] = chld2, chld
190                         redistribute(true)
191                         return true
192                     end
193                 }
194             end
195         }
196         return false
197     end
198
199     #- move specified widget `down' in the table
200     def move_down(widget)
201         @children.each_with_index { |chld, index|
202             if chld[:widget] == widget && chld[:y] < get_max_y
203                 @children.each_with_index { |chld2, index2|
204                     if chld2[:x] == chld[:x] && chld[:y] == chld2[:y] - 1
205                         @children[index], @children[index2] = chld2, chld
206                         redistribute(true)
207                         return true
208                     end
209                 }
210             end
211         }
212         return false
213     end
214
215     #- move specified widget `left' in the table
216     def move_left(widget)
217         @children.each_with_index { |chld, index|
218             if chld[:widget] == widget && chld[:x] > 0
219                 @children[index], @children[index - 1] = @children[index - 1], @children[index]
220                 redistribute(true)
221                 return true
222             end
223         }
224         return false
225     end
226
227     #- move specified widget `right' in the table
228     def move_right(widget)
229         @children.each_with_index { |chld, index|
230             if chld[:widget] == widget && @children[index + 1] && chld[:x] < @children[index + 1][:x]
231                 @children[index], @children[index + 1] = @children[index + 1], @children[index]
232                 redistribute(true)
233                 return true
234             end
235         }
236         return false
237     end
238
239
240     private
241
242     def put(element, x, y)
243         msg 3, "putting #{element[:widget].hash} at #{x},#{y}"
244         element[:x] = x
245         element[:y] = y
246         container = @containers.find { |e| e[:x] == x && e[:y] == y }
247         if !container
248             container = { :x => x, :y => y, :widget => Gtk::VBox.new, :fake => Gtk::Label.new('fake') }
249             msg 3, "attaching at #{x},#{y}"
250             @table.attach(container[:widget], x, x + 1, y, y + 1, Gtk::FILL, Gtk::FILL, 5, 0)
251             @containers << container
252         end
253         if container[:contained_element]
254             container[:widget].remove(container[:contained_element][:widget])
255         end
256         container[:contained_element] = element
257         container[:widget].add(element[:widget])
258         @table.show_all
259     end
260
261     def recreate_table
262         @containers.each { |e|
263             if e[:contained_element]
264                 e[:widget].remove(e[:contained_element][:widget])
265             end
266         }
267         @containers = []
268         if @table
269             remove(@table)
270             @table.destroy
271         end
272         add(@table = Gtk::Table.new(0, 0, true))
273         @table.set_row_spacings(@row_spacings)
274     end
275
276     def redistribute(force)
277         msg 3, "redistribute: "
278         @children.each { |e| msg_ 3, e[:allocation] ? 'O' : '.' }; msg 3, ''
279         if unallocated = @children.find { |e| !e[:allocation] }
280             #- waiting for allocations. replace last displayed widget with first unallocated.
281             last_container = @containers[-1]
282             put(unallocated, last_container[:x], last_container[:y])
283
284         else
285             if @children.size == 0
286                 recreate_table
287                 return
288
289             else
290                 maxwidth = allocation.width
291                 xpix = 5 + @children[0][:allocation][:width] + 5
292                 x = 1
293                 y = 0
294                 @children[1..-1].each { |e|
295                     if xpix + 5 + e[:allocation][:width] + 5 > maxwidth - 1
296                         x = 0
297                         y += 1
298                         xpix = 0
299                     end
300                     e[:xnew] = x
301                     e[:ynew] = y
302                     x += 1
303                     xpix += 5 + e[:allocation][:width] + 5
304                 }
305                 if @children[1..-1].find { |e| e[:xnew] != e[:x] || e[:ynew] != e[:y] } || force
306                     msg 3, "I can proceed with #{allocation}"
307                     recreate_table
308                     put(@children[0], 0, 0)
309                     @children[1..-1].each { |e|
310                         put(e, e[:xnew], e[:ynew])
311                     }
312                     show_all
313                     @queue_draw = true
314                 end
315             end
316         end
317     end
318
319 end