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