let merge new/removed images/videos only in one specified directory (in current direc...
[booh] / bin / booh
1 #! /usr/bin/ruby
2 #
3 #                         *  BOOH  *
4 #
5 # A.k.a `Best web-album Of the world, Or your money back, Humerus'.
6 #
7 # The acronyn sucks, however this is a tribute to Dragon Ball by
8 # Akira Toriyama, where the last enemy beaten by heroes of Dragon
9 # Ball is named "Boo". But there was already a free software project
10 # called Boo, so this one will be it "Booh". Or whatever.
11 #
12 #
13 # Copyright (c) 2004 Guillaume Cottenceau <gc3 at bluewin.ch>
14 #
15 # This software may be freely redistributed under the terms of the GNU
16 # public license version 2.
17 #
18 # You should have received a copy of the GNU General Public License
19 # along with this program; if not, write to the Free Software
20 # Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
21
22 require 'getoptlong'
23 require 'tempfile'
24
25 require 'gtk2'
26 require 'booh/gtkadds'
27 require 'booh/GtkAutoTable'
28
29 require 'gettext'
30 include GetText
31 bindtextdomain("booh")
32
33 require 'rexml/document'
34 include REXML
35
36 require 'booh/booh-lib'
37 include Booh
38 require 'booh/UndoHandler'
39
40
41 #- options
42 $options = [
43     [ '--help',          '-h', GetoptLong::NO_ARGUMENT,       _("Get help message") ],
44
45     [ '--verbose-level', '-v', GetoptLong::REQUIRED_ARGUMENT, _("Set max verbosity level (0: errors, 1: warnings, 2: important messages, 3: other messages)") ],
46 ]
47
48 def usage
49     puts _("Usage: %s [OPTION]...") % File.basename($0)
50     $options.each { |ary|
51         printf " %3s, %-15s %s\n", ary[1], ary[0], ary[3]
52     }
53 end
54
55 def handle_options
56     parser = GetoptLong.new
57     parser.set_options(*$options.collect { |ary| ary[0..2] })
58     begin
59         parser.each_option do |name, arg|
60             case name
61             when '--help'
62                 usage
63                 exit(0)
64
65             when '--verbose-level'
66                 $verbose_level = arg.to_i
67
68             end
69         end
70     rescue
71         puts $!
72         usage
73         exit(1)
74     end
75 end
76
77 def read_config
78     $config = {}
79     $config_file = File.expand_path('~/.booh-gui-rc')
80     if File.readable?($config_file)
81         $xmldoc = REXML::Document.new(File.new($config_file))
82         $xmldoc.root.elements.each { |element|
83             txt = element.get_text
84             if txt 
85                 if txt.value =~ /~~~/ || element.name == 'last-opens'
86                     $config[element.name] = txt.value.split(/~~~/)
87                 else
88                     $config[element.name] = txt.value
89                 end
90             else
91                 $config[element.name] = {}
92                 element.each { |chld|
93                     txt = chld.get_text
94                     $config[element.name][chld.name] = txt ? txt.value : nil
95                 }
96             end
97         }
98     end
99     $config['video-viewer'] ||= 'mplayer %f'
100     if !FileTest.directory?(File.expand_path('~/.booh'))
101         system("mkdir ~/.booh")
102     end
103 end
104
105 def write_config
106     if $config['last-opens'] && $config['last-opens'].size > 5
107         $config['last-opens'] = $config['last-opens'][-5, 5]
108     end
109
110     ios = File.open($config_file, "w")
111     $xmldoc = Document.new "<booh-gui-rc version='#{$VERSION}'/>"
112     $xmldoc << XMLDecl.new(XMLDecl::DEFAULT_VERSION, $CURRENT_CHARSET)
113     $config.each_pair { |key, value|
114         elem = $xmldoc.root.add_element key
115         if value.is_a? Hash
116             $config[key].each_pair { |subkey, subvalue|
117                 subelem = elem.add_element subkey
118                 subelem.add_text subvalue.to_s
119             }
120         elsif value.is_a? Array
121             elem.add_text value.join('~~~')
122         else
123             elem.add_text value.to_s
124         end
125     }
126     $xmldoc.write(ios, 0)
127     ios.close
128 end
129
130 def set_mousecursor(what, *widget)
131     if widget[0] && widget[0].window
132         widget[0].window.set_cursor(Gdk::Cursor.new(what))
133     end
134     if $main_window.window
135         $main_window.window.set_cursor(Gdk::Cursor.new(what))
136     end
137     $current_cursor = what
138 end
139 def set_mousecursor_wait(*widget)
140     set_mousecursor(Gdk::Cursor::WATCH, *widget)
141     if Thread.current == Thread.main
142         Gtk.main_iteration while Gtk.events_pending?
143     end
144 end
145 def set_mousecursor_normal(*widget)
146     set_mousecursor(Gdk::Cursor::LEFT_PTR, *widget)
147 end
148 def push_mousecursor_wait(*widget)
149     if $current_cursor != Gdk::Cursor::WATCH
150         $save_cursor = $current_cursor
151         set_mousecursor_wait(*widget)
152     end
153 end
154 def pop_mousecursor(*widget)
155     set_mousecursor($save_cursor || Gdk::Cursor::LEFT_PTR, *widget)
156 end
157
158 def current_dest_dir
159     source = $xmldoc.root.attributes['source']
160     dest = $xmldoc.root.attributes['destination']
161     return make_dest_filename(from_utf8($current_path.sub(/^#{Regexp.quote(source)}/, dest)))
162 end
163
164 def build_full_dest_filename(filename)
165     return current_dest_dir + '/' + make_dest_filename(from_utf8(filename))
166 end
167
168 def save_undo(name, closure, *params)
169     UndoHandler.save_undo(name, closure, [ *params ])
170     $undo_tb.sensitive = $undo_mb.sensitive = true
171 end
172
173 def view_element(filename)
174     if entry2type(filename) == 'video'
175         cmd = from_utf8($config['video-viewer']).gsub('%f', "'#{from_utf8($current_path + '/' + filename)}'")
176         msg 2, cmd
177         system(cmd)
178         return
179     end
180
181     w = Gtk::Window.new
182
183     msg 3, "filename: #{filename}"
184     dest_img = build_full_dest_filename(filename).sub(/\.[^\.]+$/, '') + "-#{$default_size['fullscreen']}.jpg"
185     #- typically this file won't exist in case of videos; try with the largest thumbnail around
186     if !File.exists?(dest_img)
187         if entry2type(filename) == 'video'
188             alternatives = Dir[build_full_dest_filename(filename).sub(/\.[^\.]+$/, '') + '-*'].sort
189             if not alternatives.empty?
190                 dest_img = alternatives[-1]
191             end
192         else
193             push_mousecursor_wait
194             gen_thumbnails_element(from_utf8("#{$current_path}/#{filename}"), $xmldir, false, [ { 'filename' => dest_img, 'size' => $default_size['fullscreen'] } ])
195             pop_mousecursor
196             if !File.exists?(dest_img)
197                 msg 2, _("Could not generate fullscreen thumbnail!")
198                 return
199                 end
200         end
201     end
202     evt = Gtk::EventBox.new.add(Gtk::Alignment.new(0.5, 0.5, 0, 0).add(Gtk::Frame.new.add(Gtk::Image.new(dest_img)).set_shadow_type(Gtk::SHADOW_ETCHED_OUT)))
203
204     tooltips = Gtk::Tooltips.new
205     tooltips.set_tip(evt, File.basename(filename).gsub(/\.jpg/, ''), nil)
206
207     bottom = Gtk::Alignment.new(0.5, 0.5, 0, 0).add(b = Gtk::Button.new(Gtk::Stock::CLOSE))
208     b.signal_connect('clicked') { w.destroy }
209
210     vb = Gtk::VBox.new
211     vb.pack_start(evt, false, false)
212     vb.pack_end(bottom, false, false)
213
214     w.add(vb)
215     w.signal_connect('delete-event') { w.destroy }
216     w.window_position = Gtk::Window::POS_CENTER
217     w.show_all
218 end
219
220 def create_editzone(scrolledwindow, pagenum, image)
221     frame = Gtk::Frame.new
222     frame.add(textview = Gtk::TextView.new.set_wrap_mode(Gtk::TextTag::WRAP_WORD))
223     frame.set_shadow_type(Gtk::SHADOW_IN)
224     textview.signal_connect('key-press-event') { |w, event|
225         textview.set_editable(event.keyval != Gdk::Keyval::GDK_Tab)
226         if event.keyval == Gdk::Keyval::GDK_Page_Up || event.keyval == Gdk::Keyval::GDK_Page_Down
227             scrolledwindow.signal_emit('key-press-event', event)
228         end
229         if (event.keyval == Gdk::Keyval::GDK_Up || event.keyval == Gdk::Keyval::GDK_Down) &&
230            event.state & (Gdk::Window::CONTROL_MASK | Gdk::Window::SHIFT_MASK | Gdk::Window::MOD1_MASK) == 0
231             if event.keyval == Gdk::Keyval::GDK_Up
232                 if scrolledwindow.vadjustment.value >= scrolledwindow.vadjustment.lower + scrolledwindow.vadjustment.step_increment
233                     scrolledwindow.vadjustment.value -= scrolledwindow.vadjustment.step_increment
234                 else
235                     scrolledwindow.vadjustment.value = scrolledwindow.vadjustment.lower
236                 end
237             else
238                 if scrolledwindow.vadjustment.value <= scrolledwindow.vadjustment.upper - scrolledwindow.vadjustment.step_increment - scrolledwindow.vadjustment.page_size
239                     scrolledwindow.vadjustment.value += scrolledwindow.vadjustment.step_increment
240                 else
241                     scrolledwindow.vadjustment.value = scrolledwindow.vadjustment.upper - scrolledwindow.vadjustment.page_size
242                 end
243             end
244         end
245         false  #- propagate
246     }
247     textview.signal_connect('focus-in-event') { |w, event|
248         textview.buffer.select_range(textview.buffer.get_iter_at_offset(0), textview.buffer.get_iter_at_offset(-1))
249         false  #- propagate
250     }
251
252     candidate_undo_text = nil
253     textview.signal_connect('focus-in-event') { |w, event|
254         candidate_undo_text = textview.buffer.text
255         false  #- propagate
256     }
257     textview.signal_connect('key-release-event') { |w, event|
258         if candidate_undo_text && candidate_undo_text != textview.buffer.text
259             $modified = true
260             save_undo(_("text edit"),
261                       Proc.new { |text|
262                           save_text = textview.buffer.text
263                           textview.buffer.text = text
264                           textview.grab_focus
265                           $notebook.set_page(pagenum)
266                           Proc.new {
267                               textview.buffer.text = save_text
268                               textview.grab_focus
269                               $notebook.set_page(pagenum)
270                           }
271                       }, candidate_undo_text)
272             candidate_undo_text = nil
273         end
274
275         if ![Gdk::Keyval::GDK_Page_Up, Gdk::Keyval::GDK_Page_Down, Gdk::Keyval::GDK_Up, Gdk::Keyval::GDK_Down].include?(event.keyval)
276             #- autoscroll if cursor or image is not visible
277             ypos_top = (image && image.window) ? image.window.position[1] : textview.window.position[1]
278             ypos_bottom = max(textview.window.position[1] + textview.window.size[1], image && image.window ? image.window.position[1] + image.window.size[1] : -1)
279             current_miny_visible = scrolledwindow.vadjustment.value
280             current_maxy_visible = scrolledwindow.vadjustment.value + scrolledwindow.vadjustment.page_size
281             if ypos_top < current_miny_visible
282                 newval = scrolledwindow.vadjustment.value -
283                          ((current_miny_visible - ypos_top - 1) / scrolledwindow.vadjustment.step_increment + 1) * scrolledwindow.vadjustment.step_increment
284                 if newval < scrolledwindow.vadjustment.lower
285                     newval = scrolledwindow.vadjustment.lower
286                 end
287                 scrolledwindow.vadjustment.value = newval
288             elsif ypos_bottom > current_maxy_visible
289                 newval = scrolledwindow.vadjustment.value +
290                          ((ypos_bottom - current_maxy_visible - 1) / scrolledwindow.vadjustment.step_increment + 1) * scrolledwindow.vadjustment.step_increment
291                 if newval > scrolledwindow.vadjustment.upper - scrolledwindow.vadjustment.page_size
292                     newval = scrolledwindow.vadjustment.upper - scrolledwindow.vadjustment.page_size
293                 end
294                 scrolledwindow.vadjustment.value = newval
295             end
296         end
297         false  #- propagate
298     }
299
300     return [ frame, textview ]
301 end
302
303 def update_shown_pixbuf(thumbnail_img, img, desired_x, desired_y)
304
305     if !$modified_pixbufs[thumbnail_img]
306         $modified_pixbufs[thumbnail_img] = { :orig => img.pixbuf }
307     elsif !$modified_pixbufs[thumbnail_img][:orig]
308         $modified_pixbufs[thumbnail_img][:orig] = img.pixbuf
309     end
310
311     pixbuf = $modified_pixbufs[thumbnail_img][:orig].dup
312
313     #- rotate
314     if $modified_pixbufs[thumbnail_img][:angle_to_orig] && $modified_pixbufs[thumbnail_img][:angle_to_orig] != 0
315         pixbuf = rotate_pixbuf(pixbuf, $modified_pixbufs[thumbnail_img][:angle_to_orig])
316         msg 3, "sizes: #{pixbuf.width} #{pixbuf.height} - desired #{desired_x}x#{desired_x}"
317         if pixbuf.height > desired_y
318             pixbuf = pixbuf.scale(pixbuf.width * (desired_y.to_f/pixbuf.height), desired_y, Gdk::Pixbuf::INTERP_BILINEAR)
319         elsif pixbuf.width < desired_x && pixbuf.height < desired_y
320             pixbuf = pixbuf.scale(desired_x, pixbuf.height * (desired_x.to_f/pixbuf.width), Gdk::Pixbuf::INTERP_BILINEAR)
321         end
322     end
323
324     #- fix white balance
325     if $modified_pixbufs[thumbnail_img][:whitebalance]
326         pixbuf.whitebalance!($modified_pixbufs[thumbnail_img][:whitebalance])
327     end
328
329     img.pixbuf = $modified_pixbufs[thumbnail_img][:pixbuf] = pixbuf
330 end
331
332 def rotate_real(angle, thumbnail_img, img, xmlelem, attributes_prefix, desired_x, desired_y)
333     $modified = true
334
335     #- update rotate attribute
336     xmlelem.add_attribute("#{attributes_prefix}rotate", (xmlelem.attributes["#{attributes_prefix}rotate"].to_i + angle) % 360)
337
338     $modified_pixbufs[thumbnail_img] ||= {}
339     $modified_pixbufs[thumbnail_img][:angle_to_orig] = (($modified_pixbufs[thumbnail_img][:angle_to_orig] || 0) + angle) % 360
340     msg 3, "angle: #{angle}, angle to orig: #{$modified_pixbufs[thumbnail_img][:angle_to_orig]}"
341
342     update_shown_pixbuf(thumbnail_img, img, desired_x, desired_y)
343 end
344
345 def rotate(angle, thumbnail_img, img, xmlelem, attributes_prefix, desired_x, desired_y)
346     $modified = true
347
348     rotate_real(angle, thumbnail_img, img, xmlelem, attributes_prefix, desired_x, desired_y)
349
350     save_undo(angle == 90 ? _("rotate clockwise") : angle == -90 ? _("rotate counter-clockwise") : _("flip upside-down"),
351               Proc.new { |angle|
352                   rotate_real(angle, thumbnail_img, img, xmlelem, attributes_prefix, desired_x, desired_y)
353                   $notebook.set_page(attributes_prefix != '' ? 0 : 1)
354                   Proc.new {
355                       rotate_real(-angle, thumbnail_img, img, xmlelem, attributes_prefix, desired_x, desired_y)
356                       $notebook.set_page(0)
357                       $notebook.set_page(attributes_prefix != '' ? 0 : 1)
358                   }
359               }, -angle)
360 end
361
362 def color_swap(xmldir, attributes_prefix)
363     $modified = true
364     if xmldir.attributes["#{attributes_prefix}color-swap"]
365         xmldir.delete_attribute("#{attributes_prefix}color-swap")
366     else
367         xmldir.add_attribute("#{attributes_prefix}color-swap", '1')
368     end
369 end
370
371 def enhance(xmldir, attributes_prefix)
372     $modified = true
373     if xmldir.attributes["#{attributes_prefix}enhance"]
374         xmldir.delete_attribute("#{attributes_prefix}enhance")
375     else
376         xmldir.add_attribute("#{attributes_prefix}enhance", '1')
377     end
378 end
379
380 def change_frame_offset(xmldir, attributes_prefix, value)
381     $modified = true
382     xmldir.add_attribute("#{attributes_prefix}frame-offset", value)
383 end
384
385 def ask_new_frame_offset(xmldir, attributes_prefix)
386     value = xmldir.attributes["#{attributes_prefix}frame-offset"]
387
388     dialog = Gtk::Dialog.new(utf8(_("Change frame offset")),
389                              $main_window,
390                              Gtk::Dialog::MODAL | Gtk::Dialog::DESTROY_WITH_PARENT,
391                              [Gtk::Stock::OK, Gtk::Dialog::RESPONSE_OK],
392                              [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
393
394     lbl = Gtk::Label.new
395     lbl.markup = utf8(
396 _("Please specify the <b>frame offset</b> of the video, to take the thumbnail
397 from. There are approximately 25 frames per second in a video.
398 "))
399     dialog.vbox.add(lbl)
400     dialog.vbox.add(entry = Gtk::Entry.new.set_text(value))
401     entry.signal_connect('key-press-event') { |w, event|
402         if event.keyval == Gdk::Keyval::GDK_Return
403             dialog.response(Gtk::Dialog::RESPONSE_OK)
404             true
405         elsif event.keyval == Gdk::Keyval::GDK_Escape
406             dialog.response(Gtk::Dialog::RESPONSE_CANCEL)
407             true
408         else
409             false  #- propagate if needed
410         end
411     }
412     
413     dialog.window_position = Gtk::Window::POS_MOUSE
414     dialog.show_all
415
416     dialog.run { |response|
417         newval = entry.text
418         dialog.destroy
419         if response == Gtk::Dialog::RESPONSE_OK
420             $modified = true
421             msg 3, "changing frame offset top #{newval}"
422             return { :old => value, :new => newval }
423         else
424             return nil
425         end
426     }
427 end
428
429 def change_whitebalance(xmlelem, attributes_prefix, value)
430     $modified = true
431     xmlelem.add_attribute("#{attributes_prefix}white-balance", value)
432 end
433
434 def recalc_whitebalance(level, orig, thumbnail_img, img, xmlelem, attributes_prefix, desired_x, desired_y, infotype)
435
436     #- in case the white balance was already modified in the config file and thumbnail, we cannot just revert, we need to use original file
437     if (!$modified_pixbufs[thumbnail_img] || !$modified_pixbufs[thumbnail_img][:whitebalance]) && xmlelem.attributes["#{attributes_prefix}white-balance"]
438         save_whitebalance = xmlelem.attributes["#{attributes_prefix}white-balance"]
439         xmlelem.delete_attribute("#{attributes_prefix}white-balance")
440         destfile = "#{thumbnail_img}-orig-whitebalance.jpg"
441         gen_real_thumbnail_core(attributes_prefix == '' ? 'element' : 'subdir', orig, destfile,
442                                 xmlelem, attributes_prefix == '' ? $default_size['thumbnails'] : $albums_thumbnail_size, infotype)
443         $modified_pixbufs[thumbnail_img] ||= {}
444         $modified_pixbufs[thumbnail_img][:orig] = pixbuf_or_nil(destfile)
445         system("rm -f '#{destfile}'")
446         xmlelem.add_attribute("#{attributes_prefix}white-balance", save_whitebalance)
447         $modified_pixbufs[thumbnail_img][:angle_to_orig] = 0
448         if entry2type(orig) == 'video'
449             #- cleanup temp for videos
450             system("rm -f #{current_dest_dir}/screenshot.jpg000000.jpg")
451         end
452     end
453
454     $modified_pixbufs[thumbnail_img] ||= {}
455     $modified_pixbufs[thumbnail_img][:whitebalance] = level.to_f
456
457     update_shown_pixbuf(thumbnail_img, img, desired_x, desired_y)
458 end
459
460 def ask_whitebalance(orig, thumbnail_img, img_, xmlelem, attributes_prefix, desired_x, desired_y, infotype)
461     #- init $modified_pixbufs correctly
462 #    update_shown_pixbuf(thumbnail_img, img_, desired_x, desired_y)
463
464     value = xmlelem.attributes["#{attributes_prefix}white-balance"] || "0"
465
466     dialog = Gtk::Dialog.new(utf8(_("Fix white balance")),
467                              $main_window,
468                              Gtk::Dialog::MODAL | Gtk::Dialog::DESTROY_WITH_PARENT,
469                              [Gtk::Stock::OK, Gtk::Dialog::RESPONSE_OK],
470                              [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
471
472     lbl = Gtk::Label.new
473     lbl.markup = utf8(
474 _("You can fix the <b>white balance</b> of the image, if your image is too blue
475 or too yellow because your camera didn't detect the light correctly. Drag the
476 slider below the image to the left for more blue, to the right for more yellow.
477 "))
478     dialog.vbox.add(lbl)
479     dialog.vbox.add(evt = Gtk::EventBox.new.add(img = Gtk::Image.new(img_.pixbuf)))
480     dialog.vbox.add(hs = Gtk::HScale.new(-200, 200, 1).set_value(value.to_i))
481     
482     dialog.window_position = Gtk::Window::POS_MOUSE
483     dialog.show_all
484
485     lastval = nil
486     thread = nil
487     timeout = Gtk.timeout_add(100) {
488         if hs.value != lastval
489             lastval = hs.value
490             if thread
491                 thread.kill
492             end
493             thread = Thread.new {
494                 recalc_whitebalance(lastval, orig, thumbnail_img, img, xmlelem, attributes_prefix, desired_x, desired_y, infotype)
495             }
496         end
497         true
498     }
499
500     dialog.run { |response|
501         Gtk.timeout_remove(timeout)
502         if response == Gtk::Dialog::RESPONSE_OK
503             $modified = true
504             newval = hs.value.to_s
505             msg 3, "changing white balance to #{newval}"
506             dialog.destroy
507             return { :old => value, :new => newval }
508         else
509             $modified_pixbufs[thumbnail_img] ||= {}
510             $modified_pixbufs[thumbnail_img][:whitebalance] = value.to_f
511             dialog.destroy
512             return nil
513         end
514     }
515 end
516
517 def gen_real_thumbnail_core(type, origfile, destfile, xmldir, size, infotype)
518     system("rm -f '#{destfile}'")
519     #- type can be 'element' or 'subdir'
520     if type == 'element'
521         gen_thumbnails_element(origfile, xmldir, false, [ { 'filename' => destfile, 'size' => size } ])
522     else
523         gen_thumbnails_subdir(origfile, xmldir, false, [ { 'filename' => destfile, 'size' => size } ], infotype)
524     end
525 end
526
527 def gen_real_thumbnail(type, origfile, destfile, xmldir, size, img, infotype)
528     Thread.new {
529         push_mousecursor_wait
530         gen_real_thumbnail_core(type, origfile, destfile, xmldir, size, infotype)
531         img.set(destfile)
532         $modified_pixbufs[destfile] = { :orig => img.pixbuf, :pixbuf => img.pixbuf, :angle_to_orig => 0 }
533         if entry2type(origfile) == 'video'
534             #- cleanup temp for videos
535             system("rm -f #{current_dest_dir}/screenshot.jpg000000.jpg")
536         end
537         pop_mousecursor
538     }
539 end
540
541 def popup_thumbnail_menu(event, optionals, type, xmldir, attributes_prefix, closures)
542     menu = Gtk::Menu.new
543     if optionals.include?('change_image')
544         menu.append(changeimg = Gtk::ImageMenuItem.new(utf8(_("Change image"))))
545         changeimg.image = Gtk::Image.new("#{$FPATH}/images/stock-images-16.png")
546         changeimg.signal_connect('activate') { closures[:change].call }
547         menu.append(            Gtk::SeparatorMenuItem.new)
548     end
549     menu.append(r90 = Gtk::ImageMenuItem.new(utf8(_("Rotate clockwise"))))
550     r90.image = Gtk::Image.new("#{$FPATH}/images/stock-rotate-90-16.png")
551     r90.signal_connect('activate') { closures[:rotate].call(90) }
552     menu.append(r270 = Gtk::ImageMenuItem.new(utf8(_("Rotate counter-clockwise"))))
553     r270.image = Gtk::Image.new("#{$FPATH}/images/stock-rotate-270-16.png")
554     r270.signal_connect('activate') { closures[:rotate].call(-90) }
555     menu.append(whitebalance = Gtk::ImageMenuItem.new(utf8(_("Fix white-balance"))))
556     whitebalance.image = Gtk::Image.new("#{$FPATH}/images/stock-tool-color-balance-16.png")
557     whitebalance.signal_connect('activate') { closures[:whitebalance].call }
558     if type == 'video'
559         menu.append(               Gtk::SeparatorMenuItem.new)
560         menu.append(  color_swap = Gtk::ImageMenuItem.new(utf8(_("Red/blue color swap"))))
561         color_swap.image = Gtk::Image.new("#{$FPATH}/images/stock-color-triangle-16.png")
562         color_swap.signal_connect('activate') { closures[:color_swap].call }
563         menu.append(        flip = Gtk::ImageMenuItem.new(utf8(_("Flip upside-down"))))
564         flip.image = Gtk::Image.new("#{$FPATH}/images/stock-rotate-180-16.png")
565         flip.signal_connect('activate') { closures[:rotate].call(180) }
566         menu.append(frame_offset = Gtk::ImageMenuItem.new(utf8(_("Specify frame offset"))))
567         frame_offset.image = Gtk::Image.new("#{$FPATH}/images/stock-video-16.png")
568         frame_offset.signal_connect('activate') { closures[:frame_offset].call }
569     end
570     menu.append(               Gtk::SeparatorMenuItem.new)
571     menu.append(enhance      = Gtk::ImageMenuItem.new(utf8(xmldir.attributes["#{attributes_prefix}enhance"] ? _("Original contrast") :
572                                                                                                               _("Enhance constrast"))))
573     enhance.image = Gtk::Image.new("#{$FPATH}/images/stock-channels-16.png")
574     enhance.signal_connect('activate') { closures[:enhance].call }
575     if optionals.include?('delete')
576         menu.append(               Gtk::SeparatorMenuItem.new)
577         menu.append(delete_item  = Gtk::ImageMenuItem.new(Gtk::Stock::DELETE))
578         delete_item.signal_connect('activate') { closures[:delete].call }
579     end
580     menu.show_all
581     menu.popup(nil, nil, event.button, event.time)
582 end
583
584 def add_thumbnail(autotable, filename, type, thumbnail_img, caption)
585
586     img = nil
587     frame1 = Gtk::Frame.new
588
589     my_gen_real_thumbnail = proc {
590         gen_real_thumbnail('element', from_utf8("#{$current_path}/#{filename}"), thumbnail_img, $xmldir, $default_size['thumbnails'], img, '')
591     }
592
593     #- generate the thumbnail if missing (if image was rotated but booh was not relaunched)
594     if !$modified_pixbufs[thumbnail_img] && !File.exists?(thumbnail_img)
595         frame1.add(img = Gtk::Image.new)
596         my_gen_real_thumbnail.call
597     else
598         frame1.add(img = Gtk::Image.new($modified_pixbufs[thumbnail_img] ? $modified_pixbufs[thumbnail_img][:pixbuf] : thumbnail_img))
599     end
600     evtbox = Gtk::EventBox.new.add(Gtk::Alignment.new(0.5, 0.5, 0, 0).add(frame1.set_shadow_type(Gtk::SHADOW_ETCHED_OUT)))
601
602     tooltips = Gtk::Tooltips.new
603     tipname = from_utf8(File.basename(filename).gsub(/\.[^\.]+$/, ''))
604     tooltips.set_tip(evtbox, utf8(type == 'video' ? (_("%s (video - %s KB)") % [tipname, commify(file_size(from_utf8("#{$current_path}/#{filename}"))/1024)]) : tipname), nil)
605
606     frame2, textview = create_editzone($autotable_sw, 1, img)
607     textview.buffer.text = utf8(caption)
608     textview.set_justification(Gtk::Justification::CENTER)
609
610     vbox = Gtk::VBox.new(false, 5)
611     vbox.pack_start(evtbox, false, false)
612     vbox.pack_start(frame2, false, false)
613     autotable.append(vbox, filename)
614
615     #- to be able to grab focus of textview when retrieving vbox's position from AutoTable
616     $vbox2textview[vbox] = textview
617
618     #- to be able to find widgets by name
619     $name2widgets[filename] = { :textview => textview }
620
621     cleanup_all_thumbnails = Proc.new {
622         #- remove out of sync images
623         dest_img_base = build_full_dest_filename(filename).sub(/\.[^\.]+$/, '')
624         for sizeobj in $images_size
625             system("rm -f #{dest_img_base}-#{sizeobj['fullscreen']}.jpg #{dest_img_base}-#{sizeobj['thumbnails']}.jpg")
626         end
627
628     }
629
630     rotate_and_cleanup = Proc.new { |angle|
631         rotate(angle, thumbnail_img, img, $xmldir.elements["[@filename='#{filename}']"], '', $default_thumbnails[:x], $default_thumbnails[:y])
632         cleanup_all_thumbnails.call
633     }
634
635     color_swap_and_cleanup = Proc.new {
636         perform_color_swap_and_cleanup = Proc.new {
637             color_swap($xmldir.elements["[@filename='#{filename}']"], '')
638             my_gen_real_thumbnail.call
639         }
640
641         cleanup_all_thumbnails.call
642         perform_color_swap_and_cleanup.call
643
644         save_undo(_("color swap"),
645                   Proc.new {
646                       perform_color_swap_and_cleanup.call
647                       $notebook.set_page(1)
648                       Proc.new {
649                           perform_color_swap_and_cleanup.call
650                           $notebook.set_page(1)
651                       }
652                   })
653     }
654
655     change_frame_offset_and_cleanup = Proc.new {
656         if values = ask_new_frame_offset($xmldir.elements["[@filename='#{filename}']"], '')
657             perform_change_frame_offset_and_cleanup = Proc.new { |val|
658                 change_frame_offset($xmldir.elements["[@filename='#{filename}']"], '', val)
659                 my_gen_real_thumbnail.call
660             }
661             perform_change_frame_offset_and_cleanup.call(values[:new])
662
663             save_undo(_("specify frame offset"),
664                       Proc.new {
665                           perform_change_frame_offset_and_cleanup.call(values[:old])
666                           $notebook.set_page(1)
667                           Proc.new {
668                               perform_change_frame_offset_and_cleanup.call(values[:new])
669                               $notebook.set_page(1)
670                           }
671                       })
672         end
673     }
674
675     whitebalance_and_cleanup = Proc.new {
676         if values = ask_whitebalance(from_utf8("#{$current_path}/#{filename}"), thumbnail_img, img,
677                                      $xmldir.elements["[@filename='#{filename}']"], '', $default_thumbnails[:x], $default_thumbnails[:y], '')
678             perform_change_whitebalance_and_cleanup = Proc.new { |val|
679                 change_whitebalance($xmldir.elements["[@filename='#{filename}']"], '', val)
680                 recalc_whitebalance(val, from_utf8("#{$current_path}/#{filename}"), thumbnail_img, img,
681                                     $xmldir.elements["[@filename='#{filename}']"], '', $default_thumbnails[:x], $default_thumbnails[:y], '')
682                 cleanup_all_thumbnails.call
683             }
684             perform_change_whitebalance_and_cleanup.call(values[:new])
685
686             save_undo(_("fix white balance"),
687                       Proc.new {
688                           perform_change_whitebalance_and_cleanup.call(values[:old])
689                           $notebook.set_page(1)
690                           Proc.new {
691                               perform_change_whitebalance_and_cleanup.call(values[:new])
692                               $notebook.set_page(1)
693                           }
694                       })
695         end
696     }
697
698     enhance_and_cleanup = Proc.new {
699         perform_enhance_and_cleanup = Proc.new {
700             enhance($xmldir.elements["[@filename='#{filename}']"], '')
701             my_gen_real_thumbnail.call
702         }
703
704         cleanup_all_thumbnails.call
705         perform_enhance_and_cleanup.call
706
707         save_undo(_("enhance"),
708                   Proc.new {
709                       perform_enhance_and_cleanup.call
710                       $notebook.set_page(1)
711                       Proc.new {
712                           perform_enhance_and_cleanup.call
713                           $notebook.set_page(1)
714                       }
715                   })
716     }
717
718     delete = Proc.new {
719         if autotable.current_order.size > 1 || show_popup($main_window, utf8(_("Do you confirm this subalbum needs to be completely removed?")), { :okcancel => true })
720             $modified = true
721             after = nil
722             perform_delete = Proc.new {
723                 after = autotable.get_next_widget(vbox)
724                 if !after
725                     after = autotable.get_previous_widget(vbox)
726                 end
727                 autotable.remove(vbox)
728                 if after
729                     $vbox2textview[after].grab_focus
730                 end
731             }
732             
733             perform_delete.call
734
735             if !after
736                 if $xmldir.elements['dir']
737                     $xmldir.delete_attribute('thumbnails-caption')
738                     $xmldir.delete_attribute('thumbnails-captionfile')
739                 else
740                     $xmldir.remove
741                 end
742                 save_changes('forced')
743                 populate_subalbums_treeview
744             else
745                 save_undo(_("delete"),
746                           Proc.new { |pos|
747                               autotable.reinsert(pos, vbox, filename)
748                               $notebook.set_page(1)
749                               Proc.new {
750                                   perform_delete.call
751                                   $notebook.set_page(1)
752                               }
753                           }, autotable.get_current_number(vbox))
754             end
755         end
756     }
757
758     textview.signal_connect('key-press-event') { |w, event|
759         propagate = true
760         if event.state != 0
761             x, y = autotable.get_current_pos(vbox)
762             control_pressed = event.state & Gdk::Window::CONTROL_MASK != 0
763             shift_pressed = event.state & Gdk::Window::SHIFT_MASK != 0
764             alt_pressed = event.state & Gdk::Window::MOD1_MASK != 0
765             if event.keyval == Gdk::Keyval::GDK_Up && y > 0
766                 if control_pressed
767                     $vbox2textview[autotable.get_widget_at_pos(x, y - 1)].grab_focus
768                 end
769                 if shift_pressed
770                     autotable.move_up(vbox)
771                     textview.grab_focus  #- because if moving, focus is stolen
772                 end
773             end
774             if event.keyval == Gdk::Keyval::GDK_Down && y < autotable.get_max_y
775                 if control_pressed
776                     $vbox2textview[autotable.get_widget_at_pos(x, y + 1)].grab_focus
777                 end
778                 if shift_pressed
779                     autotable.move_down(vbox)
780                     textview.grab_focus  #- because if moving, focus is stolen
781                 end
782             end
783             if event.keyval == Gdk::Keyval::GDK_Left
784                 previous = autotable.get_previous_widget(vbox)
785                 if previous && autotable.get_current_pos(previous)[0] < x
786                     if control_pressed
787                         $vbox2textview[previous].grab_focus
788                     end
789                     if shift_pressed
790                         autotable.move_left(vbox)
791                         textview.grab_focus  #- because if moving, focus is stolen
792                     end
793                 end
794                 if alt_pressed
795                     rotate_and_cleanup.call(-90)
796                 end
797             end
798             if event.keyval == Gdk::Keyval::GDK_Right
799                 next_ = autotable.get_next_widget(vbox)
800                 if next_ && autotable.get_current_pos(next_)[0] > x
801                     if control_pressed
802                         $vbox2textview[next_].grab_focus
803                     end
804                     if shift_pressed
805                         autotable.move_right(vbox)
806                         textview.grab_focus  #- because if moving, focus is stolen
807                     end
808                 end
809                 if alt_pressed
810                     rotate_and_cleanup.call(90)
811                 end
812             end
813             if event.keyval == Gdk::Keyval::GDK_Delete && control_pressed
814                 delete.call
815             end
816             if event.keyval == Gdk::Keyval::GDK_Return && control_pressed
817                 view_element(filename)
818                 propagate = false
819             end
820         end
821         !propagate  #- propagate if needed
822     }
823
824     evtbox.signal_connect('button-press-event') { |w, event|
825         retval = true
826         if event.event_type == Gdk::Event::BUTTON_PRESS && event.button == 1
827             shift_or_control = event.state & Gdk::Window::SHIFT_MASK != 0 || event.state & Gdk::Window::CONTROL_MASK != 0
828             if $r90.active?
829                 rotate_and_cleanup.call(shift_or_control ? -90 : 90)
830             elsif $r270.active?
831                 rotate_and_cleanup.call(shift_or_control ? 90 : -90)
832             elsif $enhance.active?
833                 enhance_and_cleanup.call
834             elsif $delete.active?
835                 delete.call
836             else
837                 textview.grab_focus
838             end
839         end
840         if event.event_type == Gdk::Event::BUTTON_PRESS && event.button == 3
841             popup_thumbnail_menu(event, ['delete'], type, $xmldir.elements["[@filename='#{filename}']"], '',
842                                  { :rotate => rotate_and_cleanup, :color_swap => color_swap_and_cleanup, :enhance => enhance_and_cleanup,
843                                    :frame_offset => change_frame_offset_and_cleanup, :delete => delete, :whitebalance => whitebalance_and_cleanup })
844         end
845         if event.event_type == Gdk::Event::BUTTON2_PRESS && event.button == 1
846             view_element(filename)
847         else
848             retval = false  #- propagate
849         end
850         retval
851     }
852
853     vbox.signal_connect('button-press-event') { |w, event|
854         if event.event_type == Gdk::Event::BUTTON_PRESS && event.button == 1
855             $gesture_press = { :filename => filename, :x => event.x, :y => event.y }
856         end
857         false
858     }
859     vbox.signal_connect('drag-data-received') { |w, ctxt, x, y, selection_data, info, time|
860         if $gesture_press && $gesture_press[:filename] == filename
861             if (($gesture_press[:x]-x)/($gesture_press[:y]-y)).abs > 2 && ($gesture_press[:x]-x).abs > 5
862                 angle = x-$gesture_press[:x] > 0 ? 90 : -90
863                 msg 3, "gesture rotate: #{angle}"
864                 rotate_and_cleanup.call(angle)
865             end
866         end
867         $gesture_press = nil
868     }
869
870     #- handle reordering with drag and drop
871     Gtk::Drag.source_set(vbox, Gdk::Window::BUTTON1_MASK, [['reorder-elements', Gtk::Drag::TARGET_SAME_APP, 1]], Gdk::DragContext::ACTION_MOVE)
872     Gtk::Drag.dest_set(vbox, Gtk::Drag::DEST_DEFAULT_ALL, [['reorder-elements', Gtk::Drag::TARGET_SAME_APP, 1]], Gdk::DragContext::ACTION_MOVE)
873     vbox.signal_connect('drag-data-get') { |w, ctxt, selection_data, info, time|
874         selection_data.set(Gdk::Selection::TYPE_STRING, autotable.get_current_number(vbox).to_s)
875     }
876     vbox.signal_connect('drag-data-received') { |w, ctxt, x, y, selection_data, info, time|
877         ctxt.targets.each { |target|
878             if target.name == 'reorder-elements'
879                 from, to = selection_data.data.to_i, autotable.get_current_number(vbox)
880                 if from != to
881                     $modified = true
882                     autotable.move(from, to)
883                     save_undo(_("reorder"),
884                               Proc.new { |from, to|
885                                   if to > from
886                                       autotable.move(to - 1, from)
887                                   else
888                                       autotable.move(to, from + 1)
889                                   end
890                                   $notebook.set_page(1)
891                                   Proc.new {
892                                       autotable.move(from, to)
893                                       $notebook.set_page(1)
894                                   }
895                               }, from, to)
896                 end
897             end
898         }
899     }
900
901     vbox.show_all
902 end
903
904 def create_auto_table
905
906     $autotable = Gtk::AutoTable.new(5)
907
908     $autotable_sw = Gtk::ScrolledWindow.new(nil, nil)
909     $autotable_sw.set_policy(Gtk::POLICY_NEVER, Gtk::POLICY_ALWAYS)
910     $autotable_sw.add_with_viewport($autotable)
911 end
912
913 def create_subalbums_page
914
915     subalbums_hb = Gtk::HBox.new
916     $subalbums_vb = Gtk::VBox.new(false, 5)
917     subalbums_hb.pack_start($subalbums_vb, false, false)
918     $subalbums_sw = Gtk::ScrolledWindow.new(nil, nil)
919     $subalbums_sw.set_policy(Gtk::POLICY_AUTOMATIC, Gtk::POLICY_AUTOMATIC)
920     $subalbums_sw.add_with_viewport(subalbums_hb)
921 end
922
923 def save_current_file
924     save_changes
925     if $filename
926         ios = File.open($filename, "w")
927         $xmldoc.write(ios, 0)
928         ios.close
929         $modified = false
930     end
931 end
932
933 def try_quit
934     if $modified
935         dialog = Gtk::Dialog.new(utf8(_("Save before quitting?")),
936                                  $main_window,
937                                  Gtk::Dialog::MODAL | Gtk::Dialog::DESTROY_WITH_PARENT,
938                                  [Gtk::Stock::CLOSE, Gtk::Dialog::RESPONSE_NO],
939                                  [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL],
940                                  [Gtk::Stock::SAVE, Gtk::Dialog::RESPONSE_YES])
941         dialog.vbox.add(Gtk::Label.new(utf8(_("Do you want to save your changes before quitting?"))))
942         dialog.window_position = Gtk::Window::POS_CENTER
943         dialog.show_all
944         
945         dialog.run { |response|
946             dialog.destroy
947             if response == Gtk::Dialog::RESPONSE_CANCEL
948                 return
949             elsif response == Gtk::Dialog::RESPONSE_YES
950                 save_current_file
951             end
952         }
953     end
954
955     Gtk.main_quit
956 end
957
958 def show_popup(parent, msg, *options)
959     dialog = Gtk::Dialog.new
960     dialog.title = utf8(_("Booh message"))
961     lbl = Gtk::Label.new
962     lbl.markup = msg
963     if options[0] && options[0][:centered]
964         lbl.set_justify(Gtk::Justification::CENTER)
965     end
966     dialog.vbox.add(lbl)
967     if options[0] && options[0][:okcancel]
968         dialog.add_button(Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL)
969     end
970     dialog.add_button(Gtk::Stock::OK, Gtk::Dialog::RESPONSE_OK)
971
972     dialog.transient_for = parent
973     dialog.set_default_size(200, 120)
974     if options[0] && options[0][:pos_centered]
975         dialog.window_position = Gtk::Window::POS_CENTER
976     else
977         dialog.window_position = Gtk::Window::POS_MOUSE
978     end
979     dialog.show_all
980
981     dialog.run { |response|
982         dialog.destroy
983         if options[0] && options[0][:okcancel]
984             return response == Gtk::Dialog::RESPONSE_OK
985         end
986     }
987 end
988
989 def wait_message(parent, msg)
990     w = Gtk::Window.new
991     w.set_transient_for(parent)
992     w.modal = true
993
994     vb = Gtk::VBox.new(false, 5)
995     vb.set_border_width(5)
996     vb.pack_start(Gtk::Label.new(msg), false, false)
997     bottom = Gtk::Alignment.new(0.5, 0.5, 0, 0).add(b = Gtk::Button.new(utf8(_("_Abort"))))
998     b.image = Gtk::Image.new("#{$FPATH}/images/stock-close-24.png")
999     vb.pack_start(pb = Gtk::ProgressBar.new.set_pulse_step(0.05), false, false)
1000     vb.pack_start(Gtk::HSeparator.new, false, false)
1001     vb.pack_end(bottom, false, false)
1002
1003     timeout = Gtk.timeout_add(200) { pb.pulse }
1004
1005     w.add(vb)
1006     w.signal_connect('delete-event') { w.destroy }
1007     w.signal_connect('destroy') { Gtk.timeout_remove(timeout) }
1008     w.window_position = Gtk::Window::POS_CENTER
1009     w.show_all
1010
1011     return [ b, w ]
1012 end
1013
1014 def perform_in_background(cmd, waitmsg, params)
1015     button, w8 = wait_message($main_window, waitmsg)
1016     pid = nil
1017     Thread.new {
1018         msg 2, cmd
1019         if pid = fork
1020             id, exitstatus = Process.waitpid2(pid)
1021             w8.destroy
1022             if exitstatus == 0
1023                 if params[:successmsg]
1024                     show_popup($main_window, params[:successmsg])
1025                 end
1026                 if params[:closure_after]
1027                     params[:closure_after].call
1028                 end
1029             elsif exitstatus == 15
1030                 #- say nothing, user aborted
1031             else
1032                 if params[:failuremsg]
1033                     show_popup($main_window, params[:failuremsg])
1034                 end
1035             end
1036         else
1037             exec(cmd)
1038         end
1039     }
1040     button.signal_connect('clicked') {
1041         Process.kill('SIGTERM', pid)
1042     }
1043 end
1044
1045 def save_changes(*forced)
1046     if forced.empty? && (!$current_path || !$undo_tb.sensitive?)
1047         return
1048     end
1049
1050     if $xmldir.elements['dir']
1051         $xmldir.add_attribute('subdirs-caption', $subalbums_title.buffer.text)
1052         $xmldir.elements.each('dir') { |element|
1053             path = element.attributes['path']
1054             if element.attributes['subdirs-caption']
1055                 element.add_attribute('subdirs-caption',     $subalbums_edits[path][:editzone].buffer.text)
1056                 element.add_attribute('subdirs-captionfile', utf8($subalbums_edits[path][:captionfile]))
1057             else
1058                 element.add_attribute('thumbnails-caption',     $subalbums_edits[path][:editzone].buffer.text)
1059                 element.add_attribute('thumbnails-captionfile', utf8($subalbums_edits[path][:captionfile]))
1060             end
1061         }
1062         if $xmldir.attributes['thumbnails-caption']
1063             path = $xmldir.attributes['path']
1064             $xmldir.add_attribute('thumbnails-caption', $subalbums_edits[path][:editzone].buffer.text)
1065         end
1066     end
1067
1068     #- remove and reinsert elements to reflect new ordering
1069     save_attributes = {}
1070     save_types = {}
1071     cpt = 0
1072     $xmldir.elements.each { |element|
1073         if element.name == 'image' || element.name == 'video'
1074             save_types[element.attributes['filename']] = element.name
1075             save_attributes[element.attributes['filename']] = element.attributes
1076             element.remove
1077             cpt += 1
1078         end
1079     }
1080     $autotable.current_order.each { |path|
1081         chld = $xmldir.add_element save_types[path], save_attributes[path]
1082         chld.add_attribute('caption', $name2widgets[File.basename(path)][:textview].buffer.text)
1083     }
1084 end
1085
1086 def change_dir
1087     $autotable.clear
1088     $vbox2textview = {}
1089     $name2widgets = {}
1090     UndoHandler.cleanup
1091     $undo_tb.sensitive = $undo_mb.sensitive = false
1092     $redo_tb.sensitive = $redo_mb.sensitive = false
1093
1094     if !$current_path
1095         return
1096     end
1097
1098     $subalbums_vb.children.each { |chld|
1099         $subalbums_vb.remove(chld)
1100     }
1101     $subalbums = Gtk::Table.new(0, 0, true)
1102     current_y_sub_albums = 0
1103
1104     $xmldir = $xmldoc.elements["//dir[@path='#{$current_path}']"]
1105     $subalbums_edits = {}
1106
1107     add_subalbum = Proc.new { |xmldir|
1108         if xmldir == $xmldir
1109             thumbnail_file = "#{current_dest_dir}/thumbnails-thumbnail.jpg"
1110             caption = xmldir.attributes['thumbnails-caption']
1111             captionfile, dummy = find_subalbum_caption_info(xmldir)
1112             infotype = 'thumbnails'
1113         else
1114             thumbnail_file = "#{current_dest_dir}/thumbnails-#{make_dest_filename(from_utf8(File.basename(xmldir.attributes['path'])))}.jpg"
1115             captionfile, caption = find_subalbum_caption_info(xmldir)
1116             infotype = find_subalbum_info_type(xmldir)
1117         end
1118         msg 3, "add subdir: #{xmldir.attributes['path']} with file: #{thumbnail_file}"
1119         hbox = Gtk::HBox.new
1120         hbox.pack_start(Gtk::Alignment.new(1, 0.5, 0, 0).add(Gtk::Label.new.set_markup('<i>' + File.basename(xmldir.attributes['path']) + ':</i>')))
1121         f = Gtk::Frame.new
1122         f.set_shadow_type(Gtk::SHADOW_ETCHED_OUT)
1123
1124         img = nil
1125         my_gen_real_thumbnail = proc {
1126             gen_real_thumbnail('subdir', captionfile, thumbnail_file, xmldir, $albums_thumbnail_size, img, infotype)
1127         }
1128
1129         if !$modified_pixbufs[thumbnail_file] && !File.exists?(thumbnail_file)
1130             f.add(img = Gtk::Image.new)
1131             my_gen_real_thumbnail.call
1132         else
1133             f.add(img = Gtk::Image.new($modified_pixbufs[thumbnail_file] ? $modified_pixbufs[thumbnail_file][:pixbuf] : thumbnail_file))
1134         end
1135         hbox.pack_end(Gtk::Alignment.new(0.5, 0.5, 0, 0).add(evtbox = Gtk::EventBox.new.add(f)), false, false)
1136         $subalbums.attach(hbox,
1137                           0, 1, current_y_sub_albums, current_y_sub_albums + 1, Gtk::FILL, Gtk::FILL, 2, 2)
1138
1139         change_image = Proc.new {
1140             fc = Gtk::FileChooserDialog.new(utf8(_("Select image for caption")),
1141                                             nil,
1142                                             Gtk::FileChooser::ACTION_OPEN,
1143                                             nil,
1144                                             [Gtk::Stock::OPEN, Gtk::Dialog::RESPONSE_ACCEPT], [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
1145             fc.set_current_folder(from_utf8(xmldir.attributes['path']))
1146             fc.transient_for = $main_window
1147             fc.preview_widget = preview = Gtk::Alignment.new(0.5, 0.5, 0, 0).add(f = Gtk::Frame.new.set_shadow_type(Gtk::SHADOW_ETCHED_OUT))
1148             f.add(preview_img = Gtk::Image.new)
1149             preview.show_all
1150             fc.signal_connect('update-preview') { |w|
1151                 begin
1152                     if fc.preview_filename
1153                         preview_img.pixbuf = rotate_pixbuf(Gdk::Pixbuf.new(fc.preview_filename, 240, 180), guess_rotate(fc.preview_filename))
1154                         fc.preview_widget_active = true
1155                     end
1156                 rescue Gdk::PixbufError
1157                     fc.preview_widget_active = false
1158                 end
1159             }
1160             if fc.run == Gtk::Dialog::RESPONSE_ACCEPT
1161                 $modified = true
1162                 old_file = captionfile
1163                 old_rotate = xmldir.attributes["#{infotype}-rotate"]
1164                 old_color_swap = xmldir.attributes["#{infotype}-color-swap"]
1165                 old_enhance = xmldir.attributes["#{infotype}-enhance"]
1166                 old_frame_offset = xmldir.attributes["#{infotype}-frame-offset"]
1167
1168                 new_file = fc.filename
1169                 msg 3, "new captionfile is: #{fc.filename}"
1170                 perform_changefile = Proc.new {
1171                     $subalbums_edits[xmldir.attributes['path']][:captionfile] = captionfile = new_file
1172                     $modified_pixbufs.delete(thumbnail_file)
1173                     xmldir.delete_attribute("#{infotype}-rotate")
1174                     xmldir.delete_attribute("#{infotype}-color-swap")
1175                     xmldir.delete_attribute("#{infotype}-enhance")
1176                     xmldir.delete_attribute("#{infotype}-frame-offset")
1177                     my_gen_real_thumbnail.call
1178                 }
1179                 perform_changefile.call
1180
1181                 save_undo(_("change caption file for sub-album"),
1182                           Proc.new {
1183                               $subalbums_edits[xmldir.attributes['path']][:captionfile] = captionfile = old_file
1184                               xmldir.add_attribute("#{infotype}-rotate", old_rotate)
1185                               xmldir.add_attribute("#{infotype}-color-swap", old_color_swap)
1186                               xmldir.add_attribute("#{infotype}-enhance", old_enhance)
1187                               xmldir.add_attribute("#{infotype}-frame-offset", old_frame_offset)
1188                               my_gen_real_thumbnail.call
1189                               $notebook.set_page(0)
1190                               Proc.new {
1191                                   perform_changefile.call
1192                                   $notebook.set_page(0)
1193                               }
1194                           })
1195             end
1196             fc.destroy
1197         }
1198
1199         rotate_and_cleanup = Proc.new { |angle|
1200             rotate(angle, thumbnail_file, img, xmldir, "#{infotype}-", $default_albums_thumbnails[:x], $default_albums_thumbnails[:y])
1201             system("rm -f '#{thumbnail_file}'")
1202         }
1203
1204         color_swap_and_cleanup = Proc.new {
1205             perform_color_swap_and_cleanup = Proc.new {
1206                 color_swap(xmldir, "#{infotype}-")
1207                 my_gen_real_thumbnail.call
1208             }
1209             perform_color_swap_and_cleanup.call
1210
1211             save_undo(_("color swap"),
1212                       Proc.new {
1213                           perform_color_swap_and_cleanup.call
1214                           $notebook.set_page(0)
1215                           Proc.new {
1216                               perform_color_swap_and_cleanup.call
1217                               $notebook.set_page(0)
1218                           }
1219                       })
1220         }
1221
1222         change_frame_offset_and_cleanup = Proc.new {
1223             if values = ask_new_frame_offset(xmldir, "#{infotype}-")
1224                 perform_change_frame_offset_and_cleanup = Proc.new { |val|
1225                     change_frame_offset(xmldir, "#{infotype}-", val)
1226                     my_gen_real_thumbnail.call
1227                 }
1228                 perform_change_frame_offset_and_cleanup.call(values[:new])
1229
1230                 save_undo(_("specify frame offset"),
1231                           Proc.new {
1232                               perform_change_frame_offset_and_cleanup.call(values[:old])
1233                               $notebook.set_page(0)
1234                               Proc.new {
1235                                   perform_change_frame_offset_and_cleanup.call(values[:new])
1236                                   $notebook.set_page(0)
1237                               }
1238                           })
1239             end
1240         }
1241
1242         whitebalance_and_cleanup = Proc.new {
1243             if values = ask_whitebalance(captionfile, thumbnail_file, img, xmldir, "#{infotype}-",
1244                                          $default_albums_thumbnails[:x], $default_albums_thumbnails[:y], infotype)
1245                 perform_change_whitebalance_and_cleanup = Proc.new { |val|
1246                     change_whitebalance(xmldir, "#{infotype}-", val)
1247                     recalc_whitebalance(val, captionfile, thumbnail_file, img, xmldir, "#{infotype}-",
1248                                         $default_albums_thumbnails[:x], $default_albums_thumbnails[:y], infotype)
1249                     system("rm -f '#{thumbnail_file}'")
1250                 }
1251                 perform_change_whitebalance_and_cleanup.call(values[:new])
1252                 
1253                 save_undo(_("fix white balance"),
1254                           Proc.new {
1255                               perform_change_whitebalance_and_cleanup.call(values[:old])
1256                               $notebook.set_page(0)
1257                               Proc.new {
1258                                   perform_change_whitebalance_and_cleanup.call(values[:new])
1259                                   $notebook.set_page(0)
1260                               }
1261                           })
1262             end
1263         }
1264
1265         enhance_and_cleanup = Proc.new {
1266             perform_enhance_and_cleanup = Proc.new {
1267                 enhance(xmldir, "#{infotype}-")
1268                 my_gen_real_thumbnail.call
1269             }
1270             
1271             perform_enhance_and_cleanup.call
1272             
1273             save_undo(_("enhance"),
1274                       Proc.new {
1275                           perform_enhance_and_cleanup.call
1276                           $notebook.set_page(0)
1277                           Proc.new {
1278                               perform_enhance_and_cleanup.call
1279                               $notebook.set_page(0)
1280                           }
1281                       })
1282         }
1283
1284         evtbox.signal_connect('button-press-event') { |w, event|
1285             if event.event_type == Gdk::Event::BUTTON_PRESS && event.button == 1
1286                 if $r90.active?
1287                     rotate_and_cleanup.call(90)
1288                 elsif $r270.active?
1289                     rotate_and_cleanup.call(-90)
1290                 elsif $enhance.active?
1291                     enhance_and_cleanup.call
1292                 end
1293             end
1294             if event.event_type == Gdk::Event::BUTTON_PRESS && event.button == 3
1295                 popup_thumbnail_menu(event, ['change_image'], entry2type(captionfile), xmldir, "#{infotype}-",
1296                                      { :change => change_image, :rotate => rotate_and_cleanup, :enhance => enhance_and_cleanup,
1297                                        :color_swap => color_swap_and_cleanup, :frame_offset => change_frame_offset_and_cleanup, :whitebalance => whitebalance_and_cleanup })
1298             end
1299             if event.event_type == Gdk::Event::BUTTON2_PRESS && event.button == 1
1300                 change_image.call
1301                 true   #- handled
1302             end
1303         }
1304         evtbox.signal_connect('button-press-event') { |w, event|
1305             $gesture_press = { :filename => thumbnail_file, :x => event.x, :y => event.y }
1306             false
1307         }
1308
1309         evtbox.signal_connect('button-release-event') { |w, event|
1310             if !$r90.active? && !$r270.active? && $gesture_press && $gesture_press[:filename] == thumbnail_file
1311                 msg 3, "press: #{$gesture_press[:x]} release: #{event.x}"
1312                 if (($gesture_press[:x]-event.x)/($gesture_press[:y]-event.y)).abs > 2 && ($gesture_press[:x]-event.x).abs > 5
1313                     angle = event.x-$gesture_press[:x] > 0 ? 90 : -90
1314                     msg 3, "gesture rotate: #{angle}"
1315                     rotate_and_cleanup.call(angle)
1316                 end
1317             end
1318             $gesture_press = nil
1319         }
1320         
1321         frame, textview = create_editzone($subalbums_sw, 0, img)
1322         textview.buffer.text = caption
1323         $subalbums.attach(Gtk::Alignment.new(0, 0.5, 0.5, 0).add(frame),
1324                           1, 2, current_y_sub_albums, current_y_sub_albums + 1, Gtk::FILL, Gtk::FILL, 2, 2)
1325         
1326         $subalbums_edits[xmldir.attributes['path']] = { :editzone => textview, :captionfile => captionfile }
1327         current_y_sub_albums += 1
1328     }
1329
1330     if $xmldir.elements['dir']
1331         #- title edition
1332         frame, $subalbums_title = create_editzone($subalbums_sw, 0, nil)
1333         $subalbums_title.buffer.text = $xmldir.attributes['subdirs-caption']
1334         $subalbums_title.set_justification(Gtk::Justification::CENTER)
1335         $subalbums_vb.pack_start(Gtk::Alignment.new(0.5, 0.5, 0.5, 0).add(frame), false, false)
1336         #- this album image/caption
1337         if $xmldir.attributes['thumbnails-caption']
1338             add_subalbum.call($xmldir)
1339         end
1340     end
1341     $xmldir.elements.each { |element|
1342         if element.name == 'image' || element.name == 'video'
1343             #- element (image or video) of this album
1344             dest_img = build_full_dest_filename(element.attributes['filename']).sub(/\.[^\.]+$/, '') + "-#{$default_size['thumbnails']}.jpg"
1345             msg 3, "dest_img: #{dest_img}"
1346             add_thumbnail($autotable, element.attributes['filename'], element.name, dest_img, from_utf8(element.attributes['caption']))
1347         end
1348         if element.name == 'dir'
1349             #- sub-album image/caption
1350             add_subalbum.call(element)
1351         end
1352     }
1353     $subalbums_vb.add($subalbums)
1354     $subalbums_vb.show_all
1355
1356     if !$xmldir.elements['image'] && !$xmldir.elements['video']
1357         $notebook.get_tab_label($autotable_sw).sensitive = false
1358         $notebook.set_page(0)
1359     else
1360         $notebook.get_tab_label($autotable_sw).sensitive = true
1361     end
1362
1363     if !$xmldir.elements['dir']
1364         $notebook.get_tab_label($subalbums_sw).sensitive = false
1365         $notebook.set_page(1)
1366     else
1367         $notebook.get_tab_label($subalbums_sw).sensitive = true
1368     end
1369 end
1370
1371 def pixbuf_or_nil(filename)
1372     begin
1373         return Gdk::Pixbuf.new(filename)
1374     rescue
1375         return nil
1376     end
1377 end
1378
1379 def theme_choose(current)
1380     dialog = Gtk::Dialog.new(utf8(_("Select your preferred theme")),
1381                              $main_window,
1382                              Gtk::Dialog::MODAL | Gtk::Dialog::DESTROY_WITH_PARENT,
1383                              [Gtk::Stock::OK, Gtk::Dialog::RESPONSE_OK],
1384                              [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
1385
1386     model = Gtk::ListStore.new(String, Gdk::Pixbuf, Gdk::Pixbuf, Gdk::Pixbuf)
1387     treeview = Gtk::TreeView.new(model).set_rules_hint(true)
1388     treeview.append_column(Gtk::TreeViewColumn.new(utf8(_("Theme name")), Gtk::CellRendererText.new, { :text => 0 }).set_alignment(0.5).set_spacing(5))
1389     treeview.append_column(Gtk::TreeViewColumn.new(utf8(_("Sub-albums page look")), Gtk::CellRendererPixbuf.new, { :pixbuf => 1 }).set_alignment(0.5).set_spacing(5))
1390     treeview.append_column(Gtk::TreeViewColumn.new(utf8(_("Thumbnails page look")), Gtk::CellRendererPixbuf.new, { :pixbuf => 2 }).set_alignment(0.5).set_spacing(5))
1391     treeview.append_column(Gtk::TreeViewColumn.new(utf8(_("Fullscreen page look")), Gtk::CellRendererPixbuf.new, { :pixbuf => 3 }).set_alignment(0.5).set_spacing(5))
1392     treeview.append_column(Gtk::TreeViewColumn.new('', Gtk::CellRendererText.new, {}))
1393     treeview.signal_connect('button-press-event') { |w, event|
1394         if event.event_type == Gdk::Event::BUTTON2_PRESS && event.button == 1
1395             dialog.response(Gtk::Dialog::RESPONSE_OK)
1396         end
1397     }
1398
1399     dialog.vbox.add(sw = Gtk::ScrolledWindow.new(nil, nil).add(treeview).set_policy(Gtk::POLICY_AUTOMATIC, Gtk::POLICY_AUTOMATIC))
1400
1401     `find '#{$FPATH}/themes' -mindepth 1 -maxdepth 1 -type d`.each { |dir|
1402         dir.chomp!
1403         iter = model.append
1404         iter[0] = File.basename(dir)
1405         iter[1] = pixbuf_or_nil("#{dir}/metadata/screenshot-1.png")
1406         iter[2] = pixbuf_or_nil("#{dir}/metadata/screenshot-2.png")
1407         iter[3] = pixbuf_or_nil("#{dir}/metadata/screenshot-3.png")
1408         if File.basename(dir) == current
1409             treeview.selection.select_iter(iter)
1410         end
1411     }
1412
1413     dialog.set_default_size(700, 400)
1414     dialog.vbox.show_all
1415     dialog.run { |response|
1416         iter = treeview.selection.selected
1417         dialog.destroy
1418         if response == Gtk::Dialog::RESPONSE_OK && iter
1419             return model.get_value(iter, 0)
1420         end
1421     }
1422     return nil
1423 end
1424
1425 def populate_subalbums_treeview
1426     $albums_ts.clear
1427     $autotable.clear
1428     $subalbums_vb.children.each { |chld|
1429         $subalbums_vb.remove(chld)
1430     }
1431
1432     source = $xmldoc.root.attributes['source']
1433     msg 3, "source: #{source}"
1434
1435     xmldir = $xmldoc.elements["//dir[@path='#{source}']"]
1436     if !xmldir
1437         msg 1, _("Corrupted booh file...")
1438         return
1439     end
1440
1441     append_dir_elem = Proc.new { |parent_iter, location|
1442         child_iter = $albums_ts.append(parent_iter)
1443         child_iter[0] = File.basename(location)
1444         child_iter[1] = location
1445         msg 3, "puttin location: #{location}"
1446         $xmldoc.elements.each("//dir[@path='#{location}']/dir") { |elem|
1447             append_dir_elem.call(child_iter, elem.attributes['path'])
1448         }
1449     }
1450     append_dir_elem.call(nil, source)
1451
1452     $albums_tv.expand_all
1453     $albums_tv.selection.select_iter($albums_ts.iter_first)
1454 end
1455
1456 def open_file(filename)
1457
1458     $filename = nil
1459     $modified = false
1460     $current_path = nil   #- invalidate
1461     $modified_pixbufs = {}
1462     $albums_ts.clear
1463     $autotable.clear
1464     $subalbums_vb.children.each { |chld|
1465         $subalbums_vb.remove(chld)
1466     }
1467
1468     if !File.exists?(filename)
1469         return utf8(_("File not found."))
1470     end
1471
1472     begin
1473         $xmldoc = REXML::Document.new File.new(filename)
1474     rescue Exception
1475         $xmldoc = nil
1476     end
1477
1478     if !$xmldoc || !$xmldoc.root || $xmldoc.root.name != 'booh'
1479         if entry2type(filename).nil?
1480             return utf8(_("Not a booh file!"))
1481         else
1482             return utf8(_("Not a booh file!\n\nHint: you cannot import directly an image or video with File/Open.\nUse File/New to create a new album."))
1483         end
1484     end
1485
1486     if !source = $xmldoc.root.attributes['source']
1487         return utf8(_("Corrupted booh file..."))
1488     end
1489
1490     if !dest = $xmldoc.root.attributes['destination']
1491         return utf8(_("Corrupted booh file..."))
1492     end
1493
1494     if !theme = $xmldoc.root.attributes['theme']
1495         return utf8(_("Corrupted booh file..."))
1496     end
1497
1498     limit_sizes = $xmldoc.root.attributes['limit-sizes']
1499
1500     $filename = filename
1501     select_theme(theme, limit_sizes)
1502     $default_size['thumbnails'] =~ /(.*)x(.*)/
1503     $default_thumbnails = { :x => $1.to_i, :y => $2.to_i }
1504     $albums_thumbnail_size =~ /(.*)x(.*)/
1505     $default_albums_thumbnails = { :x => $1.to_i, :y => $2.to_i }
1506
1507     populate_subalbums_treeview
1508
1509     $config['last-opens'] ||= []
1510     if $config['last-opens'][-1] != utf8(filename)
1511         $config['last-opens'] << utf8(filename)
1512     end
1513     $save.sensitive = $save_as.sensitive = $merge_current.sensitive = $merge.sensitive = $merge_subalbums.sensitive = $generate.sensitive = $properties.sensitive = true
1514     return nil
1515 end
1516
1517 def open_file_popup
1518     fc = Gtk::FileChooserDialog.new(utf8(_("Open file")),
1519                                     nil,
1520                                     Gtk::FileChooser::ACTION_OPEN,
1521                                     nil,
1522                                     [Gtk::Stock::OPEN, Gtk::Dialog::RESPONSE_ACCEPT], [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
1523     fc.add_shortcut_folder(File.expand_path("~/.booh"))
1524     fc.set_current_folder(File.expand_path("~/.booh"))
1525     fc.transient_for = $main_window
1526     ok = false
1527     while !ok
1528         if fc.run == Gtk::Dialog::RESPONSE_ACCEPT
1529             push_mousecursor_wait(fc)
1530             msg = open_file(fc.filename)
1531             pop_mousecursor(fc)
1532             if msg
1533                 show_popup(fc, msg)
1534                 ok = false
1535             else
1536                 ok = true
1537             end
1538         else
1539             ok = true
1540         end
1541     end
1542     fc.destroy
1543 end
1544
1545 def additional_booh_options
1546     options = ''
1547     if $config['mproc']
1548         options += "--mproc #{$config['mproc'].to_i} "
1549     end
1550     return options
1551 end
1552
1553 def new_album
1554     dialog = Gtk::Dialog.new(utf8(_("Create a new album")),
1555                              $main_window,
1556                              Gtk::Dialog::MODAL | Gtk::Dialog::DESTROY_WITH_PARENT,
1557                              [Gtk::Stock::OK, Gtk::Dialog::RESPONSE_OK],
1558                              [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
1559     
1560     frame1 = Gtk::Frame.new(utf8(_("Locations"))).add(tbl = Gtk::Table.new(0, 0, false))
1561     tbl.attach(Gtk::Label.new(utf8(_("Directory of images/videos: "))),
1562                0, 1, 0, 1, Gtk::SHRINK, Gtk::SHRINK, 2, 2)
1563     tbl.attach(src = Gtk::Entry.new,
1564                1, 2, 0, 1, Gtk::FILL, Gtk::SHRINK, 2, 2)
1565     tbl.attach(src_browse = Gtk::Button.new(utf8(_("browse..."))),
1566                2, 3, 0, 1, Gtk::SHRINK, Gtk::SHRINK, 2, 2)
1567     tbl.attach(Gtk::Label.new.set_markup(utf8(_("<span size='small'><i>number of images/videos down this directory:</i></span> "))),
1568                0, 1, 1, 2, Gtk::SHRINK, Gtk::SHRINK, 2, 2)
1569     tbl.attach(src_nb = Gtk::Label.new.set_markup(utf8(_("<span size='small'><i>N/A</i></span>"))),
1570                1, 2, 1, 2, Gtk::FILL, Gtk::SHRINK, 2, 2)
1571     tbl.attach(Gtk::Label.new(utf8(_("Directory where to put the web-album: "))),
1572                0, 1, 2, 3, Gtk::SHRINK, Gtk::SHRINK, 2, 2)
1573     tbl.attach(dest = Gtk::Entry.new,
1574                1, 2, 2, 3, Gtk::FILL, Gtk::SHRINK, 2, 2)
1575     tbl.attach(dest_browse = Gtk::Button.new(utf8(_("browse..."))),
1576                2, 3, 2, 3, Gtk::SHRINK, Gtk::SHRINK, 2, 2)
1577     tbl.attach(Gtk::Label.new(utf8(_("Filename to store this album's properties: "))),
1578                0, 1, 3, 4, Gtk::SHRINK, Gtk::SHRINK, 2, 2)
1579     tbl.attach(conf = Gtk::Entry.new.set_size_request(250, -1),
1580                1, 2, 3, 4, Gtk::FILL, Gtk::SHRINK, 2, 2)
1581     tbl.attach(conf_browse = Gtk::Button.new(utf8(_("browse..."))),
1582                2, 3, 3, 4, Gtk::SHRINK, Gtk::SHRINK, 2, 2)
1583
1584     frame2 = Gtk::Frame.new(utf8(_("Configuration"))).add(tbl = Gtk::Table.new(0, 0, false))
1585     tbl.attach(Gtk::Label.new(utf8(_("Theme: "))),
1586                0, 1, 0, 1, Gtk::SHRINK, Gtk::SHRINK, 2, 2)
1587     tbl.attach(theme_button = Gtk::Button.new($config['default-theme'] || 'simple'),
1588                1, 2, 0, 1, Gtk::FILL, Gtk::SHRINK, 2, 2)
1589     tbl.attach(Gtk::Label.new(utf8(_("Sizes of images to generate: "))),
1590                0, 1, 1, 2, Gtk::SHRINK, Gtk::SHRINK, 2, 2)
1591     tbl.attach(sizes = Gtk::HBox.new,
1592                1, 3, 1, 2, Gtk::SHRINK, Gtk::SHRINK, 2, 2)
1593
1594     src_nb_calculated_for = ''
1595     src_nb_thread = nil
1596     process_src_nb = Proc.new {
1597         if src.text != src_nb_calculated_for
1598             src_nb_calculated_for = src.text
1599             if src_nb_thread
1600                 Thread.kill(src_nb_thread)
1601                 src_nb_thread = nil
1602             end
1603             if File.directory?(from_utf8(src_nb_calculated_for)) && src_nb_calculated_for != '/'
1604                 if File.readable?(from_utf8(src_nb_calculated_for))
1605                     src_nb_thread = Thread.new {
1606                         src_nb.set_markup(utf8(_("<span size='small'><i>processing...</i></span>")))
1607                         total = { 'image' => 0, 'video' => 0, nil => 0 }
1608                         `find '#{from_utf8(src_nb_calculated_for)}' -type d`.each { |dir|
1609                             if File.basename(dir) =~ /^\./
1610                                 next
1611                             else
1612                                 begin
1613                                     Dir.entries(dir.chomp).each { |file|
1614                                         total[entry2type(file)] += 1
1615                                     }
1616                                 rescue Errno::EACCES, Errno::ENOENT
1617                                 end
1618                             end
1619                         }
1620                         src_nb.set_markup(utf8(_("<span size='small'><i>%s images and %s videos</i></span>") % [ total['image'], total['video'] ]))
1621                         src_nb_thread = nil
1622                     }
1623                 else
1624                     src_nb.set_markup(utf8(_("<span size='small'><i>permission denied</i></span>")))
1625                 end
1626             else
1627                 src_nb.set_markup(utf8(_("<span size='small'><i>N/A</i></span>")))
1628             end
1629         end
1630         true
1631     }
1632     timeout_src_nb = Gtk.timeout_add(100) {
1633         process_src_nb.call
1634     }
1635
1636     src_browse.signal_connect('clicked') {
1637         fc = Gtk::FileChooserDialog.new(utf8(_("Select the directory of images/videos")),
1638                                         nil,
1639                                         Gtk::FileChooser::ACTION_SELECT_FOLDER,
1640                                         nil,
1641                                         [Gtk::Stock::OPEN, Gtk::Dialog::RESPONSE_ACCEPT], [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
1642         fc.transient_for = $main_window
1643         if fc.run == Gtk::Dialog::RESPONSE_ACCEPT
1644             src.text = utf8(fc.filename)
1645             process_src_nb.call
1646             conf.text = File.expand_path("~/.booh/#{File.basename(src.text)}")
1647         end
1648         fc.destroy
1649     }
1650
1651     dest_browse.signal_connect('clicked') {
1652         fc = Gtk::FileChooserDialog.new(utf8(_("Select a new directory where to put the web-album")),
1653                                         nil,
1654                                         Gtk::FileChooser::ACTION_CREATE_FOLDER,
1655                                         nil,
1656                                         [Gtk::Stock::OPEN, Gtk::Dialog::RESPONSE_ACCEPT], [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
1657         fc.transient_for = $main_window
1658         if fc.run == Gtk::Dialog::RESPONSE_ACCEPT
1659             dest.text = utf8(fc.filename)
1660         end
1661         fc.destroy
1662     }
1663
1664     conf_browse.signal_connect('clicked') {
1665         fc = Gtk::FileChooserDialog.new(utf8(_("Select a new file to store this album's properties")),
1666                                         nil,
1667                                         Gtk::FileChooser::ACTION_SAVE,
1668                                         nil,
1669                                         [Gtk::Stock::OPEN, Gtk::Dialog::RESPONSE_ACCEPT], [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
1670         fc.transient_for = $main_window
1671         fc.add_shortcut_folder(File.expand_path("~/.booh"))
1672         fc.set_current_folder(File.expand_path("~/.booh"))
1673         if fc.run == Gtk::Dialog::RESPONSE_ACCEPT
1674             conf.text = utf8(fc.filename)
1675         end
1676         fc.destroy
1677     }
1678
1679     theme_sizes = []
1680     recreate_theme_config = proc {
1681         theme_sizes.each { |e| sizes.remove(e[:widget]) }
1682         theme_sizes = []
1683         select_theme(theme_button.label, 'all')
1684         $images_size.each { |s|
1685             sizes.add(cb = Gtk::CheckButton.new(sizename(s['name'])))
1686             if !s['optional']
1687                 cb.active = true
1688             end
1689             tooltips = Gtk::Tooltips.new
1690             tooltips.set_tip(cb, utf8(s['description']), nil)
1691             theme_sizes << { :widget => cb, :value => s['name'] }
1692         }
1693         sizes.add(cb = Gtk::CheckButton.new(utf8(_('original'))))
1694         tooltips = Gtk::Tooltips.new
1695         tooltips.set_tip(cb, utf8(_("Include original image in web-album")), nil)
1696         theme_sizes << { :widget => cb, :value => 'original' }
1697         sizes.show_all
1698     }
1699     recreate_theme_config.call
1700
1701     theme_button.signal_connect('clicked') {
1702         if newtheme = theme_choose(theme_button.label)
1703             theme_button.label = newtheme
1704             recreate_theme_config.call
1705         end
1706     }
1707
1708     dialog.vbox.add(frame1)
1709     dialog.vbox.add(frame2)
1710     dialog.window_position = Gtk::Window::POS_MOUSE
1711     dialog.show_all
1712
1713     keepon = true
1714     ok = true
1715     while keepon
1716         dialog.run { |response|
1717             if response == Gtk::Dialog::RESPONSE_OK
1718                 srcdir = from_utf8(src.text)
1719                 destdir = from_utf8(dest.text)
1720                 if !File.directory?(srcdir)
1721                     show_popup(dialog, utf8(_("The directory of images/videos doesn't exist. Please check your input.")))
1722                     src.grab_focus
1723                 elsif conf.text == ''
1724                     show_popup(dialog, utf8(_("Please specify a filename to store the album's properties.")))
1725                     conf.grab_focus
1726                 elsif destdir != make_dest_filename(destdir)
1727                     show_popup(dialog, utf8(_("Sorry, destination directory can't contain non simple alphanumeric characters.")))
1728                     dest.grab_focus
1729                 elsif File.directory?(destdir)
1730                     keepon = !show_popup(dialog, utf8(_("The destination directory already exists. Are you sure you want to continue?")), { :okcancel => true })
1731                     dest.grab_focus
1732                 elsif File.exists?(destdir)
1733                     show_popup(dialog, utf8(_("There is already a file by the name of the destination directory. Please choose another one.")))
1734                     dest.grab_focus
1735                 elsif !theme_sizes.detect { |e| e[:value] != 'original' && e[:widget].active? }
1736                     show_popup(dialog, utf8(_("You need to select at least one size (not counting original).")))
1737                 else
1738                     system("mkdir '#{destdir}'")
1739                     if !File.directory?(destdir)
1740                         show_popup(dialog, utf8(_("Could not create destination directory. Permission denied?")))
1741                         dest.grab_focus
1742                     else
1743                         keepon = false
1744                     end
1745                 end
1746             else
1747                 keepon = ok = false
1748             end
1749         }
1750     end
1751     srcdir = from_utf8(src.text)
1752     destdir = from_utf8(dest.text)
1753     configskel = File.expand_path(from_utf8(conf.text))
1754     theme = theme_button.label
1755     sizes = theme_sizes.find_all { |e| e[:widget].active? }.collect { |e| e[:value] }.join(',')
1756     dialog.destroy
1757     if src_nb_thread
1758         Thread.kill(src_nb_thread)
1759     end
1760     Gtk.timeout_remove(timeout_src_nb)
1761
1762     if ok
1763         perform_in_background("booh-backend --source '#{srcdir}' --destination '#{destdir}' --config-skel '#{configskel}' --for-gui " +
1764                                   "--verbose-level #{$verbose_level} --theme #{theme} --sizes #{sizes} #{additional_booh_options}",
1765                               utf8(_("Please wait while scanning source directory...")),
1766                               { :closure_after => proc { open_file(configskel) } })
1767     end
1768 end
1769
1770 def properties
1771     dialog = Gtk::Dialog.new(utf8(_("Properties of your album")),
1772                              $main_window,
1773                              Gtk::Dialog::MODAL | Gtk::Dialog::DESTROY_WITH_PARENT,
1774                              [Gtk::Stock::OK, Gtk::Dialog::RESPONSE_OK],
1775                              [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
1776     
1777     source = $xmldoc.root.attributes['source']
1778     dest = $xmldoc.root.attributes['destination']
1779     theme = $xmldoc.root.attributes['theme']
1780     limit_sizes = $xmldoc.root.attributes['limit-sizes']
1781     if limit_sizes
1782         limit_sizes = limit_sizes.split(/,/)
1783     end
1784
1785     frame1 = Gtk::Frame.new(utf8(_("Locations"))).add(tbl = Gtk::Table.new(0, 0, false))
1786     tbl.attach(Gtk::Label.new(utf8(_("Directory of source images/videos: "))),
1787                0, 1, 0, 1, Gtk::SHRINK, Gtk::SHRINK, 2, 2)
1788     tbl.attach(Gtk::Alignment.new(0, 0.5, 0, 0).add(Gtk::Label.new.set_markup('<i>' + source + '</i>')),
1789                1, 2, 0, 1, Gtk::FILL, Gtk::SHRINK, 2, 2)
1790     tbl.attach(Gtk::Label.new(utf8(_("Directory where the web-album is created: "))),
1791                0, 1, 2, 3, Gtk::SHRINK, Gtk::SHRINK, 2, 2)
1792     tbl.attach(Gtk::Alignment.new(0, 0.5, 0, 0).add(Gtk::Label.new.set_markup('<i>' + dest + '</i>')),
1793                1, 2, 2, 3, Gtk::FILL, Gtk::SHRINK, 2, 2)
1794     tbl.attach(Gtk::Label.new(utf8(_("Filename where this album's properties are stored: "))),
1795                0, 1, 3, 4, Gtk::SHRINK, Gtk::SHRINK, 2, 2)
1796     tbl.attach(Gtk::Alignment.new(0, 0.5, 0, 0).add(Gtk::Label.new.set_markup('<i>' + $filename + '</i>')),
1797                1, 2, 3, 4, Gtk::FILL, Gtk::SHRINK, 2, 2)
1798
1799     frame2 = Gtk::Frame.new(utf8(_("Configuration"))).add(tbl = Gtk::Table.new(0, 0, false))
1800     tbl.attach(Gtk::Label.new(utf8(_("Theme: "))),
1801                0, 1, 0, 1, Gtk::SHRINK, Gtk::SHRINK, 2, 2)
1802     tbl.attach(theme_button = Gtk::Button.new(theme),
1803                1, 2, 0, 1, Gtk::FILL, Gtk::SHRINK, 2, 2)
1804     tbl.attach(Gtk::Label.new(utf8(_("Sizes of images to generate: "))),
1805                0, 1, 1, 2, Gtk::SHRINK, Gtk::SHRINK, 2, 2)
1806     tbl.attach(sizes = Gtk::HBox.new,
1807                1, 3, 1, 2, Gtk::SHRINK, Gtk::SHRINK, 2, 2)
1808
1809     theme_sizes = []
1810     recreate_theme_config = proc {
1811         theme_sizes.each { |e| sizes.remove(e[:widget]) }
1812         theme_sizes = []
1813         select_theme(theme_button.label, 'all')
1814         $images_size.each { |s|
1815             sizes.add(cb = Gtk::CheckButton.new(sizename(s['name'])))
1816             if limit_sizes
1817                 if limit_sizes.include?(s['name'])
1818                     cb.active = true
1819                 end
1820             else
1821                 if !s['optional']
1822                     cb.active = true
1823                 end
1824             end
1825             tooltips = Gtk::Tooltips.new
1826             tooltips.set_tip(cb, utf8(s['description']), nil)
1827             theme_sizes << { :widget => cb, :value => s['name'] }
1828         }
1829         sizes.add(cb = Gtk::CheckButton.new(utf8(_('original'))))
1830         tooltips = Gtk::Tooltips.new
1831         tooltips.set_tip(cb, utf8(_("Include original image in web-album")), nil)
1832         if limit_sizes && limit_sizes.include?('original')
1833             cb.active = true
1834         end
1835         theme_sizes << { :widget => cb, :value => 'original' }
1836         sizes.show_all
1837     }
1838     recreate_theme_config.call
1839
1840     theme_button.signal_connect('clicked') {
1841         if newtheme = theme_choose(theme_button.label)
1842             limit_sizes = nil
1843             theme_button.label = newtheme
1844             recreate_theme_config.call
1845         end
1846     }
1847
1848     dialog.vbox.add(frame1)
1849     dialog.vbox.add(frame2)
1850     dialog.window_position = Gtk::Window::POS_MOUSE
1851     dialog.show_all
1852
1853     keepon = true
1854     ok = true
1855     while keepon
1856         dialog.run { |response|
1857             if response == Gtk::Dialog::RESPONSE_OK
1858                 if !theme_sizes.detect { |e| e[:value] != 'original' && e[:widget].active? }
1859                     show_popup(dialog, utf8(_("You need to select at least one size (not counting original).")))
1860                 else
1861                     keepon = false
1862                 end
1863             else
1864                 keepon = ok = false
1865             end
1866         }
1867     end
1868     save_theme = theme_button.label
1869     save_limit_sizes = theme_sizes.find_all { |e| e[:widget].active? }.collect { |e| e[:value] }
1870     dialog.destroy
1871
1872     if ok && (save_theme != theme || save_limit_sizes != limit_sizes)
1873         save_current_file
1874         perform_in_background("booh-backend --use-config '#{$filename}' --for-gui " +
1875                                   "--verbose-level #{$verbose_level} --theme #{theme} --sizes #{save_limit_sizes.join(',')} #{additional_booh_options}",
1876                               utf8(_("Please wait while scanning source directory...")),
1877                               { :closure_after => proc { open_file($filename) } })
1878     end
1879 end
1880
1881 def merge_current
1882     save_current_file
1883
1884     sel = $albums_tv.selection.selected_rows
1885
1886     perform_in_background("booh-backend --merge-config-onedir '#{$filename}' --dir '#{from_utf8($current_path)}' --for-gui " +
1887                               "--verbose-level #{$verbose_level} #{additional_booh_options}",
1888                           utf8(_("Please wait while scanning source directory...")),
1889                           { :closure_after => proc {
1890                                   open_file($filename)
1891                                   $albums_tv.selection.select_path(sel[0])
1892                               } })
1893 end
1894
1895 def merge
1896     save_current_file
1897
1898     theme = $xmldoc.root.attributes['theme']
1899     limit_sizes = $xmldoc.root.attributes['limit-sizes']
1900     if limit_sizes
1901         limit_sizes = "--sizes #{limit_sizes}"
1902     end
1903     perform_in_background("booh-backend --merge-config '#{$filename}' --for-gui " +
1904                               "--verbose-level #{$verbose_level} --theme #{theme} #{limit_sizes} #{additional_booh_options}",
1905                           utf8(_("Please wait while scanning source directory...")),
1906                           { :closure_after => proc { open_file($filename) } })
1907 end
1908
1909 def merge_subalbums
1910     save_current_file
1911
1912     perform_in_background("booh-backend --merge-config-newdirs '#{$filename}' --for-gui " +
1913                               "--verbose-level #{$verbose_level} #{additional_booh_options}",
1914                           utf8(_("Please wait while scanning source directory...")),
1915                           { :closure_after => proc { open_file($filename) } })
1916 end
1917
1918 def save_as_do
1919     fc = Gtk::FileChooserDialog.new(utf8(_("Select a new filename to store this album's properties")),
1920                                     nil,
1921                                     Gtk::FileChooser::ACTION_SAVE,
1922                                     nil,
1923                                     [Gtk::Stock::OPEN, Gtk::Dialog::RESPONSE_ACCEPT], [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
1924     fc.transient_for = $main_window
1925     fc.add_shortcut_folder(File.expand_path("~/.booh"))
1926     fc.set_current_folder(File.expand_path("~/.booh"))
1927     fc.filename = $filename
1928     if fc.run == Gtk::Dialog::RESPONSE_ACCEPT
1929         $filename = fc.filename
1930         save_current_file
1931     end
1932     fc.destroy
1933 end
1934
1935 def preferences
1936     dialog = Gtk::Dialog.new(utf8(_("Edit preferences")),
1937                              $main_window,
1938                              Gtk::Dialog::MODAL | Gtk::Dialog::DESTROY_WITH_PARENT,
1939                              [Gtk::Stock::OK, Gtk::Dialog::RESPONSE_OK],
1940                              [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
1941
1942     dialog.vbox.add(notebook = Gtk::Notebook.new)
1943     notebook.append_page(tbl = Gtk::Table.new(0, 0, false), Gtk::Label.new(utf8(_("Options"))))
1944     tbl.attach(Gtk::Alignment.new(1, 0.5, 0, 0).add(Gtk::Label.new.set_markup(utf8(_("Command for watching videos: ")))),
1945                0, 1, 0, 1, Gtk::FILL, Gtk::SHRINK, 2, 2)
1946     tbl.attach(Gtk::Alignment.new(0, 0.5, 1, 0).add(video_viewer_entry = Gtk::Entry.new.set_text($config['video-viewer'])),
1947                1, 2, 0, 1, Gtk::FILL, Gtk::SHRINK, 2, 2)
1948     tooltips = Gtk::Tooltips.new
1949     tooltips.set_tip(video_viewer_entry, utf8(_("Use %f to specify the filename; for example: mplayer %f")), nil)
1950     tbl.attach(Gtk::Alignment.new(1, 0.5, 0, 0).add(smp_check = Gtk::CheckButton.new(utf8(_("Use symmetric multi-processing")))),
1951                0, 1, 1, 2, Gtk::FILL, Gtk::SHRINK, 2, 2)
1952     tbl.attach(Gtk::Alignment.new(0, 0.5, 1, 0).add(smp_hbox = Gtk::HBox.new.add(smp_spin = Gtk::SpinButton.new(2, 16, 1)).add(Gtk::Label.new(utf8(_("processors")))).set_sensitive(false)),
1953                1, 2, 1, 2, Gtk::FILL, Gtk::SHRINK, 2, 2)
1954     smp_check.signal_connect('toggled') {
1955         if smp_check.active?
1956             smp_hbox.sensitive = true
1957         else
1958             smp_hbox.sensitive = false
1959         end
1960     }
1961     if $config['mproc']
1962         smp_check.active = true
1963         smp_spin.value = $config['mproc'].to_i
1964     end
1965
1966     notebook.append_page(tbl = Gtk::Table.new(0, 0, false), Gtk::Label.new(utf8(_("Advanced"))))
1967     tbl.attach(Gtk::Label.new.set_markup(utf8(_("Options to pass to <i>convert</i> when\nperforming 'enhance contrast': "))),
1968                0, 1, 0, 1, Gtk::SHRINK, Gtk::SHRINK, 2, 2)
1969     tbl.attach(enhance_entry = Gtk::Entry.new.set_text($config['convert-enhance'] || $convert_enhance).set_size_request(250, -1),
1970                1, 2, 0, 1, Gtk::FILL, Gtk::SHRINK, 2, 2)
1971
1972     dialog.vbox.show_all
1973     dialog.run { |response|
1974         if response == Gtk::Dialog::RESPONSE_OK
1975             $config['video-viewer'] = video_viewer_entry.text
1976             if smp_check.active?
1977                 $config['mproc'] = smp_spin.value.to_i
1978             else
1979                 $config.delete('mproc')
1980             end
1981
1982             $config['convert-enhance'] = enhance_entry.text
1983         end
1984     }
1985     dialog.destroy
1986 end
1987
1988 def create_menu_and_toolbar
1989
1990     #- menu
1991     mb = Gtk::MenuBar.new
1992
1993     filemenu = Gtk::MenuItem.new(utf8(_("_File")))
1994     filesubmenu = Gtk::Menu.new
1995     filesubmenu.append(new       = Gtk::ImageMenuItem.new(Gtk::Stock::NEW))
1996     filesubmenu.append(open      = Gtk::ImageMenuItem.new(Gtk::Stock::OPEN))
1997     filesubmenu.append(            Gtk::SeparatorMenuItem.new)
1998     filesubmenu.append($save     = Gtk::ImageMenuItem.new(Gtk::Stock::SAVE).set_sensitive(false))
1999     filesubmenu.append($save_as  = Gtk::ImageMenuItem.new(Gtk::Stock::SAVE_AS).set_sensitive(false))
2000     filesubmenu.append(            Gtk::SeparatorMenuItem.new)
2001     tooltips = Gtk::Tooltips.new
2002     filesubmenu.append($merge_current = Gtk::ImageMenuItem.new(utf8(_("Merge new/removed images/videos in current subalbum"))).set_sensitive(false))
2003     $merge_current.image = Gtk::Image.new("#{$FPATH}/images/stock-reset-16.png")
2004     tooltips.set_tip($merge_current, utf8(_("Take into account new/removed images/videos in currently viewed subalbum")), nil)
2005     filesubmenu.append($merge_subalbums = Gtk::ImageMenuItem.new(utf8(_("Scan source directory to merge new subalbums"))).set_sensitive(false))
2006     $merge_subalbums.image = Gtk::Image.new("#{$FPATH}/images/stock-reset-16.png")
2007     tooltips.set_tip($merge_subalbums, utf8(_("Take into account new/removed subalbums (subdirectories) in the source directory (but don't touch existing subalbums)")), nil)
2008     filesubmenu.append($merge    = Gtk::ImageMenuItem.new(utf8(_("Scan source directory to merge new subalbums and new/removed images/videos"))).set_sensitive(false))
2009     $merge.image = Gtk::Image.new("#{$FPATH}/images/stock-reset-16.png")
2010     tooltips.set_tip($merge, utf8(_("Take into account new/removed subalbums (subdirectories) and new/removed images/videos in existing subalbums")), nil)
2011     filesubmenu.append(            Gtk::SeparatorMenuItem.new)
2012     filesubmenu.append($generate = Gtk::ImageMenuItem.new(utf8(_("Generate web-album"))).set_sensitive(false))
2013     $generate.image = Gtk::Image.new("#{$FPATH}/images/stock-web-16.png")
2014     tooltips.set_tip($generate, utf8(_("(Re)generate web-album from latest changes into the destination directory")), nil)
2015     filesubmenu.append(            Gtk::SeparatorMenuItem.new)
2016     filesubmenu.append($properties = Gtk::ImageMenuItem.new(Gtk::Stock::PROPERTIES).set_sensitive(false))
2017     tooltips.set_tip($properties, utf8(_("View and modify properties of the web-album")), nil)
2018     filesubmenu.append(            Gtk::SeparatorMenuItem.new)
2019     filesubmenu.append(quit      = Gtk::ImageMenuItem.new(Gtk::Stock::QUIT))
2020     filemenu.set_submenu(filesubmenu)
2021     mb.append(filemenu)
2022
2023     new.signal_connect('activate') { new_album }
2024     open.signal_connect('activate') { open_file_popup }
2025     $save.signal_connect('activate') { save_current_file }
2026     $save_as.signal_connect('activate') { save_as_do }
2027     $merge_current.signal_connect('activate') { merge_current }
2028     $merge.signal_connect('activate') { merge }
2029     $merge_subalbums.signal_connect('activate') { merge_subalbums }
2030     $generate.signal_connect('activate') {
2031         save_current_file
2032         perform_in_background("booh-backend --config '#{$filename}' --verbose-level #{$verbose_level} #{additional_booh_options}",
2033                               utf8(_("Please wait while generating web-album...\nThis may take a while, please be patient.")),
2034                               { :successmsg => utf8(_("Your web-album is now ready in directory `%s'.") % $xmldoc.root.attributes['destination']),
2035                                 :failuremsg => utf8(_("There was something wrong when generating the web-album, sorry.")) })
2036     }
2037     $properties.signal_connect('activate') { properties }
2038
2039     quit.signal_connect('activate') { try_quit }
2040
2041     editmenu = Gtk::MenuItem.new(utf8(_("_Edit")))
2042     editsubmenu = Gtk::Menu.new
2043     editsubmenu.append($undo_mb = Gtk::ImageMenuItem.new(Gtk::Stock::UNDO).set_sensitive(false))
2044     editsubmenu.append($redo_mb = Gtk::ImageMenuItem.new(Gtk::Stock::REDO).set_sensitive(false))
2045     editsubmenu.append(           Gtk::SeparatorMenuItem.new)
2046     editsubmenu.append(prefs    = Gtk::ImageMenuItem.new(Gtk::Stock::PREFERENCES))
2047     editmenu.set_submenu(editsubmenu)
2048     mb.append(editmenu)
2049
2050     prefs.signal_connect('activate') { preferences }
2051
2052     helpmenu = Gtk::MenuItem.new(utf8(_("_Help")))
2053     helpsubmenu = Gtk::Menu.new
2054     helpsubmenu.append(about = Gtk::ImageMenuItem.new(Gtk::Stock::ABOUT))
2055     helpmenu.set_submenu(helpsubmenu)
2056     mb.append(helpmenu)
2057
2058     about.signal_connect('activate') {
2059             show_popup($main_window, utf8(_("<span size='x-large' weight='bold'>Booh %s</span>
2060
2061 <i>``The Web-Album of choice for discriminating Linux users''</i>
2062
2063 Copyright (c) 2005 Guillaume Cottenceau") % $VERSION), { :centered => true, :pos_centered => true })
2064     }
2065
2066
2067     #- toolbar
2068     tb = Gtk::Toolbar.new
2069
2070     tb.insert(-1, open = Gtk::MenuToolButton.new(Gtk::Stock::OPEN))
2071     open.label = utf8(_("Open"))  #- to avoid missing gtk2 l10n catalogs
2072     open.menu = Gtk::Menu.new
2073     open.signal_connect('clicked') { open_file_popup }
2074     open.signal_connect('show-menu') {
2075         lastopens = Gtk::Menu.new
2076         j = 0
2077         if $config['last-opens']
2078             $config['last-opens'].reverse.each { |e|
2079                 lastopens.attach(item = Gtk::ImageMenuItem.new(e, false), 0, 1, j, j + 1)
2080                 item.signal_connect('activate') {
2081                     push_mousecursor_wait
2082                     msg = open_file(from_utf8(e))
2083                     pop_mousecursor
2084                     if msg
2085                         show_popup($main_window, msg)
2086                     end
2087                 }
2088                 j += 1
2089             }
2090             lastopens.show_all
2091         end
2092         open.menu = lastopens
2093     }
2094
2095     tb.insert(-1, Gtk::SeparatorToolItem.new)
2096
2097     tb.insert(-1, $r90 = Gtk::ToggleToolButton.new)
2098     $r90.icon_widget = Gtk::Image.new("#{$FPATH}/images/stock-rotate-90-16.png")
2099     $r90.label = utf8(_("Rotate"))
2100     tb.insert(-1, $r270 = Gtk::ToggleToolButton.new)
2101     $r270.icon_widget = Gtk::Image.new("#{$FPATH}/images/stock-rotate-270-16.png")
2102     $r270.label = utf8(_("Rotate"))
2103     tb.insert(-1, $enhance = Gtk::ToggleToolButton.new)
2104     $enhance.icon_widget = Gtk::Image.new("#{$FPATH}/images/stock-channels-16.png")
2105     $enhance.label = utf8(_("Enhance"))
2106     tb.insert(-1, $delete = Gtk::ToggleToolButton.new(Gtk::Stock::DELETE))
2107     $delete.label = utf8(_("Delete"))  #- to avoid missing gtk2 l10n catalogs
2108     tb.insert(-1, nothing = Gtk::ToolButton.new('').set_sensitive(false))
2109     nothing.icon_widget = Gtk::Image.new("#{$FPATH}/images/stock-none-16.png")
2110     nothing.label = utf8(_("None"))
2111
2112     tb.insert(-1, Gtk::SeparatorToolItem.new)
2113
2114     tb.insert(-1, $undo_tb = Gtk::ToolButton.new(Gtk::Stock::UNDO).set_sensitive(false))
2115     tb.insert(-1, $redo_tb = Gtk::ToolButton.new(Gtk::Stock::REDO).set_sensitive(false))
2116
2117     perform_undo = Proc.new {
2118         $redo_tb.sensitive = $redo_mb.sensitive = true
2119         if not more_undoes = UndoHandler.undo($statusbar)
2120             $undo_tb.sensitive = $undo_mb.sensitive = false
2121         end
2122     }
2123     perform_redo = Proc.new {
2124         $undo_tb.sensitive = $undo_mb.sensitive = true
2125         if not more_redoes = UndoHandler.redo($statusbar)
2126             $redo_tb.sensitive = $redo_mb.sensitive = false
2127         end
2128     }
2129
2130     $undo_tb.signal_connect('clicked')  { perform_undo.call }
2131     $undo_mb.signal_connect('activate') { perform_undo.call }
2132     $redo_tb.signal_connect('clicked')  { perform_redo.call }
2133     $redo_mb.signal_connect('activate') { perform_redo.call }
2134
2135     one_click_explain_try = Proc.new {
2136         if !$config['one-click-explained']
2137             show_popup($main_window, utf8(_("<b>One-Click tools.</b>
2138
2139 You have just clicked on a One-Click tool. When such a tool is activated
2140 (<span foreground='darkblue'>Rotate clockwise</span>, <span foreground='darkblue'>Rotate counter-clockwise</span>, <span foreground='darkblue'>Enhance</span> or <span foreground='darkblue'>Delete</span>), clicking
2141 on a thumbnail will immediately apply the desired action.
2142
2143 Click the <span foreground='darkblue'>None</span> icon when you're finished with One-Click tools.
2144 ")))
2145             $config['one-click-explained'] = true
2146         end
2147     }
2148
2149     $r90.signal_connect('toggled') {
2150         if $r90.active?
2151             set_mousecursor(Gdk::Cursor::SB_RIGHT_ARROW)
2152             one_click_explain_try.call
2153             $r270.active = false
2154             $enhance.active = false
2155             $delete.active = false
2156             nothing.sensitive = true
2157         else
2158             if !$r270.active? && !$enhance.active? && !$delete.active?
2159                 set_mousecursor_normal
2160                 nothing.sensitive = false
2161             else
2162                 nothing.sensitive = true
2163             end
2164         end
2165     }
2166     $r270.signal_connect('toggled') {
2167         if $r270.active?
2168             set_mousecursor(Gdk::Cursor::SB_LEFT_ARROW)
2169             one_click_explain_try.call
2170             $r90.active = false
2171             $enhance.active = false
2172             $delete.active = false
2173             nothing.sensitive = true
2174         else
2175             if !$r90.active? && !$enhance.active? && !$delete.active?
2176                 set_mousecursor_normal
2177                 nothing.sensitive = false
2178             else
2179                 nothing.sensitive = true
2180             end
2181         end
2182     }
2183     $enhance.signal_connect('toggled') {
2184         if $enhance.active?
2185             set_mousecursor(Gdk::Cursor::SPRAYCAN)
2186             one_click_explain_try.call
2187             $r90.active = false
2188             $r270.active = false
2189             $delete.active = false
2190             nothing.sensitive = true
2191         else
2192             if !$r90.active? && !$r270.active? && !$delete.active?
2193                 set_mousecursor_normal
2194                 nothing.sensitive = false
2195             else
2196                 nothing.sensitive = true
2197             end
2198         end
2199     }
2200     $delete.signal_connect('toggled') {
2201         if $delete.active?
2202             set_mousecursor(Gdk::Cursor::PIRATE)
2203             one_click_explain_try.call
2204             $r90.active = false
2205             $r270.active = false
2206             $enhance.active = false
2207             nothing.sensitive = true
2208         else
2209             if !$r90.active? && !$r270.active? && !$enhance.active?
2210                 set_mousecursor_normal
2211                 nothing.sensitive = false
2212             else
2213                 nothing.sensitive = true
2214             end
2215         end
2216     }
2217     nothing.signal_connect('clicked') {
2218         $r90.active = $r270.active = $enhance.active = $delete.active = false
2219         set_mousecursor_normal
2220     }
2221
2222     return [ mb, tb ]
2223 end
2224
2225 def create_main_window
2226
2227     mb, tb = create_menu_and_toolbar
2228
2229 #    open_file('/home/gc/booh/foo')
2230
2231     $albums_tv = Gtk::TreeView.new
2232     $albums_tv.set_size_request(120, -1)
2233     renderer = Gtk::CellRendererText.new
2234     column = Gtk::TreeViewColumn.new('', renderer, { :text => 0 })
2235     $albums_tv.append_column(column)
2236     $albums_tv.set_headers_visible(false)
2237     $albums_tv.selection.signal_connect('changed') { |w|
2238         push_mousecursor_wait
2239         save_changes
2240         iter = w.selected
2241         if !iter
2242             msg 3, "no selection"
2243         else
2244             $current_path = $albums_ts.get_value(iter, 1)
2245             change_dir
2246         end
2247         pop_mousecursor
2248     }
2249     $albums_ts = Gtk::TreeStore.new(String, String)
2250     $albums_tv.set_model($albums_ts)
2251     $albums_tv.signal_connect('realize') { $albums_tv.grab_focus }
2252
2253     albums_sw = Gtk::ScrolledWindow.new(nil, nil)
2254     albums_sw.set_policy(Gtk::POLICY_NEVER, Gtk::POLICY_AUTOMATIC)
2255     albums_sw.add_with_viewport($albums_tv)
2256
2257     $notebook = Gtk::Notebook.new
2258     create_subalbums_page
2259     $notebook.append_page($subalbums_sw, Gtk::Label.new(utf8(_("Sub-albums page"))))
2260     create_auto_table
2261     $notebook.append_page($autotable_sw, Gtk::Label.new(utf8(_("Thumbnails page"))))
2262     $notebook.show_all
2263     $notebook.signal_connect('switch-page') { |w, page, num|
2264         if num == 0
2265             $delete.active = false
2266             $delete.sensitive = false
2267         else
2268             $delete.sensitive = true
2269         end
2270     }
2271
2272     paned = Gtk::HPaned.new
2273     paned.pack1(albums_sw, false, false)
2274     paned.pack2($notebook, true, true)
2275
2276     main_vbox = Gtk::VBox.new(false, 0)
2277     main_vbox.pack_start(mb, false, false)
2278     main_vbox.pack_start(tb, false, false)
2279     main_vbox.pack_start(paned, true, true)
2280     main_vbox.pack_end($statusbar = Gtk::Statusbar.new, false, false)
2281
2282     $main_window = Gtk::Window.new
2283     $main_window.add(main_vbox)
2284     $main_window.signal_connect('delete-event') {
2285         try_quit
2286     }
2287
2288     #- read/save size and position of window
2289     if $config['pos-x'] && $config['pos-y']
2290         $main_window.move($config['pos-x'].to_i, $config['pos-y'].to_i)
2291     else
2292         $main_window.window_position = Gtk::Window::POS_CENTER
2293     end
2294     msg 3, "size: #{$config['width']}x#{$config['height']}"
2295     $main_window.set_default_size(($config['width'] || 600).to_i, ($config['height'] || 400).to_i)
2296     $main_window.signal_connect('configure-event') {
2297         msg 3, "configure: pos: #{$main_window.window.root_origin.inspect} size: #{$main_window.window.size.inspect}"
2298         x, y = $main_window.window.root_origin
2299         width, height = $main_window.window.size
2300         $config['pos-x'] = x
2301         $config['pos-y'] = y
2302         $config['width'] = width
2303         $config['height'] = height
2304         false
2305     }
2306
2307     $statusbar.push(0, utf8(_("Ready.")))
2308     $main_window.show_all
2309 end
2310
2311 Thread.abort_on_exception = true
2312 handle_options
2313 read_config
2314
2315 Gtk.init
2316 create_main_window
2317 if ARGV[0]
2318     open_file(ARGV[0])
2319 end
2320 Gtk.main
2321
2322 write_config