support rotation (typically, of portrait images that came with no EXIF) from GUI...
[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         if src != dst
136             chld = @children.delete_at(src)
137             @children[dst > src ? dst - 1 : dst, 0] = chld
138             redistribute(true)
139         end
140     end
141
142     #- get widget at [x, y] position of automatically handled table
143     def get_widget_at_pos(x, y)
144         @children.each { |chld|
145             if chld[:x] == x && chld[:y] == y
146                 return chld[:widget]
147             end
148         }
149         return nil
150     end
151
152     #- get maximum `y' position within the automatically handled table
153     def get_max_y
154         return @children[-1][:y]
155     end
156
157     #- get the current `previous' widget (table-wise); important since widgets can be reordered with mouse drags
158     def get_previous_widget(widget)
159         @children.each_with_index { |chld, index|
160             if chld[:widget] == widget
161                 if index == 0
162                     return nil
163                 else
164                     return @children[index - 1][:widget]
165                 end
166             end
167         }
168         return nil
169     end
170
171     #- get the current `next' widget (table-wise); important since widgets can be reordered with mouse drags
172     def get_next_widget(widget)
173         @children.each_with_index { |chld, index|
174             if chld[:widget] == widget
175                 if index == @children.size - 1
176                     return nil
177                 else
178                     return @children[index + 1][:widget]
179                 end
180             end
181         }
182         return nil
183     end
184
185     #- move specified widget `up' in the table
186     def move_up(widget)
187         @children.each_with_index { |chld, index|
188             if chld[:widget] == widget && chld[:y] > 0
189                 @children.each_with_index { |chld2, index2|
190                     if chld2[:x] == chld[:x] && chld[:y] == chld2[:y] + 1
191                         @children[index], @children[index2] = chld2, chld
192                         redistribute(true)
193                         return true
194                     end
195                 }
196             end
197         }
198         return false
199     end
200
201     #- move specified widget `down' in the table
202     def move_down(widget)
203         @children.each_with_index { |chld, index|
204             if chld[:widget] == widget && chld[:y] < get_max_y
205                 @children.each_with_index { |chld2, index2|
206                     if chld2[:x] == chld[:x] && chld[:y] == chld2[:y] - 1
207                         @children[index], @children[index2] = chld2, chld
208                         redistribute(true)
209                         return true
210                     end
211                 }
212             end
213         }
214         return false
215     end
216
217     #- move specified widget `left' in the table
218     def move_left(widget)
219         @children.each_with_index { |chld, index|
220             if chld[:widget] == widget && chld[:x] > 0
221                 @children[index], @children[index - 1] = @children[index - 1], @children[index]
222                 redistribute(true)
223                 return true
224             end
225         }
226         return false
227     end
228
229     #- move specified widget `right' in the table
230     def move_right(widget)
231         @children.each_with_index { |chld, index|
232             if chld[:widget] == widget && @children[index + 1] && chld[:x] < @children[index + 1][:x]
233                 @children[index], @children[index + 1] = @children[index + 1], @children[index]
234                 redistribute(true)
235                 return true
236             end
237         }
238         return false
239     end
240
241
242     private
243
244     def put(element, x, y)
245         msg 3, "putting #{element[:widget].hash} at #{x},#{y}"
246         element[:x] = x
247         element[:y] = y
248         container = @containers.find { |e| e[:x] == x && e[:y] == y }
249         if !container
250             container = { :x => x, :y => y, :widget => Gtk::VBox.new, :fake => Gtk::Label.new('fake') }
251             msg 3, "attaching at #{x},#{y}"
252             @table.attach(container[:widget], x, x + 1, y, y + 1, Gtk::FILL, Gtk::FILL, 5, 0)
253             @containers << container
254         end
255         if container[:contained_element]
256             container[:widget].remove(container[:contained_element][:widget])
257         end
258         container[:contained_element] = element
259         container[:widget].add(element[:widget])
260         @table.show_all
261     end
262
263     def recreate_table
264         @containers.each { |e|
265             if e[:contained_element]
266                 e[:widget].remove(e[:contained_element][:widget])
267             end
268         }
269         @containers = []
270         if @table
271             remove(@table)
272             @table.destroy
273         end
274         add(@table = Gtk::Table.new(0, 0, true))
275         @table.set_row_spacings(@row_spacings)
276     end
277
278     def redistribute(force)
279         msg 3, "redistribute: "
280         @children.each { |e| msg_ 3, e[:allocation] ? 'O' : '.' }; msg 3, ''
281         if unallocated = @children.find { |e| !e[:allocation] }
282             #- waiting for allocations. replace last displayed widget with first unallocated.
283             last_container = @containers[-1]
284             put(unallocated, last_container[:x], last_container[:y])
285
286         else
287             if @children.size == 0
288                 recreate_table
289                 return
290
291             else
292                 maxwidth = allocation.width
293                 xpix = 5 + @children[0][:allocation][:width] + 5
294                 x = 1
295                 y = 0
296                 @children[1..-1].each { |e|
297                     if xpix + 5 + e[:allocation][:width] + 5 > maxwidth - 1
298                         x = 0
299                         y += 1
300                         xpix = 0
301                     end
302                     e[:xnew] = x
303                     e[:ynew] = y
304                     x += 1
305                     xpix += 5 + e[:allocation][:width] + 5
306                 }
307                 if @children[1..-1].find { |e| e[:xnew] != e[:x] || e[:ynew] != e[:y] } || force
308                     msg 3, "I can proceed with #{allocation}"
309                     recreate_table
310                     put(@children[0], 0, 0)
311                     @children[1..-1].each { |e|
312                         put(e, e[:xnew], e[:ynew])
313                     }
314                     show_all
315                     @queue_draw = true
316                 end
317             end
318         end
319     end
320
321 end