06c9c4387d5bbf442373c59ad6bd6288f88249fb
[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-2008 Guillaume Cottenceau <http://zarb.org/~gc/resource/gc_mail.png>
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., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
21
22 require 'getoptlong'
23 require 'tempfile'
24 require 'thread'
25
26 require 'gtk2'
27 require 'booh/libadds'
28 require 'booh/GtkAutoTable'
29
30 require 'gettext'
31 include GetText
32 bindtextdomain("booh")
33
34 require 'booh/rexml/document'
35 include REXML
36
37 require 'booh/booh-lib'
38 include Booh
39 require 'booh/UndoHandler'
40 require 'booh/Synchronizator'
41
42
43 #- options
44 $options = [
45     [ '--help',          '-h', GetoptLong::NO_ARGUMENT,       _("Get help message") ],
46
47     [ '--verbose-level', '-v', GetoptLong::REQUIRED_ARGUMENT, _("Set max verbosity level (0: errors, 1: warnings, 2: important messages, 3: other messages)") ],
48 ]
49
50 #- default values for some globals 
51 $xmldir = nil
52 $modified = false
53 $current_cursor = nil
54 $ignore_videos = false
55 $button1_pressed_autotable = false
56 $generated_outofline = false
57
58 def usage
59     puts _("Usage: %s [OPTION]...") % File.basename($0)
60     $options.each { |ary|
61         printf " %3s, %-15s %s\n", ary[1], ary[0], ary[3]
62     }
63 end
64
65 def handle_options
66     parser = GetoptLong.new
67     parser.set_options(*$options.collect { |ary| ary[0..2] })
68     begin
69         parser.each_option do |name, arg|
70             case name
71             when '--help'
72                 usage
73                 exit(0)
74
75             when '--verbose-level'
76                 $verbose_level = arg.to_i
77
78             end
79         end
80     rescue
81         puts $!
82         usage
83         exit(1)
84     end
85 end
86
87 def read_config
88     $config = {}
89     $config_file = File.expand_path('~/.booh-gui-rc')
90     if File.readable?($config_file)
91         $xmldoc = REXML::Document.new(File.new($config_file))
92         $xmldoc.root.elements.each { |element|
93             txt = element.get_text
94             if txt
95                 if txt.value =~ /~~~/ || element.name == 'last-opens'
96                     $config[element.name] = txt.value.split(/~~~/)
97                 else
98                     $config[element.name] = txt.value
99                 end
100             elsif element.elements.size == 0
101                 $config[element.name] = ''
102             else
103                 $config[element.name] = {}
104                 element.each { |chld|
105                     txt = chld.get_text
106                     $config[element.name][chld.name] = txt ? txt.value : nil
107                 }
108             end
109         }
110     end
111     $config['video-viewer'] ||= '/usr/bin/mplayer %f'
112     $config['image-editor'] ||= '/usr/bin/gimp-remote %f'
113     $config['browser'] ||= "/usr/bin/mozilla-firefox -remote 'openURL(%f,new-window)' || /usr/bin/mozilla-firefox %f"
114     $config['comments-format'] ||= '%t'
115     if !FileTest.directory?(File.expand_path('~/.booh'))
116         system("mkdir ~/.booh")
117     end
118     if $config['mproc'].nil?
119         cpus = 0
120         for line in IO.readlines('/proc/cpuinfo') do
121             line =~ /^processor/ and cpus += 1
122         end
123         if cpus > 1
124             $config['mproc'] = cpus
125         end
126     end
127     $config['rotate-set-exif'] ||= 'true'
128     $tempfiles = []
129     $todelete = []
130 end
131
132 def check_config
133     if !system("which convert >/dev/null 2>/dev/null")
134         show_popup($main_window, utf8(_("The program 'convert' is needed. Please install it.
135 It is generally available with the 'ImageMagick' software package.")), { :pos_centered => true })
136         exit 1
137     end
138     if !system("which identify >/dev/null 2>/dev/null")
139         show_popup($main_window, utf8(_("The program 'identify' is needed to get images sizes and EXIF data. Please install it.
140 It is generally available with the 'ImageMagick' software package.")), { :pos_centered => true })
141     end
142     if !system("which exif >/dev/null 2>/dev/null")
143         show_popup($main_window, utf8(_("The program 'exif' is needed to view EXIF data. Please install it.")), { :pos_centered => true })
144     end
145     missing = %w(mplayer).delete_if { |prg| system("which #{prg} >/dev/null 2>/dev/null") }
146     if missing != []
147         show_popup($main_window, utf8(_("The following program(s) are needed to handle videos: '%s'. Videos will be ignored.") % missing.join(', ')), { :pos_centered => true })
148     end
149
150     viewer_binary = $config['video-viewer'].split.first
151     if viewer_binary && !File.executable?(viewer_binary)
152         show_popup($main_window, utf8(_("The configured video viewer seems to be unavailable.
153 You should fix this in Edit/Preferences so that you can view videos.
154
155 Problem was: '%s' is not an executable file.
156 Hint: don't forget to specify the full path to the executable,
157 e.g. '/usr/bin/mplayer' is correct but 'mplayer' only is not.") % viewer_binary), { :pos_centered => true, :not_transient => true })
158     end
159     image_editor_binary = $config['image-editor'].split.first
160     if image_editor_binary && !File.executable?(image_editor_binary)
161         show_popup($main_window, utf8(_("The configured image editor seems to be unavailable.
162 You should fix this in Edit/Preferences so that you can edit images externally.
163
164 Problem was: '%s' is not an executable file.
165 Hint: don't forget to specify the full path to the executable,
166 e.g. '/usr/bin/gimp-remote' is correct but 'gimp-remote' only is not.") % image_editor_binary), { :pos_centered => true, :not_transient => true })
167     end
168     browser_binary = $config['browser'].split.first
169     if browser_binary && !File.executable?(browser_binary)
170         show_popup($main_window, utf8(_("The configured browser seems to be unavailable.
171 You should fix this in Edit/Preferences so that you can open URLs.
172
173 Problem was: '%s' is not an executable file.") % browser_binary), { :pos_centered => true, :not_transient => true })
174     end
175 end
176
177 def write_config
178     if $config['last-opens'] && $config['last-opens'].size > 10
179         $config['last-opens'] = $config['last-opens'][-10, 10]
180     end
181
182     ios = File.open($config_file, "w")
183     $xmldoc = Document.new "<booh-gui-rc version='#{$VERSION}'/>"
184     $xmldoc << XMLDecl.new(XMLDecl::DEFAULT_VERSION, $CURRENT_CHARSET)
185     $config.each_pair { |key, value|
186         elem = $xmldoc.root.add_element key
187         if value.is_a? Hash
188             $config[key].each_pair { |subkey, subvalue|
189                 subelem = elem.add_element subkey
190                 subelem.add_text subvalue.to_s
191             }
192         elsif value.is_a? Array
193             elem.add_text value.join('~~~')
194         else
195             if !value
196                 elem.remove
197             else
198                 elem.add_text value.to_s
199             end
200         end
201     }
202     $xmldoc.write(ios, 0)
203     ios.close
204
205     $tempfiles.each { |f|
206         if File.exists?(f)
207             File.delete(f)
208         end
209     }
210 end
211
212 def set_mousecursor(what, *widget)
213     cursor = what.nil? ? nil : Gdk::Cursor.new(what)
214     if widget[0] && widget[0].window
215         widget[0].window.cursor = cursor
216     end
217     if $main_window && $main_window.window
218         $main_window.window.cursor = cursor
219     end
220     $current_cursor = what
221 end
222 def set_mousecursor_wait(*widget)
223     gtk_thread_protect { set_mousecursor(Gdk::Cursor::WATCH, *widget) }
224     if Thread.current == Thread.main
225         Gtk.main_iteration while Gtk.events_pending?
226     end
227 end
228 def set_mousecursor_normal(*widget)
229     gtk_thread_protect { set_mousecursor($save_cursor = nil, *widget) }
230 end
231 def push_mousecursor_wait(*widget)
232     if $current_cursor != Gdk::Cursor::WATCH
233         $save_cursor = $current_cursor
234         gtk_thread_protect { set_mousecursor_wait(*widget) }
235     end
236 end
237 def pop_mousecursor(*widget)
238     gtk_thread_protect { set_mousecursor($save_cursor || nil, *widget) }
239 end
240
241 def current_dest_dir
242     source = $xmldoc.root.attributes['source']
243     dest = $xmldoc.root.attributes['destination']
244     return make_dest_filename(from_utf8($current_path.sub(/^#{Regexp.quote(source)}/, dest)))
245 end
246
247 def full_src_dir_to_rel(path, source)
248     return path.sub(/^#{Regexp.quote(from_utf8(source))}/, '')
249 end
250
251 def build_full_dest_filename(filename)
252     return current_dest_dir + '/' + make_dest_filename(from_utf8(filename))
253 end
254
255 def save_undo(name, closure, *params)
256     UndoHandler.save_undo(name, closure, [ *params ])
257     $undo_tb.sensitive = $undo_mb.sensitive = true
258     $redo_tb.sensitive = $redo_mb.sensitive = false
259 end
260
261 def view_element(filename, closures)
262     if entry2type(filename) == 'video'
263         cmd = from_utf8($config['video-viewer']).gsub('%f', "'#{from_utf8($current_path + '/' + filename)}'") + ' &'
264         msg 2, cmd
265         system(cmd)
266         return
267     end
268
269     w = create_window.set_title(filename)
270
271     msg 3, "filename: #{filename}"
272     dest_img = build_full_dest_filename(filename).sub(/\.[^\.]+$/, '') + "-#{$default_size['fullscreen']}.jpg"
273     #- typically this file won't exist in case of videos; try with the largest thumbnail around
274     if !File.exists?(dest_img)
275         if entry2type(filename) == 'video'
276             alternatives = Dir[build_full_dest_filename(filename).sub(/\.[^\.]+$/, '') + '-*'].sort
277             if not alternatives.empty?
278                 dest_img = alternatives[-1]
279             end
280         else
281             push_mousecursor_wait
282             gen_thumbnails_element(from_utf8("#{$current_path}/#{filename}"), $xmldir, false, [ { 'filename' => dest_img, 'size' => $default_size['fullscreen'] } ])
283             pop_mousecursor
284             if !File.exists?(dest_img)
285                 msg 2, _("Could not generate fullscreen thumbnail!")
286                 return
287                 end
288         end
289     end
290     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)))
291     evt.signal_connect('button-press-event') { |this, event|
292         if event.event_type == Gdk::Event::BUTTON_PRESS && event.button == 1
293             $config['nogestures'] or $gesture_press = { :x => event.x, :y => event.y }
294         end
295         if event.event_type == Gdk::Event::BUTTON_PRESS && event.button == 3
296             menu = Gtk::Menu.new
297             menu.append(delete_item  = Gtk::ImageMenuItem.new(Gtk::Stock::DELETE))
298             delete_item.signal_connect('activate') {
299                 w.destroy
300                 closures[:delete].call(false)
301             }
302             menu.show_all
303             menu.popup(nil, nil, event.button, event.time)
304         end
305     }
306     evt.signal_connect('button-release-event') { |this, event|
307         if $gesture_press
308             if (($gesture_press[:y]-event.y)/($gesture_press[:x]-event.x)).abs > 2 && event.y-$gesture_press[:y] > 5
309                 msg 3, "gesture delete: click-drag right button to the bottom"
310                 w.destroy
311                 closures[:delete].call(false)
312                 $statusbar.push(0, utf8(_("Mouse gesture: delete.")))
313             end
314         end
315     }
316     tooltips = Gtk::Tooltips.new
317     tooltips.set_tip(evt, File.basename(filename).gsub(/\.jpg/, ''), nil)
318
319     w.signal_connect('key-press-event') { |w,event|
320         if event.state & Gdk::Window::CONTROL_MASK != 0 && event.keyval == Gdk::Keyval::GDK_Delete
321             w.destroy
322             closures[:delete].call(false)
323         end
324     }
325
326     bottom = Gtk::Alignment.new(0.5, 0.5, 0, 0).add(b = Gtk::Button.new(Gtk::Stock::CLOSE))
327     b.signal_connect('clicked') { w.destroy }
328
329     vb = Gtk::VBox.new
330     vb.pack_start(evt, false, false)
331     vb.pack_end(bottom, false, false)
332
333     w.add(vb)
334     w.signal_connect('delete-event') { w.destroy }
335     w.window_position = Gtk::Window::POS_CENTER
336     w.show_all
337 end
338
339 def scroll_upper(scrolledwindow, ypos_top)
340     newval = scrolledwindow.vadjustment.value -
341         ((scrolledwindow.vadjustment.value - ypos_top - 1) / scrolledwindow.vadjustment.step_increment + 1) * scrolledwindow.vadjustment.step_increment
342     if newval < scrolledwindow.vadjustment.lower
343         newval = scrolledwindow.vadjustment.lower
344     end
345     scrolledwindow.vadjustment.value = newval
346 end
347
348 def scroll_lower(scrolledwindow, ypos_bottom)
349     newval = scrolledwindow.vadjustment.value +
350         ((ypos_bottom - (scrolledwindow.vadjustment.value + scrolledwindow.vadjustment.page_size) - 1) / scrolledwindow.vadjustment.step_increment + 1) * scrolledwindow.vadjustment.step_increment
351     if newval > scrolledwindow.vadjustment.upper - scrolledwindow.vadjustment.page_size
352         newval = scrolledwindow.vadjustment.upper - scrolledwindow.vadjustment.page_size
353     end
354     scrolledwindow.vadjustment.value = newval
355 end
356
357 def autoscroll_if_needed(scrolledwindow, image, textview)
358     #- autoscroll if cursor or image is not visible, if possible
359     if image && image.window || textview.window
360         ypos_top = (image && image.window) ? image.window.position[1] : textview.window.position[1]
361         ypos_bottom = max(textview.window.position[1] + textview.window.size[1], image && image.window ? image.window.position[1] + image.window.size[1] : -1)
362         current_miny_visible = scrolledwindow.vadjustment.value
363         current_maxy_visible = scrolledwindow.vadjustment.value + scrolledwindow.vadjustment.page_size
364         if ypos_top < current_miny_visible
365             scroll_upper(scrolledwindow, ypos_top)
366         elsif ypos_bottom > current_maxy_visible
367             scroll_lower(scrolledwindow, ypos_bottom)
368         end
369     end
370 end
371
372 def create_editzone(scrolledwindow, pagenum, image)
373     frame = Gtk::Frame.new
374     frame.add(textview = Gtk::TextView.new.set_wrap_mode(Gtk::TextTag::WRAP_WORD))
375     frame.set_shadow_type(Gtk::SHADOW_IN)
376     textview.signal_connect('key-press-event') { |w, event|
377         textview.set_editable(event.keyval != Gdk::Keyval::GDK_Tab && event.keyval != Gdk::Keyval::GDK_ISO_Left_Tab)
378         if event.keyval == Gdk::Keyval::GDK_Page_Up || event.keyval == Gdk::Keyval::GDK_Page_Down
379             scrolledwindow.signal_emit('key-press-event', event)
380         end
381         if (event.keyval == Gdk::Keyval::GDK_Up || event.keyval == Gdk::Keyval::GDK_Down) &&
382            event.state & (Gdk::Window::CONTROL_MASK | Gdk::Window::SHIFT_MASK | Gdk::Window::MOD1_MASK) == 0
383             if event.keyval == Gdk::Keyval::GDK_Up
384                 if scrolledwindow.vadjustment.value >= scrolledwindow.vadjustment.lower + scrolledwindow.vadjustment.step_increment
385                     scrolledwindow.vadjustment.value -= scrolledwindow.vadjustment.step_increment
386                 else
387                     scrolledwindow.vadjustment.value = scrolledwindow.vadjustment.lower
388                 end
389             else
390                 if scrolledwindow.vadjustment.value <= scrolledwindow.vadjustment.upper - scrolledwindow.vadjustment.step_increment - scrolledwindow.vadjustment.page_size
391                     scrolledwindow.vadjustment.value += scrolledwindow.vadjustment.step_increment
392                 else
393                     scrolledwindow.vadjustment.value = scrolledwindow.vadjustment.upper - scrolledwindow.vadjustment.page_size
394                 end
395             end
396         end
397         false  #- propagate
398     }
399
400     candidate_undo_text = nil
401     textview.signal_connect('focus-in-event') { |w, event|
402         textview.buffer.select_range(textview.buffer.get_iter_at_offset(0), textview.buffer.get_iter_at_offset(-1))
403         candidate_undo_text = textview.buffer.text
404         false  #- propagate
405     }
406
407     textview.signal_connect('key-release-event') { |w, event|
408         if candidate_undo_text && candidate_undo_text != textview.buffer.text
409             $modified = true
410             save_undo(_("text edit"),
411                       proc { |text|
412                           save_text = textview.buffer.text
413                           textview.buffer.text = text
414                           textview.grab_focus
415                           $notebook.set_page(pagenum)
416                           proc {
417                               textview.buffer.text = save_text
418                               textview.grab_focus
419                               $notebook.set_page(pagenum)
420                           }
421                       }, candidate_undo_text)
422             candidate_undo_text = nil
423         end
424
425         if event.state != 0 || ![Gdk::Keyval::GDK_Page_Up, Gdk::Keyval::GDK_Page_Down, Gdk::Keyval::GDK_Up, Gdk::Keyval::GDK_Down].include?(event.keyval)
426             autoscroll_if_needed(scrolledwindow, image, textview)
427         end
428         false  #- propagate
429     }
430
431     return [ frame, textview ]
432 end
433
434 def update_shown_pixbuf(thumbnail_img, img, desired_x, desired_y)
435
436     if !$modified_pixbufs[thumbnail_img]
437         $modified_pixbufs[thumbnail_img] = { :orig => img.pixbuf }
438     elsif !$modified_pixbufs[thumbnail_img][:orig]
439         $modified_pixbufs[thumbnail_img][:orig] = img.pixbuf
440     end
441
442     pixbuf = $modified_pixbufs[thumbnail_img][:orig].dup
443
444     #- rotate
445     if $modified_pixbufs[thumbnail_img][:angle_to_orig] && $modified_pixbufs[thumbnail_img][:angle_to_orig] != 0
446         pixbuf = rotate_pixbuf(pixbuf, $modified_pixbufs[thumbnail_img][:angle_to_orig])
447         msg 3, "sizes: #{pixbuf.width} #{pixbuf.height} - desired #{desired_x}x#{desired_x}"
448         if pixbuf.height > desired_y
449             pixbuf = pixbuf.scale(pixbuf.width * (desired_y.to_f/pixbuf.height), desired_y, Gdk::Pixbuf::INTERP_BILINEAR)
450         elsif pixbuf.width < desired_x && pixbuf.height < desired_y
451             pixbuf = pixbuf.scale(desired_x, pixbuf.height * (desired_x.to_f/pixbuf.width), Gdk::Pixbuf::INTERP_BILINEAR)
452         end
453     end
454
455     #- fix white balance
456     if $modified_pixbufs[thumbnail_img][:whitebalance]
457         pixbuf.whitebalance!($modified_pixbufs[thumbnail_img][:whitebalance])
458     end
459
460     #- fix gamma correction
461     if $modified_pixbufs[thumbnail_img][:gammacorrect]
462         pixbuf.gammacorrect!($modified_pixbufs[thumbnail_img][:gammacorrect])
463     end
464
465     img.pixbuf = $modified_pixbufs[thumbnail_img][:pixbuf] = pixbuf
466 end
467
468 def rotate_real(angle, thumbnail_img, img, xmlelem, attributes_prefix, desired_x, desired_y)
469     $modified = true
470
471     #- update rotate attribute
472     new_angle = (xmlelem.attributes["#{attributes_prefix}rotate"].to_i + angle) % 360
473     xmlelem.add_attribute("#{attributes_prefix}rotate", new_angle.to_s)
474
475     if $config['rotate-set-exif'] == 'true'
476         Exif.set_orientation(from_utf8($current_path + '/' + xmlelem.attributes['filename']), angle_to_exif_orientation(new_angle))
477     end
478
479     $modified_pixbufs[thumbnail_img] ||= {}
480     $modified_pixbufs[thumbnail_img][:angle_to_orig] = (($modified_pixbufs[thumbnail_img][:angle_to_orig] || 0) + angle) % 360
481     msg 3, "angle: #{angle}, angle to orig: #{$modified_pixbufs[thumbnail_img][:angle_to_orig]}"
482
483     update_shown_pixbuf(thumbnail_img, img, desired_x, desired_y)
484 end
485
486 def rotate(angle, thumbnail_img, img, xmlelem, attributes_prefix, desired_x, desired_y)
487     $modified = true
488
489     rotate_real(angle, thumbnail_img, img, xmlelem, attributes_prefix, desired_x, desired_y)
490
491     save_undo(angle == 90 ? _("rotate clockwise") : angle == -90 ? _("rotate counter-clockwise") : _("flip upside-down"),
492               proc { |angle|
493                   rotate_real(angle, thumbnail_img, img, xmlelem, attributes_prefix, desired_x, desired_y)
494                   $notebook.set_page(attributes_prefix != '' ? 0 : 1)
495                   proc {
496                       rotate_real(-angle, thumbnail_img, img, xmlelem, attributes_prefix, desired_x, desired_y)
497                       $notebook.set_page(0)
498                       $notebook.set_page(attributes_prefix != '' ? 0 : 1)
499                   }
500               }, -angle)
501 end
502
503 def color_swap(xmldir, attributes_prefix)
504     $modified = true
505     if xmldir.attributes["#{attributes_prefix}color-swap"]
506         xmldir.delete_attribute("#{attributes_prefix}color-swap")
507     else
508         xmldir.add_attribute("#{attributes_prefix}color-swap", '1')
509     end
510 end
511
512 def enhance(xmldir, attributes_prefix)
513     $modified = true
514     if xmldir.attributes["#{attributes_prefix}enhance"]
515         xmldir.delete_attribute("#{attributes_prefix}enhance")
516     else
517         xmldir.add_attribute("#{attributes_prefix}enhance", '1')
518     end
519 end
520
521 def change_seektime(xmldir, attributes_prefix, value)
522     $modified = true
523     xmldir.add_attribute("#{attributes_prefix}seektime", value)
524 end
525
526 def ask_new_seektime(xmldir, attributes_prefix)
527     if xmldir
528         value = xmldir.attributes["#{attributes_prefix}seektime"]
529     else
530         value = ''
531     end
532
533     dialog = Gtk::Dialog.new(utf8(_("Change seek time")),
534                              $main_window,
535                              Gtk::Dialog::MODAL | Gtk::Dialog::DESTROY_WITH_PARENT,
536                              [Gtk::Stock::OK, Gtk::Dialog::RESPONSE_OK],
537                              [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
538
539     lbl = Gtk::Label.new
540     lbl.markup = utf8(
541 _("Please specify the <b>seek time</b> of the video, to take the thumbnail
542 from, in seconds.
543 "))
544     dialog.vbox.add(lbl)
545     dialog.vbox.add(entry = Gtk::Entry.new.set_text(value))
546     entry.signal_connect('key-press-event') { |w, event|
547         if event.keyval == Gdk::Keyval::GDK_Return
548             dialog.response(Gtk::Dialog::RESPONSE_OK)
549             true
550         elsif event.keyval == Gdk::Keyval::GDK_Escape
551             dialog.response(Gtk::Dialog::RESPONSE_CANCEL)
552             true
553         else
554             false  #- propagate if needed
555         end
556     }
557
558     dialog.window_position = Gtk::Window::POS_MOUSE
559     dialog.show_all
560
561     dialog.run { |response|
562         newval = entry.text
563         dialog.destroy
564         if response == Gtk::Dialog::RESPONSE_OK
565             $modified = true
566             msg 3, "changing seektime to #{newval}"
567             return { :old => value, :new => newval }
568         else
569             return nil
570         end
571     }
572 end
573
574 def change_pano_amount(xmldir, attributes_prefix, value)
575     $modified = true
576     if value.nil?
577         xmldir.delete_attribute("#{attributes_prefix}pano-amount")
578     else
579         xmldir.add_attribute("#{attributes_prefix}pano-amount", value.to_s)
580     end
581 end
582
583 def ask_new_pano_amount(xmldir, attributes_prefix)
584     if xmldir
585         value = xmldir.attributes["#{attributes_prefix}pano-amount"]
586     else
587         value = nil
588     end
589
590     dialog = Gtk::Dialog.new(utf8(_("Specify panorama amount")),
591                              $main_window,
592                              Gtk::Dialog::MODAL | Gtk::Dialog::DESTROY_WITH_PARENT,
593                              [Gtk::Stock::OK, Gtk::Dialog::RESPONSE_OK],
594                              [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
595
596     lbl = Gtk::Label.new
597     lbl.markup = utf8(
598 _("Please specify the <b>panorama 'amount'</b> of the image, which indicates the width
599 of this panorama image compared to other regular images. For example, if the panorama
600 was taken out of four photos on one row, counting the necessary overlap, the width of
601 this panorama image should probably be roughly three times the width of regular images.
602
603 With this information, booh will be able to generate panorama thumbnails looking
604 the right 'size'.
605 "))
606     dialog.vbox.add(lbl)
607     dialog.vbox.add(Gtk::Alignment.new(0.5, 0.5, 0, 0).add(Gtk::HBox.new.add(rb_no = Gtk::RadioButton.new(utf8(_("none (not a panorama image)")))).
608                                                                          add(rb_yes = Gtk::RadioButton.new(rb_no, utf8(_("amount of: ")))).
609                                                                          add(spin = Gtk::SpinButton.new(1, 8, 0.1)).
610                                                                          add(Gtk::Label.new(utf8(_("times the width of other images"))))))
611     spin.signal_connect('value-changed') {
612         rb_yes.active = true
613     }
614     dialog.window_position = Gtk::Window::POS_MOUSE
615     dialog.show_all
616     if value
617         spin.value = value.to_f
618         rb_yes.active = true
619         spin.grab_focus
620     else
621         rb_no.active = true
622     end
623
624     dialog.run { |response|
625         if rb_no.active?
626             newval = nil
627         else
628             newval = spin.value.to_f
629         end
630         dialog.destroy
631         if response == Gtk::Dialog::RESPONSE_OK
632             $modified = true
633             msg 3, "changing panorama amount to #{newval}"
634             return { :old => value, :new => newval }
635         else
636             return nil
637         end
638     }
639 end
640
641 def change_whitebalance(xmlelem, attributes_prefix, value)
642     $modified = true
643     xmlelem.add_attribute("#{attributes_prefix}white-balance", value)
644 end
645
646 def recalc_whitebalance(level, orig, thumbnail_img, img, xmlelem, attributes_prefix, desired_x, desired_y, infotype)
647
648     #- in case the white balance was already modified in the config file and thumbnail, we cannot just revert, we need to use original file
649     if (!$modified_pixbufs[thumbnail_img] || !$modified_pixbufs[thumbnail_img][:whitebalance]) && xmlelem.attributes["#{attributes_prefix}white-balance"]
650         save_gammacorrect = xmlelem.attributes["#{attributes_prefix}gamma-correction"]
651         xmlelem.delete_attribute("#{attributes_prefix}gamma-correction")
652         save_whitebalance = xmlelem.attributes["#{attributes_prefix}white-balance"]
653         xmlelem.delete_attribute("#{attributes_prefix}white-balance")
654         destfile = "#{thumbnail_img}-orig-gammacorrect-whitebalance.jpg"
655         gen_real_thumbnail_core(attributes_prefix == '' ? 'element' : 'subdir', orig, destfile,
656                                 xmlelem, attributes_prefix == '' ? $default_size['thumbnails'] : $albums_thumbnail_size, infotype)
657         $modified_pixbufs[thumbnail_img] ||= {}
658         $modified_pixbufs[thumbnail_img][:orig] = pixbuf_or_nil(destfile)
659         xmlelem.add_attribute("#{attributes_prefix}white-balance", save_whitebalance)
660         if save_gammacorrect
661             xmlelem.add_attribute("#{attributes_prefix}gamma-correction", save_gammacorrect)
662             $modified_pixbufs[thumbnail_img][:gammacorrect] = save_gammacorrect.to_f
663         end
664         $modified_pixbufs[thumbnail_img][:angle_to_orig] = 0
665     end
666
667     $modified_pixbufs[thumbnail_img] ||= {}
668     $modified_pixbufs[thumbnail_img][:whitebalance] = level.to_f
669
670     update_shown_pixbuf(thumbnail_img, img, desired_x, desired_y)
671 end
672
673 def ask_whitebalance(orig, thumbnail_img, img_, xmlelem, attributes_prefix, desired_x, desired_y, infotype)
674     #- init $modified_pixbufs correctly
675 #    update_shown_pixbuf(thumbnail_img, img_, desired_x, desired_y)
676
677     value = xmlelem ? (xmlelem.attributes["#{attributes_prefix}white-balance"] || "0") : "0"
678
679     dialog = Gtk::Dialog.new(utf8(_("Fix white balance")),
680                              $main_window,
681                              Gtk::Dialog::MODAL | Gtk::Dialog::DESTROY_WITH_PARENT,
682                              [Gtk::Stock::OK, Gtk::Dialog::RESPONSE_OK],
683                              [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
684
685     lbl = Gtk::Label.new
686     lbl.markup = utf8(
687 _("You can fix the <b>white balance</b> of the image, if your image is too blue
688 or too yellow because your camera didn't detect the light correctly. Drag the
689 slider below the image to the left for more blue, to the right for more yellow.
690 "))
691     dialog.vbox.add(lbl)
692     if img_
693         dialog.vbox.add(evt = Gtk::EventBox.new.add(img = Gtk::Image.new(img_.pixbuf)))
694     end
695     dialog.vbox.add(hs = Gtk::HScale.new(-200, 200, 1).set_value(value.to_i))
696     
697     dialog.window_position = Gtk::Window::POS_MOUSE
698     dialog.show_all
699
700     lastval = nil
701     timeout = Gtk.timeout_add(100) {
702         if hs.value != lastval
703             lastval = hs.value
704             if img_
705                 recalc_whitebalance(lastval, orig, thumbnail_img, img, xmlelem, attributes_prefix, desired_x, desired_y, infotype)
706             end
707         end
708         true
709     }
710
711     dialog.run { |response|
712         Gtk.timeout_remove(timeout)
713         if response == Gtk::Dialog::RESPONSE_OK
714             $modified = true
715             newval = hs.value.to_s
716             msg 3, "changing white balance to #{newval}"
717             dialog.destroy
718             return { :old => value, :new => newval }
719         else
720             if thumbnail_img
721                 $modified_pixbufs[thumbnail_img] ||= {}
722                 $modified_pixbufs[thumbnail_img][:whitebalance] = value.to_f
723                 $modified_pixbufs[thumbnail_img][:pixbuf] = img_.pixbuf
724             end
725             dialog.destroy
726             return nil
727         end
728     }
729 end
730
731 def change_gammacorrect(xmlelem, attributes_prefix, value)
732     $modified = true
733     xmlelem.add_attribute("#{attributes_prefix}gamma-correction", value)
734 end
735
736 def recalc_gammacorrect(level, orig, thumbnail_img, img, xmlelem, attributes_prefix, desired_x, desired_y, infotype)
737
738     #- in case the gamma correction was already modified in the config file and thumbnail, we cannot just revert, we need to use original file
739     if (!$modified_pixbufs[thumbnail_img] || !$modified_pixbufs[thumbnail_img][:gammacorrect]) && xmlelem.attributes["#{attributes_prefix}gamma-correction"]
740         save_gammacorrect = xmlelem.attributes["#{attributes_prefix}gamma-correction"]
741         xmlelem.delete_attribute("#{attributes_prefix}gamma-correction")
742         save_whitebalance = xmlelem.attributes["#{attributes_prefix}white-balance"]
743         xmlelem.delete_attribute("#{attributes_prefix}white-balance")
744         destfile = "#{thumbnail_img}-orig-gammacorrect-whitebalance.jpg"
745         gen_real_thumbnail_core(attributes_prefix == '' ? 'element' : 'subdir', orig, destfile,
746                                 xmlelem, attributes_prefix == '' ? $default_size['thumbnails'] : $albums_thumbnail_size, infotype)
747         $modified_pixbufs[thumbnail_img] ||= {}
748         $modified_pixbufs[thumbnail_img][:orig] = pixbuf_or_nil(destfile)
749         xmlelem.add_attribute("#{attributes_prefix}gamma-correction", save_gammacorrect)
750         if save_whitebalance
751             xmlelem.add_attribute("#{attributes_prefix}white-balance", save_whitebalance)
752             $modified_pixbufs[thumbnail_img][:whitebalance] = save_whitebalance.to_f
753         end
754         $modified_pixbufs[thumbnail_img][:angle_to_orig] = 0
755     end
756
757     $modified_pixbufs[thumbnail_img] ||= {}
758     $modified_pixbufs[thumbnail_img][:gammacorrect] = level.to_f
759
760     update_shown_pixbuf(thumbnail_img, img, desired_x, desired_y)
761 end
762
763 def ask_gammacorrect(orig, thumbnail_img, img_, xmlelem, attributes_prefix, desired_x, desired_y, infotype)
764     #- init $modified_pixbufs correctly
765 #    update_shown_pixbuf(thumbnail_img, img_, desired_x, desired_y)
766
767     value = xmlelem ? (xmlelem.attributes["#{attributes_prefix}gamma-correction"] || "0") : "0"
768
769     dialog = Gtk::Dialog.new(utf8(_("Gamma correction")),
770                              $main_window,
771                              Gtk::Dialog::MODAL | Gtk::Dialog::DESTROY_WITH_PARENT,
772                              [Gtk::Stock::OK, Gtk::Dialog::RESPONSE_OK],
773                              [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
774
775     lbl = Gtk::Label.new
776     lbl.markup = utf8(
777 _("You can perform <b>gamma correction</b> of the image, if your image is too dark
778 or too bright. Drag the slider below the image.
779 "))
780     dialog.vbox.add(lbl)
781     if img_
782         dialog.vbox.add(evt = Gtk::EventBox.new.add(img = Gtk::Image.new(img_.pixbuf)))
783     end
784     dialog.vbox.add(hs = Gtk::HScale.new(-200, 200, 1).set_value(value.to_i))
785     
786     dialog.window_position = Gtk::Window::POS_MOUSE
787     dialog.show_all
788
789     lastval = nil
790     timeout = Gtk.timeout_add(100) {
791         if hs.value != lastval
792             lastval = hs.value
793             if img_
794                 recalc_gammacorrect(lastval, orig, thumbnail_img, img, xmlelem, attributes_prefix, desired_x, desired_y, infotype)
795             end
796         end
797         true
798     }
799
800     dialog.run { |response|
801         Gtk.timeout_remove(timeout)
802         if response == Gtk::Dialog::RESPONSE_OK
803             $modified = true
804             newval = hs.value.to_s
805             msg 3, "gamma correction to #{newval}"
806             dialog.destroy
807             return { :old => value, :new => newval }
808         else
809             if thumbnail_img
810                 $modified_pixbufs[thumbnail_img] ||= {}
811                 $modified_pixbufs[thumbnail_img][:gammacorrect] = value.to_f
812                 $modified_pixbufs[thumbnail_img][:pixbuf] = img_.pixbuf
813             end
814             dialog.destroy
815             return nil
816         end
817     }
818 end
819
820 def gen_real_thumbnail_core(type, origfile, destfile, xmldir, size, infotype)
821     if File.exists?(destfile)
822         File.delete(destfile)
823     end
824     #- type can be 'element' or 'subdir'
825     if type == 'element'
826         gen_thumbnails_element(origfile, xmldir, false, [ { 'filename' => destfile, 'size' => size } ])
827     else
828         gen_thumbnails_subdir(origfile, xmldir, false, [ { 'filename' => destfile, 'size' => size } ], infotype)
829     end
830 end
831
832 def gen_real_thumbnail(type, origfile, destfile, xmldir, size, img, infotype)
833     Thread.new {
834         push_mousecursor_wait
835         gen_real_thumbnail_core(type, origfile, destfile, xmldir, size, infotype)
836         gtk_thread_protect {
837             img.set(destfile)
838             $modified_pixbufs[destfile] = { :orig => img.pixbuf, :pixbuf => img.pixbuf, :angle_to_orig => 0 }
839         }
840         pop_mousecursor
841     }
842 end
843
844 def popup_thumbnail_menu(event, optionals, fullpath, type, xmldir, attributes_prefix, possible_actions, closures)
845     distribute_multiple_call = Proc.new { |action, arg|
846         $selected_elements.each_key { |path|
847             $name2widgets[path][:img].pixbuf = $selected_elements[path][:pixbuf]
848         }
849         if possible_actions[:can_multiple] && $selected_elements.length > 0
850             UndoHandler.begin_batch
851             $selected_elements.each_key { |k| $name2closures[k][action].call(arg) }
852             UndoHandler.end_batch
853         else
854             closures[action].call(arg)
855         end
856         $selected_elements = {}
857     }
858     menu = Gtk::Menu.new
859     if optionals.include?('change_image')
860         menu.append(changeimg = Gtk::ImageMenuItem.new(utf8(_("Change image"))))
861         changeimg.image = Gtk::Image.new("#{$FPATH}/images/stock-images-16.png")
862         changeimg.signal_connect('activate') { closures[:change].call }
863         menu.append(Gtk::SeparatorMenuItem.new)
864     end
865     if !possible_actions[:can_multiple] || $selected_elements.length == 0
866         if closures[:view]
867             if type == 'image'
868                 menu.append(view = Gtk::ImageMenuItem.new(utf8(_("View larger"))))
869                 view.image = Gtk::Image.new("#{$FPATH}/images/stock-view-16.png")
870                 view.signal_connect('activate') { closures[:view].call }
871             else
872                 menu.append(view = Gtk::ImageMenuItem.new(utf8(_("Play video"))))
873                 view.image = Gtk::Image.new("#{$FPATH}/images/stock-video-16.png")
874                 view.signal_connect('activate') { closures[:view].call }
875                 menu.append(Gtk::SeparatorMenuItem.new)
876             end
877         end
878         if type == 'image' && (!possible_actions[:can_multiple] || $selected_elements.length == 0)
879             menu.append(exif = Gtk::ImageMenuItem.new(utf8(_("View EXIF data"))))
880             exif.image = Gtk::Image.new("#{$FPATH}/images/stock-list-16.png")
881             exif.signal_connect('activate') { show_popup($main_window,
882                                                          utf8(`exif -m '#{fullpath}'`),
883                                                          { :title => utf8(_("EXIF data of %s") % File.basename(fullpath)), :nomarkup => true, :scrolled => true }) }
884             menu.append(Gtk::SeparatorMenuItem.new)
885         end
886     end
887     menu.append(r90 = Gtk::ImageMenuItem.new(utf8(_("Rotate clockwise"))))
888     r90.image = Gtk::Image.new("#{$FPATH}/images/stock-rotate-90-16.png")
889     r90.signal_connect('activate') { distribute_multiple_call.call(:rotate, 90) }
890     menu.append(r270 = Gtk::ImageMenuItem.new(utf8(_("Rotate counter-clockwise"))))
891     r270.image = Gtk::Image.new("#{$FPATH}/images/stock-rotate-270-16.png")
892     r270.signal_connect('activate') { distribute_multiple_call.call(:rotate, -90) }
893     if !possible_actions[:can_multiple] || $selected_elements.length == 0
894         menu.append(Gtk::SeparatorMenuItem.new)
895         if !possible_actions[:forbid_left]
896             menu.append(moveleft = Gtk::ImageMenuItem.new(utf8(_("Move left"))))
897             moveleft.image = Gtk::Image.new("#{$FPATH}/images/stock-move-left.png")
898             moveleft.signal_connect('activate') { closures[:move].call('left') }
899             if !possible_actions[:can_left]
900                 moveleft.sensitive = false
901             end
902         end
903         if !possible_actions[:forbid_right]
904             menu.append(moveright = Gtk::ImageMenuItem.new(utf8(_("Move right"))))
905             moveright.image = Gtk::Image.new("#{$FPATH}/images/stock-move-right.png")
906             moveright.signal_connect('activate') { closures[:move].call('right') }
907             if !possible_actions[:can_right]
908                 moveright.sensitive = false
909             end
910         end
911         if optionals.include?('move_top')
912             menu.append(movetop = Gtk::ImageMenuItem.new(utf8(_("Move top"))))
913             movetop.image = Gtk::Image.new("#{$FPATH}/images/move-top.png")
914             movetop.signal_connect('activate') { closures[:move].call('top') }
915             if !possible_actions[:can_top]
916                 movetop.sensitive = false
917             end
918         end
919         menu.append(moveup = Gtk::ImageMenuItem.new(utf8(_("Move up"))))
920         moveup.image = Gtk::Image.new("#{$FPATH}/images/stock-move-up.png")
921         moveup.signal_connect('activate') { closures[:move].call('up') }
922         if !possible_actions[:can_up]
923             moveup.sensitive = false
924         end
925         menu.append(movedown = Gtk::ImageMenuItem.new(utf8(_("Move down"))))
926         movedown.image = Gtk::Image.new("#{$FPATH}/images/stock-move-down.png")
927         movedown.signal_connect('activate') { closures[:move].call('down') }
928         if !possible_actions[:can_down]
929             movedown.sensitive = false
930         end
931         if optionals.include?('move_bottom')
932             menu.append(movebottom = Gtk::ImageMenuItem.new(utf8(_("Move bottom"))))
933             movebottom.image = Gtk::Image.new("#{$FPATH}/images/move-bottom.png")
934             movebottom.signal_connect('activate') { closures[:move].call('bottom') }
935             if !possible_actions[:can_bottom]
936                 movebottom.sensitive = false
937             end
938         end
939     end
940     if type == 'video'
941         if !possible_actions[:can_multiple] || $selected_elements.length == 0 || $selected_elements.reject { |k,v| $name2widgets[k][:type] == 'video' }.empty?
942             menu.append(Gtk::SeparatorMenuItem.new)
943 #            menu.append(color_swap = Gtk::ImageMenuItem.new(utf8(_("Red/blue color swap"))))
944 #            color_swap.image = Gtk::Image.new("#{$FPATH}/images/stock-color-triangle-16.png")
945 #            color_swap.signal_connect('activate') { distribute_multiple_call.call(:color_swap) }
946             menu.append(flip = Gtk::ImageMenuItem.new(utf8(_("Flip upside-down"))))
947             flip.image = Gtk::Image.new("#{$FPATH}/images/stock-rotate-180-16.png")
948             flip.signal_connect('activate') { distribute_multiple_call.call(:rotate, 180) }
949             menu.append(seektime = Gtk::ImageMenuItem.new(utf8(_("Specify seek time"))))
950             seektime.image = Gtk::Image.new("#{$FPATH}/images/stock-video-16.png")
951             seektime.signal_connect('activate') {
952                 if possible_actions[:can_multiple] && $selected_elements.length > 0
953                     if values = ask_new_seektime(nil, '')
954                         distribute_multiple_call.call(:seektime, values)
955                     end
956                 else
957                     closures[:seektime].call
958                 end
959             }
960         end
961     end
962     menu.append(               Gtk::SeparatorMenuItem.new)
963     menu.append(gammacorrect = Gtk::ImageMenuItem.new(utf8(_("Gamma correction"))))
964     gammacorrect.image = Gtk::Image.new("#{$FPATH}/images/stock-tool-brightness-contrast-16.png")
965     gammacorrect.signal_connect('activate') { 
966         if possible_actions[:can_multiple] && $selected_elements.length > 0
967             if values = ask_gammacorrect(nil, nil, nil, nil, '', nil, nil, '')
968                 distribute_multiple_call.call(:gammacorrect, values)
969             end
970         else
971             closures[:gammacorrect].call
972         end
973     }
974     menu.append(whitebalance = Gtk::ImageMenuItem.new(utf8(_("Fix white-balance"))))
975     whitebalance.image = Gtk::Image.new("#{$FPATH}/images/stock-tool-color-balance-16.png")
976     whitebalance.signal_connect('activate') { 
977         if possible_actions[:can_multiple] && $selected_elements.length > 0
978             if values = ask_whitebalance(nil, nil, nil, nil, '', nil, nil, '')
979                 distribute_multiple_call.call(:whitebalance, values)
980             end
981         else
982             closures[:whitebalance].call
983         end
984     }
985     if !possible_actions[:can_multiple] || $selected_elements.length == 0
986         menu.append(enhance = Gtk::ImageMenuItem.new(utf8(xmldir.attributes["#{attributes_prefix}enhance"] ? _("Original contrast") :
987                                                                                                              _("Enhance constrast"))))
988     else
989         menu.append(enhance = Gtk::ImageMenuItem.new(utf8(_("Toggle contrast enhancement"))))
990     end
991     enhance.image = Gtk::Image.new("#{$FPATH}/images/stock-channels-16.png")
992     enhance.signal_connect('activate') { distribute_multiple_call.call(:enhance) }
993     if type == 'image' && possible_actions[:can_panorama]
994         menu.append(panorama = Gtk::ImageMenuItem.new(utf8(_("Set as panorama"))))
995         panorama.image = Gtk::Image.new("#{$FPATH}/images/stock-images-16.png")
996         panorama.signal_connect('activate') {
997             if possible_actions[:can_multiple] && $selected_elements.length > 0
998                 if values = ask_new_pano_amount(nil, '')
999                     distribute_multiple_call.call(:pano, values)
1000                 end
1001             else
1002                 distribute_multiple_call.call(:pano)
1003             end
1004        }
1005     end
1006     menu.append(               Gtk::SeparatorMenuItem.new)
1007     if optionals.include?('delete')
1008         menu.append(cut_item     = Gtk::ImageMenuItem.new(Gtk::Stock::CUT))
1009         cut_item.signal_connect('activate') { distribute_multiple_call.call(:cut) }
1010         if !possible_actions[:can_multiple] || $selected_elements.length == 0
1011             menu.append(paste_item   = Gtk::ImageMenuItem.new(Gtk::Stock::PASTE))
1012             paste_item.signal_connect('activate') { closures[:paste].call }
1013             menu.append(clear_item   = Gtk::ImageMenuItem.new(Gtk::Stock::CLEAR))
1014             clear_item.signal_connect('activate') { $cuts = [] }
1015             if $cuts.size == 0
1016                 paste_item.sensitive = clear_item.sensitive = false
1017             end
1018         end
1019         menu.append(               Gtk::SeparatorMenuItem.new)
1020     end
1021     if type == 'image' && (! possible_actions[:can_multiple] || $selected_elements.length == 0)
1022         menu.append(editexternally = Gtk::ImageMenuItem.new(utf8(_("Edit image"))))
1023         editexternally.image = Gtk::Image.new("#{$FPATH}/images/stock-tool-ink-16.png")
1024         editexternally.signal_connect('activate') {
1025             cmd = from_utf8($config['image-editor']).gsub('%f', "'#{fullpath}'")
1026             msg 2, cmd
1027             system(cmd)
1028         }
1029     end
1030     menu.append(refresh_item = Gtk::ImageMenuItem.new(Gtk::Stock::REFRESH))
1031     refresh_item.signal_connect('activate') { distribute_multiple_call.call(:refresh) }
1032     if optionals.include?('delete')
1033         menu.append(delete_item  = Gtk::ImageMenuItem.new(Gtk::Stock::DELETE))
1034         delete_item.signal_connect('activate') { distribute_multiple_call.call(:delete) }
1035     end
1036     menu.show_all
1037     menu.popup(nil, nil, event.button, event.time)
1038 end
1039
1040 def delete_current_subalbum
1041     $modified = true
1042     sel = $albums_tv.selection.selected_rows
1043     $xmldir.elements.each { |e|
1044         if e.name == 'image' || e.name == 'video'
1045             e.add_attribute('deleted', 'true')
1046         end
1047     }
1048     #- branch if we have a non deleted subalbum
1049     if $xmldir.child_byname_notattr('dir', 'deleted')
1050         $xmldir.delete_attribute('thumbnails-caption')
1051         $xmldir.delete_attribute('thumbnails-captionfile')
1052     else
1053         $xmldir.add_attribute('deleted', 'true')
1054         moveup = $xmldir
1055         while moveup.parent.name == 'dir'
1056             moveup = moveup.parent
1057             if !moveup.child_byname_notattr('dir', 'deleted') && !moveup.child_byname_notattr('image', 'deleted') && !moveup.child_byname_notattr('video', 'deleted')
1058                 moveup.add_attribute('deleted', 'true')
1059             else
1060                 break
1061             end
1062         end
1063         sel[0].up!
1064     end
1065     save_changes('forced')
1066     populate_subalbums_treeview(false)
1067     $albums_tv.selection.select_path(sel[0])
1068 end
1069
1070 def restore_deleted
1071     $modified = true
1072     save_changes
1073     $current_path = nil  #- prevent save_changes from being rerun again
1074     sel = $albums_tv.selection.selected_rows
1075     restore_one = proc { |xmldir|
1076         xmldir.elements.each { |e|
1077             if e.name == 'dir' && e.attributes['deleted']
1078                 restore_one.call(e)
1079             end
1080             e.delete_attribute('deleted')
1081         }
1082     }
1083     restore_one.call($xmldir)
1084     populate_subalbums_treeview(false)
1085     $albums_tv.selection.select_path(sel[0])
1086 end
1087
1088 def add_thumbnail(autotable, filename, type, thumbnail_img, caption)
1089
1090     img = nil
1091     frame1 = Gtk::Frame.new
1092     fullpath = from_utf8("#{$current_path}/#{filename}")
1093
1094     my_gen_real_thumbnail = proc {
1095         gen_real_thumbnail('element', fullpath, thumbnail_img, $xmldir, $default_size['thumbnails'], img, '')
1096     }
1097
1098     if type == 'video'
1099         pxb = Gdk::Pixbuf.new("#{$FPATH}/images/video_border.png")
1100         frame1.add(Gtk::HBox.new.pack_start(da1 = Gtk::DrawingArea.new.set_size_request(pxb.width, -1), false, false).
1101                                  pack_start(img = Gtk::Image.new).
1102                                  pack_start(da2 = Gtk::DrawingArea.new.set_size_request(pxb.width, -1), false, false))
1103         px, mask = pxb.render_pixmap_and_mask(0)
1104         da1.signal_connect('realize') { da1.window.set_back_pixmap(px, false) }
1105         da2.signal_connect('realize') { da2.window.set_back_pixmap(px, false) }
1106     else
1107         frame1.add(img = Gtk::Image.new)
1108     end
1109
1110     #- generate the thumbnail if missing (if image was rotated but booh was not relaunched)
1111     if !$modified_pixbufs[thumbnail_img] && !File.exists?(thumbnail_img)
1112         my_gen_real_thumbnail.call
1113     else
1114         img.set($modified_pixbufs[thumbnail_img] ? $modified_pixbufs[thumbnail_img][:pixbuf] : thumbnail_img)
1115     end
1116
1117     evtbox = Gtk::EventBox.new.add(Gtk::Alignment.new(0.5, 0.5, 0, 0).add(frame1.set_shadow_type(Gtk::SHADOW_ETCHED_OUT)))
1118
1119     tooltips = Gtk::Tooltips.new
1120     tipname = from_utf8(File.basename(filename).gsub(/\.[^\.]+$/, ''))
1121     tooltips.set_tip(evtbox, utf8(type == 'video' ? (_("%s (video - %s KB)") % [tipname, commify(file_size(fullpath)/1024)]) : tipname), nil)
1122
1123     frame2, textview = create_editzone($autotable_sw, 1, img)
1124     textview.buffer.text = caption
1125     textview.set_justification(Gtk::Justification::CENTER)
1126
1127     vbox = Gtk::VBox.new(false, 5)
1128     vbox.pack_start(evtbox, false, false)
1129     vbox.pack_start(frame2, false, false)
1130     autotable.append(vbox, filename)
1131
1132     #- to be able to grab focus of textview when retrieving vbox's position from AutoTable
1133     $vbox2widgets[vbox] = { :textview => textview, :image => img }
1134
1135     #- to be able to find widgets by name
1136     $name2widgets[filename] = { :textview => textview, :evtbox => evtbox, :vbox => vbox, :img => img, :type => type }
1137
1138     cleanup_all_thumbnails = proc {
1139         #- remove out of sync images
1140         dest_img_base = build_full_dest_filename(filename).sub(/\.[^\.]+$/, '')
1141         for sizeobj in $images_size
1142             #- cannot use sizeobj because panoramic images will have a larger width
1143             Dir.glob("#{dest_img_base}-*.jpg") do |file|
1144                 File.delete(file)
1145             end
1146         end
1147
1148     }
1149
1150     refresh = proc {
1151         cleanup_all_thumbnails.call
1152         #- refresh is not undoable and doesn't change the album, however we must regenerate all thumbnails when generating the album
1153         $modified = true
1154         $xmldir.delete_attribute('already-generated')
1155         my_gen_real_thumbnail.call
1156     }
1157  
1158     rotate_and_cleanup = proc { |angle|
1159         cleanup_all_thumbnails.call
1160         rotate(angle, thumbnail_img, img, $xmldir.elements["*[@filename='#{filename}']"], '', $default_thumbnails[:x], $default_thumbnails[:y])
1161     }
1162
1163     move = proc { |direction|
1164         do_method = "move_#{direction}"
1165         undo_method = "move_" + case direction; when 'left'; 'right'; when 'right'; 'left'; when 'up'; 'down'; when 'down'; 'up' end
1166         perform = proc {
1167             done = autotable.method(do_method).call(vbox)
1168             textview.grab_focus  #- because if moving, focus is stolen
1169             done
1170         }
1171         if perform.call
1172             save_undo(_("move %s") % direction,
1173                       proc {
1174                           autotable.method(undo_method).call(vbox)
1175                           textview.grab_focus  #- because if moving, focus is stolen
1176                           autoscroll_if_needed($autotable_sw, img, textview)
1177                           $notebook.set_page(1)
1178                           proc {
1179                               autotable.method(do_method).call(vbox)
1180                               textview.grab_focus  #- because if moving, focus is stolen
1181                               autoscroll_if_needed($autotable_sw, img, textview)
1182                               $notebook.set_page(1)
1183                           }
1184                       })
1185         end
1186     }
1187
1188     color_swap_and_cleanup = proc {
1189         perform_color_swap_and_cleanup = proc {
1190             cleanup_all_thumbnails.call
1191             color_swap($xmldir.elements["*[@filename='#{filename}']"], '')
1192             my_gen_real_thumbnail.call
1193         }
1194
1195         perform_color_swap_and_cleanup.call
1196
1197         save_undo(_("color swap"),
1198                   proc {
1199                       perform_color_swap_and_cleanup.call
1200                       textview.grab_focus
1201                       autoscroll_if_needed($autotable_sw, img, textview)
1202                       $notebook.set_page(1)
1203                       proc {
1204                           perform_color_swap_and_cleanup.call
1205                           textview.grab_focus
1206                           autoscroll_if_needed($autotable_sw, img, textview)
1207                           $notebook.set_page(1)
1208                       }
1209                   })
1210     }
1211
1212     change_seektime_and_cleanup_real = proc { |values|
1213         perform_change_seektime_and_cleanup = proc { |val|
1214             cleanup_all_thumbnails.call
1215             change_seektime($xmldir.elements["*[@filename='#{filename}']"], '', val)
1216             my_gen_real_thumbnail.call
1217         }
1218         perform_change_seektime_and_cleanup.call(values[:new])
1219         
1220         save_undo(_("specify seektime"),
1221                   proc {
1222                       perform_change_seektime_and_cleanup.call(values[:old])
1223                       textview.grab_focus
1224                       autoscroll_if_needed($autotable_sw, img, textview)
1225                       $notebook.set_page(1)
1226                       proc {
1227                           perform_change_seektime_and_cleanup.call(values[:new])
1228                           textview.grab_focus
1229                           autoscroll_if_needed($autotable_sw, img, textview)
1230                           $notebook.set_page(1)
1231                       }
1232                   })
1233     }
1234
1235     change_seektime_and_cleanup = proc {
1236         if values = ask_new_seektime($xmldir.elements["*[@filename='#{filename}']"], '')
1237             change_seektime_and_cleanup_real.call(values)
1238         end
1239     }
1240
1241     change_pano_amount_and_cleanup_real = proc { |values|
1242         perform_change_pano_amount_and_cleanup = proc { |val|
1243             cleanup_all_thumbnails.call
1244             change_pano_amount($xmldir.elements["*[@filename='#{filename}']"], '', val)
1245         }
1246         perform_change_pano_amount_and_cleanup.call(values[:new])
1247         
1248         save_undo(_("change panorama amount"),
1249                   proc {
1250                       perform_change_pano_amount_and_cleanup.call(values[:old])
1251                       textview.grab_focus
1252                       autoscroll_if_needed($autotable_sw, img, textview)
1253                       $notebook.set_page(1)
1254                       proc {
1255                           perform_change_pano_amount_and_cleanup.call(values[:new])
1256                           textview.grab_focus
1257                           autoscroll_if_needed($autotable_sw, img, textview)
1258                           $notebook.set_page(1)
1259                       }
1260                   })
1261     }
1262
1263     change_pano_amount_and_cleanup = proc {
1264         if values = ask_new_pano_amount($xmldir.elements["*[@filename='#{filename}']"], '')
1265             change_pano_amount_and_cleanup_real.call(values)
1266         end
1267     }
1268
1269     whitebalance_and_cleanup_real = proc { |values|
1270         perform_change_whitebalance_and_cleanup = proc { |val|
1271             cleanup_all_thumbnails.call
1272             change_whitebalance($xmldir.elements["*[@filename='#{filename}']"], '', val)
1273             recalc_whitebalance(val, fullpath, thumbnail_img, img,
1274                                 $xmldir.elements["*[@filename='#{filename}']"], '', $default_thumbnails[:x], $default_thumbnails[:y], '')
1275         }
1276         perform_change_whitebalance_and_cleanup.call(values[:new])
1277
1278         save_undo(_("fix white balance"),
1279                   proc {
1280                       perform_change_whitebalance_and_cleanup.call(values[:old])
1281                       textview.grab_focus
1282                       autoscroll_if_needed($autotable_sw, img, textview)
1283                       $notebook.set_page(1)
1284                       proc {
1285                           perform_change_whitebalance_and_cleanup.call(values[:new])
1286                           textview.grab_focus
1287                           autoscroll_if_needed($autotable_sw, img, textview)
1288                           $notebook.set_page(1)
1289                       }
1290                   })
1291     }
1292
1293     whitebalance_and_cleanup = proc {
1294         if values = ask_whitebalance(fullpath, thumbnail_img, img,
1295                                      $xmldir.elements["*[@filename='#{filename}']"], '', $default_thumbnails[:x], $default_thumbnails[:y], '')
1296             whitebalance_and_cleanup_real.call(values)
1297         end
1298     }
1299
1300     gammacorrect_and_cleanup_real = proc { |values|
1301         perform_change_gammacorrect_and_cleanup = Proc.new { |val|
1302             cleanup_all_thumbnails.call
1303             change_gammacorrect($xmldir.elements["*[@filename='#{filename}']"], '', val)
1304             recalc_gammacorrect(val, fullpath, thumbnail_img, img,
1305                                 $xmldir.elements["*[@filename='#{filename}']"], '', $default_thumbnails[:x], $default_thumbnails[:y], '')
1306         }
1307         perform_change_gammacorrect_and_cleanup.call(values[:new])
1308         
1309         save_undo(_("gamma correction"),
1310                   Proc.new {
1311                       perform_change_gammacorrect_and_cleanup.call(values[:old])
1312                       textview.grab_focus
1313                       autoscroll_if_needed($autotable_sw, img, textview)
1314                       $notebook.set_page(1)
1315                       Proc.new {
1316                           perform_change_gammacorrect_and_cleanup.call(values[:new])
1317                           textview.grab_focus
1318                           autoscroll_if_needed($autotable_sw, img, textview)
1319                           $notebook.set_page(1)
1320                       }
1321                   })
1322     }
1323     
1324     gammacorrect_and_cleanup = Proc.new {
1325         if values = ask_gammacorrect(fullpath, thumbnail_img, img,
1326                                      $xmldir.elements["*[@filename='#{filename}']"], '', $default_thumbnails[:x], $default_thumbnails[:y], '')
1327             gammacorrect_and_cleanup_real.call(values)
1328         end
1329     }
1330     
1331     enhance_and_cleanup = proc {
1332         perform_enhance_and_cleanup = proc {
1333             cleanup_all_thumbnails.call
1334             enhance($xmldir.elements["*[@filename='#{filename}']"], '')
1335             my_gen_real_thumbnail.call
1336         }
1337         
1338         cleanup_all_thumbnails.call
1339         perform_enhance_and_cleanup.call
1340
1341         save_undo(_("enhance"),
1342                   proc {
1343                       perform_enhance_and_cleanup.call
1344                       textview.grab_focus
1345                       autoscroll_if_needed($autotable_sw, img, textview)
1346                       $notebook.set_page(1)
1347                       proc {
1348                           perform_enhance_and_cleanup.call
1349                           textview.grab_focus
1350                           autoscroll_if_needed($autotable_sw, img, textview)
1351                           $notebook.set_page(1)
1352                       }
1353                   })
1354     }
1355
1356     delete = proc { |isacut|
1357         if autotable.current_order.size > 1 || show_popup($main_window, utf8(_("Do you confirm this subalbum needs to be completely removed? This operation cannot be undone.")), { :okcancel => true })
1358             $modified = true
1359             after = nil
1360             perform_delete = proc {
1361                 after = autotable.get_next_widget(vbox)
1362                 if !after
1363                     after = autotable.get_previous_widget(vbox)
1364                 end
1365                 if $config['deleteondisk'] && !isacut
1366                     msg 3, "scheduling for delete: #{fullpath}"
1367                     $todelete << fullpath
1368                 end
1369                 autotable.remove_widget(vbox)
1370                 if after
1371                     $vbox2widgets[after][:textview].grab_focus
1372                     autoscroll_if_needed($autotable_sw, $vbox2widgets[after][:image], $vbox2widgets[after][:textview])
1373                 end
1374             }
1375             
1376             previous_pos = autotable.get_current_number(vbox)
1377             perform_delete.call
1378
1379             if !after
1380                 delete_current_subalbum
1381             else
1382                 save_undo(_("delete"),
1383                           proc { |pos|
1384                               autotable.reinsert(pos, vbox, filename)
1385                               $notebook.set_page(1)
1386                               autotable.queue_draws << proc { textview.grab_focus; autoscroll_if_needed($autotable_sw, img, textview) }
1387                               $cuts = []
1388                               msg 3, "removing deletion schedule of: #{fullpath}"
1389                               $todelete.delete(fullpath)  #- unconditional because deleteondisk option could have been modified
1390                               proc {
1391                                   perform_delete.call
1392                                   $notebook.set_page(1)
1393                               }
1394                           }, previous_pos)
1395             end
1396         end
1397     }
1398
1399     cut = proc {
1400         delete.call(true)
1401         $cuts << { :vbox => vbox, :filename => filename }
1402         $statusbar.push(0, utf8(_("%s elements in the clipboard.") % $cuts.size ))
1403     }
1404     paste = proc {
1405         if $cuts.size > 0
1406             $cuts.each { |elem|
1407                 autotable.reinsert(autotable.get_current_number(vbox), elem[:vbox], elem[:filename])
1408             }
1409             last = $cuts[-1]
1410             autotable.queue_draws << proc {
1411                 $vbox2widgets[last[:vbox]][:textview].grab_focus
1412                 autoscroll_if_needed($autotable_sw, $vbox2widgets[last[:vbox]][:image], $vbox2widgets[last[:vbox]][:textview])
1413             }
1414             save_undo(_("paste"),
1415                       proc { |cuts|
1416                           cuts.each { |elem| autotable.remove_widget(elem[:vbox]) }
1417                           $notebook.set_page(1)
1418                           proc {
1419                               cuts.each { |elem|
1420                                   autotable.reinsert(autotable.get_current_number(vbox), elem[:vbox], elem[:filename])
1421                               }
1422                               $notebook.set_page(1)
1423                           }
1424                       }, $cuts)
1425             $statusbar.push(0, utf8(_("Pasted %s elements.") % $cuts.size ))
1426             $cuts = []
1427         end
1428     }
1429
1430     $name2closures[filename] = { :rotate => rotate_and_cleanup, :enhance => enhance_and_cleanup, :delete => delete, :cut => cut,
1431                                  :color_swap => color_swap_and_cleanup, :seektime => change_seektime_and_cleanup_real,
1432                                  :whitebalance => whitebalance_and_cleanup_real, :gammacorrect => gammacorrect_and_cleanup_real,
1433                                  :pano => change_pano_amount_and_cleanup_real, :refresh => refresh }
1434
1435     textview.signal_connect('key-press-event') { |w, event|
1436         propagate = true
1437         if event.state != 0
1438             x, y = autotable.get_current_pos(vbox)
1439             control_pressed = event.state & Gdk::Window::CONTROL_MASK != 0
1440             shift_pressed = event.state & Gdk::Window::SHIFT_MASK != 0
1441             alt_pressed = event.state & Gdk::Window::MOD1_MASK != 0
1442             if event.keyval == Gdk::Keyval::GDK_Up && y > 0
1443                 if control_pressed
1444                     if widget_up = autotable.get_widget_at_pos(x, y - 1)
1445                         $vbox2widgets[widget_up][:textview].grab_focus
1446                     end
1447                 end
1448                 if shift_pressed
1449                     move.call('up')
1450                 end
1451             end
1452             if event.keyval == Gdk::Keyval::GDK_Down && y < autotable.get_max_y
1453                 if control_pressed
1454                     if widget_down = autotable.get_widget_at_pos(x, y + 1)
1455                         $vbox2widgets[widget_down][:textview].grab_focus
1456                     end
1457                 end
1458                 if shift_pressed
1459                     move.call('down')
1460                 end
1461             end
1462             if event.keyval == Gdk::Keyval::GDK_Left
1463                 if x > 0
1464                     if control_pressed
1465                         $vbox2widgets[autotable.get_previous_widget(vbox)][:textview].grab_focus
1466                     end
1467                     if shift_pressed
1468                         move.call('left')
1469                     end
1470                 end
1471                 if alt_pressed
1472                     rotate_and_cleanup.call(-90)
1473                 end
1474             end
1475             if event.keyval == Gdk::Keyval::GDK_Right
1476                 next_ = autotable.get_next_widget(vbox)
1477                 if next_ && autotable.get_current_pos(next_)[0] > x
1478                     if control_pressed
1479                         $vbox2widgets[next_][:textview].grab_focus
1480                     end
1481                     if shift_pressed
1482                         move.call('right')
1483                     end
1484                 end
1485                 if alt_pressed
1486                     rotate_and_cleanup.call(90)
1487                 end
1488             end
1489             if event.keyval == Gdk::Keyval::GDK_Delete && control_pressed
1490                 delete.call(false)
1491             end
1492             if event.keyval == Gdk::Keyval::GDK_Return && control_pressed
1493                 view_element(filename, { :delete => delete })
1494                 propagate = false
1495             end
1496             if event.keyval == Gdk::Keyval::GDK_z && control_pressed
1497                 perform_undo
1498             end
1499             if event.keyval == Gdk::Keyval::GDK_r && control_pressed
1500                 perform_redo
1501             end
1502         end
1503         !propagate  #- propagate if needed
1504     }
1505
1506     $ignore_next_release = false
1507     evtbox.signal_connect('button-press-event') { |w, event|
1508         if event.event_type == Gdk::Event::BUTTON_PRESS && event.button == 1
1509             if event.state & Gdk::Window::BUTTON3_MASK != 0
1510                 #- gesture redo: hold right mouse button then click left mouse button
1511                 $config['nogestures'] or perform_redo
1512                 $ignore_next_release = true
1513             else
1514                 shift_or_control = event.state & Gdk::Window::SHIFT_MASK != 0 || event.state & Gdk::Window::CONTROL_MASK != 0
1515                 if $r90.active?
1516                     rotate_and_cleanup.call(shift_or_control ? -90 : 90)
1517                 elsif $r270.active?
1518                     rotate_and_cleanup.call(shift_or_control ? 90 : -90)
1519                 elsif $enhance.active?
1520                     enhance_and_cleanup.call
1521                 elsif $delete.active?
1522                     delete.call(false)
1523                 else
1524                     textview.grab_focus
1525                     $config['nogestures'] or $gesture_press = { :filename => filename, :x => event.x, :y => event.y }
1526                 end
1527             end
1528             $button1_pressed_autotable = true
1529         elsif event.event_type == Gdk::Event::BUTTON_PRESS && event.button == 3
1530             if event.state & Gdk::Window::BUTTON1_MASK != 0
1531                 #- gesture undo: hold left mouse button then click right mouse button
1532                 $config['nogestures'] or perform_undo
1533                 $ignore_next_release = true
1534             end
1535         elsif event.event_type == Gdk::Event::BUTTON2_PRESS && event.button == 1
1536             view_element(filename, { :delete => delete })
1537         end
1538         false   #- propagate
1539     }
1540
1541     evtbox.signal_connect('button-release-event') { |w, event|
1542         if event.event_type == Gdk::Event::BUTTON_RELEASE && event.button == 3
1543             if !$ignore_next_release
1544                 x, y = autotable.get_current_pos(vbox)
1545                 next_ = autotable.get_next_widget(vbox)
1546                 popup_thumbnail_menu(event, ['delete'], fullpath, type, $xmldir.elements["*[@filename='#{filename}']"], '',
1547                                      { :can_left => x > 0, :can_right => next_ && autotable.get_current_pos(next_)[0] > x,
1548                                        :can_up => y > 0, :can_down => y < autotable.get_max_y, :can_multiple => true, :can_panorama => true },
1549                                      { :rotate => rotate_and_cleanup, :move => move, :color_swap => color_swap_and_cleanup, :enhance => enhance_and_cleanup,
1550                                        :seektime => change_seektime_and_cleanup, :delete => delete, :whitebalance => whitebalance_and_cleanup,
1551                                        :cut => cut, :paste => paste, :view => proc { view_element(filename, { :delete => delete }) },
1552                                        :pano => change_pano_amount_and_cleanup, :refresh => refresh, :gammacorrect => gammacorrect_and_cleanup })
1553             end
1554             $ignore_next_release = false
1555             $gesture_press = nil
1556         end
1557         false   #- propagate
1558     }
1559
1560     #- handle reordering with drag and drop
1561     Gtk::Drag.source_set(vbox, Gdk::Window::BUTTON1_MASK, [['reorder-elements', Gtk::Drag::TARGET_SAME_APP, 1]], Gdk::DragContext::ACTION_MOVE)
1562     Gtk::Drag.dest_set(vbox, Gtk::Drag::DEST_DEFAULT_ALL, [['reorder-elements', Gtk::Drag::TARGET_SAME_APP, 1]], Gdk::DragContext::ACTION_MOVE)
1563     vbox.signal_connect('drag-data-get') { |w, ctxt, selection_data, info, time|
1564         selection_data.set(Gdk::Selection::TYPE_STRING, autotable.get_current_number(vbox).to_s)
1565     }
1566
1567     vbox.signal_connect('drag-data-received') { |w, ctxt, x, y, selection_data, info, time|
1568         done = false
1569         #- mouse gesture first (dnd disables button-release-event)
1570         if $gesture_press && $gesture_press[:filename] == filename
1571             if (($gesture_press[:x]-x)/($gesture_press[:y]-y)).abs > 2 && ($gesture_press[:x]-x).abs > 5
1572                 angle = x-$gesture_press[:x] > 0 ? 90 : -90
1573                 msg 3, "gesture rotate: #{angle}: click-drag right button to the left/right"
1574                 rotate_and_cleanup.call(angle)
1575                 $statusbar.push(0, utf8(_("Mouse gesture: rotate.")))
1576                 done = true
1577             elsif (($gesture_press[:y]-y)/($gesture_press[:x]-x)).abs > 2 && y-$gesture_press[:y] > 5
1578                 msg 3, "gesture delete: click-drag right button to the bottom"
1579                 delete.call(false)
1580                 $statusbar.push(0, utf8(_("Mouse gesture: delete.")))
1581                 done = true
1582             end
1583         end
1584         if !done
1585             ctxt.targets.each { |target|
1586                 if target.name == 'reorder-elements'
1587                     move_dnd = proc { |from,to|
1588                         if from != to
1589                             $modified = true
1590                             autotable.move(from, to)
1591                             save_undo(_("reorder"),
1592                                       proc { |from, to|
1593                                           if to > from
1594                                               autotable.move(to - 1, from)
1595                                           else
1596                                               autotable.move(to, from + 1)
1597                                           end
1598                                           $notebook.set_page(1)
1599                                           proc {
1600                                               autotable.move(from, to)
1601                                               $notebook.set_page(1)
1602                                           }
1603                                       }, from, to)
1604                         end
1605                     }
1606                     if $multiple_dnd.size == 0
1607                         move_dnd.call(selection_data.data.to_i,
1608                                       autotable.get_current_number(vbox))
1609                     else
1610                         UndoHandler.begin_batch
1611                         $multiple_dnd.sort { |a,b| autotable.get_current_number($name2widgets[a][:vbox]) <=> autotable.get_current_number($name2widgets[b][:vbox]) }.
1612                                       each { |path|
1613                             #- need to update current position between each call
1614                             move_dnd.call(autotable.get_current_number($name2widgets[path][:vbox]),
1615                                           autotable.get_current_number(vbox))
1616                         }
1617                         UndoHandler.end_batch
1618                     end
1619                     $multiple_dnd = []
1620                 end
1621             }
1622         end
1623     }
1624
1625     vbox.show_all
1626 end
1627
1628 def create_auto_table
1629
1630     $autotable = Gtk::AutoTable.new(5)
1631
1632     $autotable_sw = Gtk::ScrolledWindow.new(nil, nil)
1633     thumbnails_vb = Gtk::VBox.new(false, 5)
1634
1635     frame, $thumbnails_title = create_editzone($autotable_sw, 0, nil)
1636     $thumbnails_title.set_justification(Gtk::Justification::CENTER)
1637     thumbnails_vb.pack_start(Gtk::Alignment.new(0.5, 0.5, 0.5, 0).add(frame), false, false)
1638     thumbnails_vb.add($autotable)
1639
1640     $autotable_sw.set_policy(Gtk::POLICY_NEVER, Gtk::POLICY_ALWAYS)
1641     $autotable_sw.add_with_viewport(thumbnails_vb)
1642
1643     #- follows stuff for handling multiple elements selection
1644     press_x = nil; press_y = nil; pos_x = nil; pos_y = nil; $selected_elements = {}
1645     gc = nil
1646     update_selected = proc {
1647         $autotable.current_order.each { |path|
1648             w = $name2widgets[path][:evtbox].window
1649             xm = w.position[0] + w.size[0]/2
1650             ym = w.position[1] + w.size[1]/2
1651             if ym < press_y && ym > pos_y || ym < pos_y && ym > press_y
1652                 if (xm < press_x && xm > pos_x || xm < pos_x && xm > press_x) && ! $selected_elements[path]
1653                     $selected_elements[path] = { :pixbuf => $name2widgets[path][:img].pixbuf }
1654                     $name2widgets[path][:img].pixbuf = $name2widgets[path][:img].pixbuf.saturate_and_pixelate(1, true)
1655                 end
1656             end
1657             if $selected_elements[path] && ! $selected_elements[path][:keep]
1658                 if ((xm < press_x && xm < pos_x || xm > pos_x && xm > press_x) || (ym < press_y && ym < pos_y || ym > pos_y && ym > press_y))
1659                     $name2widgets[path][:img].pixbuf = $selected_elements[path][:pixbuf]
1660                     $selected_elements.delete(path)
1661                 end
1662             end
1663         }
1664     }
1665     $autotable.signal_connect('realize') { |w,e|
1666         gc = Gdk::GC.new($autotable.window)
1667         gc.set_line_attributes(1, Gdk::GC::LINE_ON_OFF_DASH, Gdk::GC::CAP_PROJECTING, Gdk::GC::JOIN_ROUND)
1668         gc.function = Gdk::GC::INVERT
1669         #- autoscroll handling for DND and multiple selections
1670         Gtk.timeout_add(100) {
1671             if ! $autotable.window.nil?
1672                 w, x, y, mask = $autotable.window.pointer
1673                 if mask & Gdk::Window::BUTTON1_MASK != 0
1674                     if y < $autotable_sw.vadjustment.value
1675                         if pos_x
1676                             $autotable.window.draw_lines(gc, [[press_x, press_y], [pos_x, press_y], [pos_x, pos_y], [press_x, pos_y], [press_x, press_y]])
1677                         end
1678                         if $button1_pressed_autotable || press_x
1679                             scroll_upper($autotable_sw, y)
1680                         end
1681                         if not press_x.nil?
1682                             w, pos_x, pos_y = $autotable.window.pointer
1683                             $autotable.window.draw_lines(gc, [[press_x, press_y], [pos_x, press_y], [pos_x, pos_y], [press_x, pos_y], [press_x, press_y]])
1684                             update_selected.call
1685                         end
1686                     end
1687                     if y > $autotable_sw.vadjustment.value + $autotable_sw.vadjustment.page_size
1688                         if pos_x
1689                             $autotable.window.draw_lines(gc, [[press_x, press_y], [pos_x, press_y], [pos_x, pos_y], [press_x, pos_y], [press_x, press_y]])
1690                         end
1691                         if $button1_pressed_autotable || press_x
1692                             scroll_lower($autotable_sw, y)
1693                         end
1694                         if not press_x.nil?
1695                             w, pos_x, pos_y = $autotable.window.pointer
1696                             $autotable.window.draw_lines(gc, [[press_x, press_y], [pos_x, press_y], [pos_x, pos_y], [press_x, pos_y], [press_x, press_y]])
1697                             update_selected.call
1698                         end
1699                     end
1700                 end
1701             end
1702             ! $autotable.window.nil?
1703         }
1704     }
1705
1706     $autotable.signal_connect('button-press-event') { |w,e|
1707         if e.button == 1
1708             if !$button1_pressed_autotable
1709                 press_x = e.x
1710                 press_y = e.y
1711                 if e.state & Gdk::Window::SHIFT_MASK == 0
1712                     $selected_elements.each_key { |path| $name2widgets[path][:img].pixbuf = $selected_elements[path][:pixbuf] }
1713                     $selected_elements = {}
1714                     $statusbar.push(0, utf8(_("Nothing selected.")))
1715                 else
1716                     $selected_elements.each_key { |path| $selected_elements[path][:keep] = true }
1717                 end
1718                 set_mousecursor(Gdk::Cursor::TCROSS)
1719             end
1720         end
1721     }
1722     $autotable.signal_connect('button-release-event') { |w,e|
1723         if e.button == 1
1724             if $button1_pressed_autotable
1725                 #- unselect all only now
1726                 $multiple_dnd = $selected_elements.keys
1727                 $selected_elements.each_key { |path| $name2widgets[path][:img].pixbuf = $selected_elements[path][:pixbuf] }
1728                 $selected_elements = {}
1729                 $button1_pressed_autotable = false
1730             else
1731                 if pos_x
1732                     $autotable.window.draw_lines(gc, [[press_x, press_y], [pos_x, press_y], [pos_x, pos_y], [press_x, pos_y], [press_x, press_y]])
1733                     if $selected_elements.length > 0
1734                         $statusbar.push(0, utf8(_("%s elements selected.") % $selected_elements.length))
1735                     end
1736                 end
1737                 press_x = press_y = pos_x = pos_y = nil
1738                 set_mousecursor(Gdk::Cursor::LEFT_PTR)
1739             end
1740         end
1741     }
1742     $autotable.signal_connect('motion-notify-event') { |w,e|
1743         if ! press_x.nil?
1744             if pos_x
1745                 $autotable.window.draw_lines(gc, [[press_x, press_y], [pos_x, press_y], [pos_x, pos_y], [press_x, pos_y], [press_x, press_y]])
1746             end
1747             pos_x = e.x
1748             pos_y = e.y
1749             $autotable.window.draw_lines(gc, [[press_x, press_y], [pos_x, press_y], [pos_x, pos_y], [press_x, pos_y], [press_x, press_y]])
1750             update_selected.call
1751         end
1752     }
1753
1754 end
1755
1756 def create_subalbums_page
1757
1758     subalbums_hb = Gtk::HBox.new
1759     $subalbums_vb = Gtk::VBox.new(false, 5)
1760     subalbums_hb.pack_start($subalbums_vb, false, false)
1761     $subalbums_sw = Gtk::ScrolledWindow.new(nil, nil)
1762     $subalbums_sw.set_policy(Gtk::POLICY_AUTOMATIC, Gtk::POLICY_AUTOMATIC)
1763     $subalbums_sw.add_with_viewport(subalbums_hb)
1764 end
1765
1766 def save_current_file
1767     save_changes
1768
1769     if $filename
1770         begin
1771             begin
1772                 ios = File.open($filename, "w")
1773                 $xmldoc.write(ios, 0)
1774                 ios.close
1775             rescue Iconv::IllegalSequence
1776                 #- user might have entered text which cannot be encoded in his default encoding. retry in UTF-8.
1777                 if ! ios.nil? && ! ios.closed?
1778                     ios.close
1779                 end
1780                 $xmldoc.xml_decl.encoding = 'UTF-8'
1781                 ios = File.open($filename, "w")
1782                 $xmldoc.write(ios, 0)
1783                 ios.close
1784             end
1785             return true
1786         rescue Exception
1787             puts $!
1788             return false
1789         end
1790     end
1791 end
1792
1793 def save_current_file_user
1794     save_tempfilename = $filename
1795     $filename = $orig_filename
1796     if ! save_current_file
1797         show_popup($main_window, utf8(_("Save failed! Try another location/name.")))
1798         $filename = save_tempfilename
1799         return
1800     end
1801     $modified = false
1802     $generated_outofline = false
1803     $filename = save_tempfilename
1804
1805     msg 3, "performing actual deletion of: " + $todelete.join(', ')
1806     $todelete.each { |f|
1807         File.delete(f)
1808     }
1809 end
1810
1811 def mark_document_as_dirty
1812     $xmldoc.elements.each('//dir') { |elem|
1813         elem.delete_attribute('already-generated')
1814     }
1815 end
1816
1817 #- ret: true => ok  false => cancel
1818 def ask_save_modifications(msg1, msg2, *options)
1819     ret = true
1820     options = options.size > 0 ? options[0] : {}
1821     if $modified
1822         if options[:disallow_cancel]
1823             dialog = Gtk::Dialog.new(msg1,
1824                                      $main_window,
1825                                      Gtk::Dialog::MODAL | Gtk::Dialog::DESTROY_WITH_PARENT,
1826                                      [options[:no] || Gtk::Stock::CLOSE, Gtk::Dialog::RESPONSE_NO],
1827                                      [options[:yes] || Gtk::Stock::SAVE, Gtk::Dialog::RESPONSE_YES])
1828         else
1829             dialog = Gtk::Dialog.new(msg1,
1830                                      $main_window,
1831                                      Gtk::Dialog::MODAL | Gtk::Dialog::DESTROY_WITH_PARENT,
1832                                      [options[:cancel] || Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL],
1833                                      [options[:no] || Gtk::Stock::CLOSE, Gtk::Dialog::RESPONSE_NO],
1834                                      [options[:yes] || Gtk::Stock::SAVE, Gtk::Dialog::RESPONSE_YES])
1835         end
1836         dialog.default_response = Gtk::Dialog::RESPONSE_YES
1837         dialog.vbox.add(Gtk::Label.new(msg2))
1838         dialog.window_position = Gtk::Window::POS_CENTER
1839         dialog.show_all
1840         
1841         dialog.run { |response|
1842             dialog.destroy
1843             if response == Gtk::Dialog::RESPONSE_YES
1844                 if ! save_current_file_user
1845                     return ask_save_modifications(msg1, msg2, options)
1846                 end
1847             else
1848                 #- if we have generated an album but won't save modifications, we must remove 
1849                 #- already-generated markers in original file
1850                 if $generated_outofline
1851                     begin
1852                         $xmldoc = REXML::Document.new File.new($orig_filename)
1853                         mark_document_as_dirty
1854                         ios = File.open($orig_filename, "w")
1855                         $xmldoc.write(ios, 0)
1856                         ios.close
1857                     rescue Exception
1858                         puts "exception: #{$!}"
1859                     end
1860                 end
1861             end
1862             if response == Gtk::Dialog::RESPONSE_CANCEL
1863                 ret = false
1864             end
1865             $todelete = []  #- unconditionally clear the list of images/videos to delete
1866         }
1867     end
1868     return ret
1869 end
1870
1871 def try_quit(*options)
1872     if ask_save_modifications(utf8(_("Save before quitting?")),
1873                               utf8(_("Do you want to save your changes before quitting?")),
1874                               *options)
1875         Gtk.main_quit
1876     end
1877 end
1878
1879 def show_popup(parent, msg, *options)
1880     dialog = Gtk::Dialog.new
1881     if options[0] && options[0][:title]
1882         dialog.title = options[0][:title]
1883     else
1884         dialog.title = utf8(_("Booh message"))
1885     end
1886     lbl = Gtk::Label.new
1887     if options[0] && options[0][:nomarkup]
1888         lbl.text = msg
1889     else
1890         lbl.markup = msg
1891     end
1892     if options[0] && options[0][:centered]
1893         lbl.set_justify(Gtk::Justification::CENTER)
1894     end
1895     if options[0] && options[0][:selectable]
1896         lbl.selectable = true
1897     end
1898     if options[0] && options[0][:topwidget]
1899         dialog.vbox.add(options[0][:topwidget])
1900     end
1901     if options[0] && options[0][:scrolled]
1902         sw = Gtk::ScrolledWindow.new(nil, nil)
1903         sw.set_policy(Gtk::POLICY_AUTOMATIC, Gtk::POLICY_AUTOMATIC)
1904         sw.add_with_viewport(lbl)
1905         dialog.vbox.add(sw)
1906         dialog.set_default_size(500, 600)
1907     else
1908         dialog.vbox.add(lbl)
1909         dialog.set_default_size(200, 120)
1910     end
1911     if options[0] && options[0][:okcancel]
1912         dialog.add_button(Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL)
1913     end
1914     dialog.add_button(Gtk::Stock::OK, Gtk::Dialog::RESPONSE_OK)
1915
1916     if options[0] && options[0][:pos_centered]
1917         dialog.window_position = Gtk::Window::POS_CENTER
1918     else
1919         dialog.window_position = Gtk::Window::POS_MOUSE
1920     end
1921
1922     if options[0] && options[0][:linkurl]
1923         linkbut = Gtk::Button.new('')
1924         linkbut.child.markup = "<span foreground=\"#00000000FFFF\" underline=\"single\">#{options[0][:linkurl]}</span>"
1925         linkbut.signal_connect('clicked') {
1926             open_url(options[0][:linkurl])
1927             dialog.response(Gtk::Dialog::RESPONSE_OK)
1928             set_mousecursor_normal
1929         }
1930         linkbut.relief = Gtk::RELIEF_NONE
1931         linkbut.signal_connect('enter-notify-event') { set_mousecursor(Gdk::Cursor::HAND2, linkbut); false }
1932         linkbut.signal_connect('leave-notify-event') { set_mousecursor(nil, linkbut); false }
1933         dialog.vbox.add(Gtk::Alignment.new(0.5, 0.5, 0, 0).add(linkbut))
1934     end
1935
1936     dialog.show_all
1937
1938     if !options[0] || !options[0][:not_transient]
1939         dialog.transient_for = parent
1940         dialog.run { |response|
1941             dialog.destroy
1942             if options[0] && options[0][:okcancel]
1943                 return response == Gtk::Dialog::RESPONSE_OK
1944             end
1945         }
1946     else
1947         dialog.signal_connect('response') { dialog.destroy }
1948     end
1949 end
1950
1951 def set_mainwindow_title(progress)
1952     filename = $orig_filename || $filename
1953     if progress
1954         if filename
1955             $main_window.title = 'booh [' + (progress * 100).to_i.to_s + '%] - ' + File.basename(filename)
1956         else
1957             $main_window.title = 'booh [' + (progress * 100).to_i.to_s + '%] '
1958         end
1959     else
1960         if filename
1961             $main_window.title = 'booh - ' + File.basename(filename)
1962         else
1963             $main_window.title = 'booh'
1964         end
1965     end
1966 end
1967
1968 def backend_wait_message(parent, msg, infopipe_path, mode)
1969     w = create_window
1970     w.set_transient_for(parent)
1971     w.modal = true
1972
1973     vb = Gtk::VBox.new(false, 5).set_border_width(5)
1974     vb.pack_start(Gtk::Label.new(msg), false, false)
1975
1976     vb.pack_start(frame1 = Gtk::Frame.new(utf8(_("Thumbnails"))).add(vb1 = Gtk::VBox.new(false, 5)))
1977     vb1.pack_start(pb1_1 = Gtk::ProgressBar.new.set_text(utf8(_("Scanning images and videos..."))), false, false)
1978     if mode != 'one dir scan'
1979         vb1.pack_start(pb1_2 = Gtk::ProgressBar.new.set_text(utf8(_("not started"))), false, false)
1980     end
1981     if mode == 'web-album'
1982         vb.pack_start(frame2 = Gtk::Frame.new(utf8(_("HTML pages"))).add(vb1 = Gtk::VBox.new(false, 5)))
1983         vb1.pack_start(pb2 = Gtk::ProgressBar.new.set_text(utf8(_("not started"))), false, false)
1984     end
1985     vb.pack_start(Gtk::HSeparator.new, false, false)
1986
1987     bottom = Gtk::Alignment.new(0.5, 0.5, 0, 0).add(b = Gtk::Button.new(utf8(_("_Abort"))))
1988     b.image = Gtk::Image.new("#{$FPATH}/images/stock-close-24.png")
1989     vb.pack_end(bottom, false, false)
1990
1991     directories = nil
1992     update_progression_title_pb1 = proc {
1993         if mode != 'web-album'
1994             set_mainwindow_title(pb1_2.fraction + pb1_1.fraction / directories)
1995         else
1996             set_mainwindow_title((pb1_2.fraction + pb1_1.fraction / directories) * 9 / 10)
1997         end
1998     }
1999
2000     infopipe = File.open(infopipe_path, File::RDONLY | File::NONBLOCK)
2001     refresh_thread = Thread.new {
2002         directories_counter = 0
2003         while line = infopipe.gets
2004             if line =~ /^directories: (\d+), sizes: (\d+)/
2005                 directories = $1.to_f + 1
2006                 sizes = $2.to_f
2007             elsif line =~ /^walking: (.+)\|(.+), (\d+) elements$/
2008                 elements = $3.to_f + 1
2009                 if mode == 'web-album'
2010                     elements += sizes
2011                 end
2012                 element_counter = 0
2013                 gtk_thread_protect { pb1_1.fraction = 0 }
2014                 if mode != 'one dir scan'
2015                     newtext = utf8(full_src_dir_to_rel($1, $2))
2016                     newtext = '/' if newtext == ''
2017                     gtk_thread_protect { pb1_2.text = newtext }
2018                     directories_counter += 1
2019                     gtk_thread_protect {
2020                         pb1_2.fraction = directories_counter / directories
2021                         update_progression_title_pb1.call
2022                     }
2023                 end
2024             elsif line =~ /^processing element$/
2025                 element_counter += 1
2026                 gtk_thread_protect {
2027                     pb1_1.fraction = element_counter / elements
2028                     update_progression_title_pb1.call
2029                 }
2030             elsif line =~ /^processing size$/
2031                 element_counter += 1
2032                 gtk_thread_protect {
2033                     pb1_1.fraction = element_counter / elements
2034                     update_progression_title_pb1.call
2035                 }
2036             elsif line =~ /^finished processing sizes$/
2037                 gtk_thread_protect { pb1_1.fraction = 1 }
2038             elsif line =~ /^creating index.html$/
2039                 gtk_thread_protect { pb1_2.text = utf8(_("finished")) }
2040                 gtk_thread_protect { pb1_1.fraction = pb1_2.fraction = 1 }
2041                 directories_counter = 0
2042             elsif line =~ /^index.html: (.+)\|(.+)/
2043                 newtext = utf8(full_src_dir_to_rel($1, $2))
2044                 newtext = '/' if newtext == ''
2045                 gtk_thread_protect { pb2.text = newtext }
2046                 directories_counter += 1
2047                 gtk_thread_protect {
2048                     pb2.fraction = directories_counter / directories
2049                     set_mainwindow_title(0.9 + pb2.fraction / 10)
2050                 }
2051             elsif line =~ /^die: (.*)$/
2052                 $diemsg = $1
2053             end
2054         end
2055     }
2056
2057     w.add(vb)
2058     w.signal_connect('delete-event') { w.destroy }
2059     w.signal_connect('destroy') {
2060         Thread.kill(refresh_thread)
2061         gtk_thread_flush  #- needed because we're about to destroy widgets in w, for which they may be some pending gtk calls
2062         if infopipe_path
2063             infopipe.close
2064             File.delete(infopipe_path)
2065         end
2066         set_mainwindow_title(nil)
2067     }
2068     w.window_position = Gtk::Window::POS_CENTER
2069     w.show_all
2070
2071     return [ b, w ]
2072 end
2073
2074 def call_backend(cmd, waitmsg, mode, params)
2075     pipe = Tempfile.new("boohpipe")
2076     pipe.close!
2077     system("mkfifo #{pipe.path}")
2078     cmd += " --info-pipe #{pipe.path}"
2079     button, w8 = backend_wait_message($main_window, waitmsg, pipe.path, mode)
2080     pid = nil
2081     Thread.new {
2082         msg 2, cmd
2083         if pid = fork
2084             id, exitstatus = Process.waitpid2(pid)
2085             gtk_thread_protect { w8.destroy }
2086             if exitstatus == 0
2087                 if params[:successmsg]
2088                     gtk_thread_protect { show_popup($main_window, params[:successmsg], { :linkurl => params[:successmsg_linkurl] }) }
2089                 end
2090                 if params[:closure_after]
2091                     gtk_thread_protect(&params[:closure_after])
2092                 end
2093             elsif exitstatus == 15
2094                 #- say nothing, user aborted
2095             else
2096                 gtk_thread_protect { show_popup($main_window,
2097                                                 utf8(_("There was something wrong, sorry:\n\n%s") % $diemsg)) }
2098             end
2099         else
2100             exec(cmd)
2101         end
2102     }
2103     button.signal_connect('clicked') {
2104         Process.kill('SIGTERM', pid)
2105     }
2106 end
2107
2108 def save_changes(*forced)
2109     if forced.empty? && (!$current_path || !$undo_tb.sensitive?)
2110         return
2111     end
2112
2113     $xmldir.delete_attribute('already-generated')
2114
2115     propagate_children = proc { |xmldir|
2116         if xmldir.attributes['subdirs-caption']
2117             xmldir.delete_attribute('already-generated')
2118         end
2119         xmldir.elements.each('dir') { |element|
2120             propagate_children.call(element)
2121         }
2122     }
2123
2124     if $xmldir.child_byname_notattr('dir', 'deleted')
2125         new_title = $subalbums_title.buffer.text
2126         if new_title != $xmldir.attributes['subdirs-caption']
2127             parent = $xmldir.parent
2128             if parent.name == 'dir'
2129                 parent.delete_attribute('already-generated')
2130             end
2131             propagate_children.call($xmldir)
2132         end
2133         $xmldir.add_attribute('subdirs-caption', new_title)
2134         $xmldir.elements.each('dir') { |element|
2135             if !element.attributes['deleted']
2136                 path = element.attributes['path']
2137                 newtext = $subalbums_edits[path][:editzone].buffer.text
2138                 if element.attributes['subdirs-caption']
2139                     if element.attributes['subdirs-caption'] != newtext
2140                         propagate_children.call(element)
2141                     end
2142                     element.add_attribute('subdirs-caption',     newtext)
2143                     element.add_attribute('subdirs-captionfile', utf8($subalbums_edits[path][:captionfile]))
2144                 else
2145                     if element.attributes['thumbnails-caption'] != newtext
2146                         element.delete_attribute('already-generated')
2147                     end
2148                     element.add_attribute('thumbnails-caption',     newtext)
2149                     element.add_attribute('thumbnails-captionfile', utf8($subalbums_edits[path][:captionfile]))
2150                 end
2151             end
2152         }
2153     end
2154
2155     if $notebook.page == 0 && $xmldir.child_byname_notattr('dir', 'deleted')
2156         if $xmldir.attributes['thumbnails-caption']
2157             path = $xmldir.attributes['path']
2158             $xmldir.add_attribute('thumbnails-caption', $subalbums_edits[path][:editzone].buffer.text)
2159         end
2160     elsif $xmldir.attributes['thumbnails-caption']
2161         $xmldir.add_attribute('thumbnails-caption', $thumbnails_title.buffer.text)
2162     end
2163
2164     if $xmldir.attributes['thumbnails-caption']
2165         if edit = $subalbums_edits[$xmldir.attributes['path']]
2166             $xmldir.add_attribute('thumbnails-captionfile', edit[:captionfile])
2167         end
2168     end
2169
2170     #- remove and reinsert elements to reflect new ordering
2171     saves = {}
2172     cpt = 0
2173     $xmldir.elements.each { |element|
2174         if element.name == 'image' || element.name == 'video'
2175             saves[element.attributes['filename']] = element.remove
2176             cpt += 1
2177         end
2178     }
2179     $autotable.current_order.each { |path|
2180         chld = $xmldir.add_element(saves[path].name, saves[path].attributes)
2181         chld.add_attribute('caption', $name2widgets[File.basename(path)][:textview].buffer.text)
2182         saves.delete(path)
2183     }
2184     saves.each_key { |path|
2185         chld = $xmldir.add_element(saves[path].name, saves[path].attributes)
2186         chld.add_attribute('deleted', 'true')
2187     }
2188 end
2189
2190 def sort_by_exif_date
2191     $modified = true
2192     save_changes
2193     current_order = []
2194     $xmldir.elements.each { |element|
2195         if element.name == 'image' || element.name == 'video'
2196             current_order << element.attributes['filename']
2197         end
2198     }
2199
2200     #- look for EXIF dates
2201     dates = {}
2202
2203     if current_order.size > 20
2204         w = create_window
2205         w.set_transient_for($main_window)
2206         w.modal = true
2207         vb = Gtk::VBox.new(false, 5).set_border_width(5)
2208         vb.pack_start(Gtk::Label.new(utf8(_("Scanning sub-album looking for EXIF dates..."))), false, false)
2209         vb.pack_start(pb = Gtk::ProgressBar.new, false, false)
2210         bottom = Gtk::Alignment.new(0.5, 0.5, 0, 0).add(b = Gtk::Button.new(utf8(_("_Abort"))))
2211         b.image = Gtk::Image.new("#{$FPATH}/images/stock-close-24.png")
2212         vb.pack_end(bottom, false, false)
2213         w.add(vb)
2214         w.signal_connect('delete-event') { w.destroy }
2215         w.window_position = Gtk::Window::POS_CENTER
2216         w.show_all
2217
2218         aborted = false
2219         b.signal_connect('clicked') { aborted = true }
2220         i = 0
2221         current_order.each { |f|
2222             i += 1
2223             if entry2type(f) == 'image'
2224                 pb.text = f
2225                 pb.fraction = i.to_f / current_order.size
2226                 Gtk.main_iteration while Gtk.events_pending?
2227                 date_time = Exif.datetimeoriginal(from_utf8($current_path + "/" + f))
2228                 if ! date_time.nil?
2229                     dates[f] = date_time
2230                 end
2231             end
2232             if aborted
2233                 break
2234             end
2235         }
2236         w.destroy
2237         if aborted
2238             return
2239         end
2240
2241     else
2242         current_order.each { |f|
2243             date_time = Exif.datetimeoriginal(from_utf8($current_path + "/" + f))
2244             if ! date_time.nil?
2245                 dates[f] = date_time
2246             end
2247         }
2248     end
2249
2250     saves = {}
2251     $xmldir.elements.each { |element|
2252         if element.name == 'image' || element.name == 'video'
2253             saves[element.attributes['filename']] = element.remove
2254         end
2255     }
2256
2257     neworder = smartsort(current_order, dates)
2258
2259     neworder.each { |f|
2260         $xmldir.add_element(saves[f].name, saves[f].attributes)
2261     }
2262
2263     #- let the auto-table reflect new ordering
2264     change_dir
2265 end
2266
2267 def remove_all_captions
2268     $modified = true
2269     texts = {}
2270     $autotable.current_order.each { |path|
2271         texts[File.basename(path) ] = $name2widgets[File.basename(path)][:textview].buffer.text
2272         $name2widgets[File.basename(path)][:textview].buffer.text = ''
2273     }
2274     save_undo(_("remove all captions"),
2275               proc { |texts|
2276                   texts.each_key { |key|
2277                       $name2widgets[key][:textview].buffer.text = texts[key]
2278                   }
2279                   $notebook.set_page(1)
2280                   proc {
2281                       texts.each_key { |key|
2282                           $name2widgets[key][:textview].buffer.text = ''
2283                       }
2284                       $notebook.set_page(1)
2285                   }
2286               }, texts)
2287 end
2288
2289 def change_dir
2290     $selected_elements.each_key { |path|
2291         $name2widgets[path][:img].pixbuf = $selected_elements[path][:pixbuf]
2292     }
2293     $autotable.clear
2294     $vbox2widgets = {}
2295     $name2widgets = {}
2296     $name2closures = {}
2297     $selected_elements = {}
2298     $cuts = []
2299     $multiple_dnd = []
2300     UndoHandler.cleanup
2301     $undo_tb.sensitive = $undo_mb.sensitive = false
2302     $redo_tb.sensitive = $redo_mb.sensitive = false
2303
2304     if !$current_path
2305         return
2306     end
2307
2308     $subalbums_vb.children.each { |chld|
2309         $subalbums_vb.remove(chld)
2310     }
2311     $subalbums = Gtk::Table.new(0, 0, true)
2312     current_y_sub_albums = 0
2313
2314     $xmldir = Synchronizator.new($xmldoc.elements["//dir[@path='#{$current_path}']"])
2315     $subalbums_edits = {}
2316     subalbums_counter = 0
2317     subalbums_edits_bypos = {}
2318
2319     add_subalbum = proc { |xmldir, counter|
2320         $subalbums_edits[xmldir.attributes['path']] = { :position => counter }
2321         subalbums_edits_bypos[counter] = $subalbums_edits[xmldir.attributes['path']]
2322         if xmldir == $xmldir
2323             thumbnail_file = "#{current_dest_dir}/thumbnails-thumbnail.jpg"
2324             captionfile = from_utf8(xmldir.attributes['thumbnails-captionfile'])
2325             caption = xmldir.attributes['thumbnails-caption']
2326             infotype = 'thumbnails'
2327         else
2328             thumbnail_file = "#{current_dest_dir}/thumbnails-#{make_dest_filename(from_utf8(File.basename(xmldir.attributes['path'])))}.jpg"
2329             captionfile, caption = find_subalbum_caption_info(xmldir)
2330             infotype = find_subalbum_info_type(xmldir)
2331         end
2332         msg 3, "add subdir: #{xmldir.attributes['path']} with file: #{thumbnail_file}"
2333         hbox = Gtk::HBox.new
2334         hbox.pack_start(Gtk::Alignment.new(1, 0.5, 0, 0).add(Gtk::Label.new.set_markup('<i>' + File.basename(xmldir.attributes['path']) + ':</i>')))
2335         f = Gtk::Frame.new
2336         f.set_shadow_type(Gtk::SHADOW_ETCHED_OUT)
2337
2338         img = nil
2339         my_gen_real_thumbnail = proc {
2340             gen_real_thumbnail('subdir', captionfile, thumbnail_file, xmldir, $albums_thumbnail_size, img, infotype)
2341         }
2342
2343         if !$modified_pixbufs[thumbnail_file] && !File.exists?(thumbnail_file)
2344             f.add(img = Gtk::Image.new)
2345             my_gen_real_thumbnail.call
2346         else
2347             f.add(img = Gtk::Image.new($modified_pixbufs[thumbnail_file] ? $modified_pixbufs[thumbnail_file][:pixbuf] : thumbnail_file))
2348         end
2349         hbox.pack_end(Gtk::Alignment.new(0.5, 0.5, 0, 0).add(evtbox = Gtk::EventBox.new.add(f)), false, false)
2350         $subalbums.attach(hbox,
2351                           0, 1, current_y_sub_albums, current_y_sub_albums + 1, Gtk::FILL, Gtk::FILL, 2, 2)
2352
2353         frame, textview = create_editzone($subalbums_sw, 0, img)
2354         textview.buffer.text = caption
2355         $subalbums.attach(Gtk::Alignment.new(0, 0.5, 0.5, 0).add(frame),
2356                           1, 2, current_y_sub_albums, current_y_sub_albums + 1, Gtk::FILL, Gtk::FILL, 2, 2)
2357
2358         change_image = proc {
2359             fc = Gtk::FileChooserDialog.new(utf8(_("Select image for caption")),
2360                                             nil,
2361                                             Gtk::FileChooser::ACTION_OPEN,
2362                                             nil,
2363                                             [Gtk::Stock::OK, Gtk::Dialog::RESPONSE_ACCEPT], [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
2364             fc.set_current_folder(from_utf8(xmldir.attributes['path']))
2365             fc.transient_for = $main_window
2366             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))
2367             f.add(preview_img = Gtk::Image.new)
2368             preview.show_all
2369             fc.signal_connect('update-preview') { |w|
2370                 begin
2371                     if fc.preview_filename
2372                         preview_img.pixbuf = rotate_pixbuf(Gdk::Pixbuf.new(fc.preview_filename, 240, 180), guess_rotate(fc.preview_filename))
2373                         fc.preview_widget_active = true
2374                     end
2375                 rescue Gdk::PixbufError
2376                     fc.preview_widget_active = false
2377                 end
2378             }
2379             if fc.run == Gtk::Dialog::RESPONSE_ACCEPT
2380                 $modified = true
2381                 old_file = captionfile
2382                 old_rotate = xmldir.attributes["#{infotype}-rotate"]
2383                 old_color_swap = xmldir.attributes["#{infotype}-color-swap"]
2384                 old_enhance = xmldir.attributes["#{infotype}-enhance"]
2385                 old_seektime = xmldir.attributes["#{infotype}-seektime"]
2386
2387                 new_file = fc.filename
2388                 msg 3, "new captionfile is: #{fc.filename}"
2389                 perform_changefile = proc {
2390                     $subalbums_edits[xmldir.attributes['path']][:captionfile] = captionfile = new_file
2391                     $modified_pixbufs.delete(thumbnail_file)
2392                     xmldir.delete_attribute("#{infotype}-rotate")
2393                     xmldir.delete_attribute("#{infotype}-color-swap")
2394                     xmldir.delete_attribute("#{infotype}-enhance")
2395                     xmldir.delete_attribute("#{infotype}-seektime")
2396                     my_gen_real_thumbnail.call
2397                 }
2398                 perform_changefile.call
2399
2400                 save_undo(_("change caption file for sub-album"),
2401                           proc {
2402                               $subalbums_edits[xmldir.attributes['path']][:captionfile] = captionfile = old_file
2403                               xmldir.add_attribute("#{infotype}-rotate", old_rotate)
2404                               xmldir.add_attribute("#{infotype}-color-swap", old_color_swap)
2405                               xmldir.add_attribute("#{infotype}-enhance", old_enhance)
2406                               xmldir.add_attribute("#{infotype}-seektime", old_seektime)
2407                               my_gen_real_thumbnail.call
2408                               $notebook.set_page(0)
2409                               proc {
2410                                   perform_changefile.call
2411                                   $notebook.set_page(0)
2412                               }
2413                           })
2414             end
2415             fc.destroy
2416         }
2417
2418         refresh = proc {
2419             if File.exists?(thumbnail_file)
2420                 File.delete(thumbnail_file)
2421             end
2422             my_gen_real_thumbnail.call
2423         }
2424
2425         rotate_and_cleanup = proc { |angle|
2426             rotate(angle, thumbnail_file, img, xmldir, "#{infotype}-", $default_albums_thumbnails[:x], $default_albums_thumbnails[:y])
2427             if File.exists?(thumbnail_file)
2428                 File.delete(thumbnail_file)
2429             end
2430         }
2431
2432         move = proc { |direction|
2433             $modified = true
2434
2435             save_changes('forced')
2436             oldpos = $subalbums_edits[xmldir.attributes['path']][:position]
2437             if direction == 'up'
2438                 $subalbums_edits[xmldir.attributes['path']][:position] -= 1
2439                 subalbums_edits_bypos[oldpos - 1][:position] += 1
2440             end
2441             if direction == 'down'
2442                 $subalbums_edits[xmldir.attributes['path']][:position] += 1
2443                 subalbums_edits_bypos[oldpos + 1][:position] -= 1
2444             end
2445             if direction == 'top'
2446                 for i in 1 .. oldpos - 1
2447                     subalbums_edits_bypos[i][:position] += 1
2448                 end
2449                 $subalbums_edits[xmldir.attributes['path']][:position] = 1
2450             end
2451             if direction == 'bottom'
2452                 for i in oldpos + 1 .. subalbums_counter
2453                     subalbums_edits_bypos[i][:position] -= 1
2454                 end
2455                 $subalbums_edits[xmldir.attributes['path']][:position] = subalbums_counter
2456             end
2457
2458             elems = []
2459             $xmldir.elements.each('dir') { |element|
2460                 if (!element.attributes['deleted'])
2461                     elems << [ element.attributes['path'], element.remove ]
2462                 end
2463             }
2464             elems.sort { |a,b| $subalbums_edits[a[0]][:position] <=> $subalbums_edits[b[0]][:position] }.
2465                   each { |e| $xmldir.add_element(e[1]) }
2466             #- need to remove the already-generated tag to all subdirs because of "previous/next albums" links completely changed
2467             $xmldir.elements.each('descendant::dir') { |elem|
2468                 elem.delete_attribute('already-generated')
2469             }
2470
2471             sel = $albums_tv.selection.selected_rows
2472             change_dir
2473             populate_subalbums_treeview(false)
2474             $albums_tv.selection.select_path(sel[0])
2475         }
2476
2477         color_swap_and_cleanup = proc {
2478             perform_color_swap_and_cleanup = proc {
2479                 color_swap(xmldir, "#{infotype}-")
2480                 my_gen_real_thumbnail.call
2481             }
2482             perform_color_swap_and_cleanup.call
2483
2484             save_undo(_("color swap"),
2485                       proc {
2486                           perform_color_swap_and_cleanup.call
2487                           $notebook.set_page(0)
2488                           proc {
2489                               perform_color_swap_and_cleanup.call
2490                               $notebook.set_page(0)
2491                           }
2492                       })
2493         }
2494
2495         change_seektime_and_cleanup = proc {
2496             if values = ask_new_seektime(xmldir, "#{infotype}-")
2497                 perform_change_seektime_and_cleanup = proc { |val|
2498                     change_seektime(xmldir, "#{infotype}-", val)
2499                     my_gen_real_thumbnail.call
2500                 }
2501                 perform_change_seektime_and_cleanup.call(values[:new])
2502
2503                 save_undo(_("specify seektime"),
2504                           proc {
2505                               perform_change_seektime_and_cleanup.call(values[:old])
2506                               $notebook.set_page(0)
2507                               proc {
2508                                   perform_change_seektime_and_cleanup.call(values[:new])
2509                                   $notebook.set_page(0)
2510                               }
2511                           })
2512             end
2513         }
2514
2515         whitebalance_and_cleanup = proc {
2516             if values = ask_whitebalance(captionfile, thumbnail_file, img, xmldir, "#{infotype}-",
2517                                          $default_albums_thumbnails[:x], $default_albums_thumbnails[:y], infotype)
2518                 perform_change_whitebalance_and_cleanup = proc { |val|
2519                     change_whitebalance(xmldir, "#{infotype}-", val)
2520                     recalc_whitebalance(val, captionfile, thumbnail_file, img, xmldir, "#{infotype}-",
2521                                         $default_albums_thumbnails[:x], $default_albums_thumbnails[:y], infotype)
2522                     if File.exists?(thumbnail_file)
2523                         File.delete(thumbnail_file)
2524                     end
2525                 }
2526                 perform_change_whitebalance_and_cleanup.call(values[:new])
2527                 
2528                 save_undo(_("fix white balance"),
2529                           proc {
2530                               perform_change_whitebalance_and_cleanup.call(values[:old])
2531                               $notebook.set_page(0)
2532                               proc {
2533                                   perform_change_whitebalance_and_cleanup.call(values[:new])
2534                                   $notebook.set_page(0)
2535                               }
2536                           })
2537             end
2538         }
2539
2540         gammacorrect_and_cleanup = proc {
2541             if values = ask_gammacorrect(captionfile, thumbnail_file, img, xmldir, "#{infotype}-",
2542                                          $default_albums_thumbnails[:x], $default_albums_thumbnails[:y], infotype)
2543                 perform_change_gammacorrect_and_cleanup = proc { |val|
2544                     change_gammacorrect(xmldir, "#{infotype}-", val)
2545                     recalc_gammacorrect(val, captionfile, thumbnail_file, img, xmldir, "#{infotype}-",
2546                                         $default_albums_thumbnails[:x], $default_albums_thumbnails[:y], infotype)
2547                     if File.exists?(thumbnail_file)
2548                         File.delete(thumbnail_file)
2549                     end
2550                 }
2551                 perform_change_gammacorrect_and_cleanup.call(values[:new])
2552                 
2553                 save_undo(_("gamma correction"),
2554                           proc {
2555                               perform_change_gammacorrect_and_cleanup.call(values[:old])
2556                               $notebook.set_page(0)
2557                               proc {
2558                                   perform_change_gammacorrect_and_cleanup.call(values[:new])
2559                                   $notebook.set_page(0)
2560                               }
2561                           })
2562             end
2563         }
2564
2565         enhance_and_cleanup = proc {
2566             perform_enhance_and_cleanup = proc {
2567                 enhance(xmldir, "#{infotype}-")
2568                 my_gen_real_thumbnail.call
2569             }
2570             
2571             perform_enhance_and_cleanup.call
2572             
2573             save_undo(_("enhance"),
2574                       proc {
2575                           perform_enhance_and_cleanup.call
2576                           $notebook.set_page(0)
2577                           proc {
2578                               perform_enhance_and_cleanup.call
2579                               $notebook.set_page(0)
2580                           }
2581                       })
2582         }
2583
2584         evtbox.signal_connect('button-press-event') { |w, event|
2585             if event.event_type == Gdk::Event::BUTTON_PRESS && event.button == 1
2586                 if $r90.active?
2587                     rotate_and_cleanup.call(90)
2588                 elsif $r270.active?
2589                     rotate_and_cleanup.call(-90)
2590                 elsif $enhance.active?
2591                     enhance_and_cleanup.call
2592                 end
2593             end
2594             if event.event_type == Gdk::Event::BUTTON_PRESS && event.button == 3
2595                 popup_thumbnail_menu(event, ['change_image', 'move_top', 'move_bottom'], captionfile, entry2type(captionfile), xmldir, "#{infotype}-",
2596                                      { :forbid_left => true, :forbid_right => true,
2597                                        :can_up => counter > 1, :can_down => counter > 0 && counter < subalbums_counter,
2598                                        :can_top => counter > 1, :can_bottom => counter > 0 && counter < subalbums_counter },
2599                                      { :change => change_image, :move => move, :rotate => rotate_and_cleanup, :enhance => enhance_and_cleanup,
2600                                        :color_swap => color_swap_and_cleanup, :seektime => change_seektime_and_cleanup, :whitebalance => whitebalance_and_cleanup,
2601                                        :gammacorrect => gammacorrect_and_cleanup, :refresh => refresh })
2602             end
2603             if event.event_type == Gdk::Event::BUTTON2_PRESS && event.button == 1
2604                 change_image.call
2605                 true   #- handled
2606             end
2607         }
2608         evtbox.signal_connect('button-press-event') { |w, event|
2609             $gesture_press = { :filename => thumbnail_file, :x => event.x, :y => event.y }
2610             false
2611         }
2612
2613         evtbox.signal_connect('button-release-event') { |w, event|
2614             if !$r90.active? && !$r270.active? && $gesture_press && $gesture_press[:filename] == thumbnail_file
2615                 msg 3, "press: #{$gesture_press[:x]} release: #{event.x}"
2616                 if (($gesture_press[:x]-event.x)/($gesture_press[:y]-event.y)).abs > 2 && ($gesture_press[:x]-event.x).abs > 5
2617                     angle = event.x-$gesture_press[:x] > 0 ? 90 : -90
2618                     msg 3, "gesture rotate: #{angle}"
2619                     rotate_and_cleanup.call(angle)
2620                 end
2621             end
2622             $gesture_press = nil
2623         }
2624                 
2625         $subalbums_edits[xmldir.attributes['path']][:editzone] = textview
2626         $subalbums_edits[xmldir.attributes['path']][:captionfile] = captionfile
2627         current_y_sub_albums += 1
2628     }
2629
2630     if $xmldir.child_byname_notattr('dir', 'deleted')
2631         #- title edition
2632         frame, $subalbums_title = create_editzone($subalbums_sw, 0, nil)
2633         $subalbums_title.buffer.text = $xmldir.attributes['subdirs-caption']
2634         $subalbums_title.set_justification(Gtk::Justification::CENTER)
2635         $subalbums_vb.pack_start(Gtk::Alignment.new(0.5, 0.5, 0.5, 0).add(frame), false, false)
2636         #- this album image/caption
2637         if $xmldir.attributes['thumbnails-caption']
2638             add_subalbum.call($xmldir, 0)
2639         end
2640     end
2641     total = { 'image' => 0, 'video' => 0, 'dir' => 0 }
2642     $xmldir.elements.each { |element|
2643         if (element.name == 'image' || element.name == 'video') && !element.attributes['deleted']
2644             #- element (image or video) of this album
2645             dest_img = build_full_dest_filename(element.attributes['filename']).sub(/\.[^\.]+$/, '') + "-#{$default_size['thumbnails']}.jpg"
2646             msg 3, "dest_img: #{dest_img}"
2647             add_thumbnail($autotable, element.attributes['filename'], element.name, dest_img, element.attributes['caption'])
2648             total[element.name] += 1
2649         end
2650         if element.name == 'dir' && !element.attributes['deleted']
2651             #- sub-album image/caption
2652             add_subalbum.call(element, subalbums_counter += 1)
2653             total[element.name] += 1
2654         end
2655     }
2656     $statusbar.push(0, utf8(_("%s: %s images and %s videos, %s sub-albums") % [ File.basename(from_utf8($xmldir.attributes['path'])),
2657                                                                                 total['image'], total['video'], total['dir'] ]))
2658     $subalbums_vb.add($subalbums)
2659     $subalbums_vb.show_all
2660
2661     if !$xmldir.child_byname_notattr('image', 'deleted') && !$xmldir.child_byname_notattr('video', 'deleted')
2662         $notebook.get_tab_label($autotable_sw).sensitive = false
2663         $notebook.set_page(0)
2664         $thumbnails_title.buffer.text = ''
2665     else
2666         $notebook.get_tab_label($autotable_sw).sensitive = true
2667         $thumbnails_title.buffer.text = $xmldir.attributes['thumbnails-caption']
2668     end
2669
2670     if !$xmldir.child_byname_notattr('dir', 'deleted')
2671         $notebook.get_tab_label($subalbums_sw).sensitive = false
2672         $notebook.set_page(1)
2673     else
2674         $notebook.get_tab_label($subalbums_sw).sensitive = true
2675     end
2676 end
2677
2678 def pixbuf_or_nil(filename)
2679     begin
2680         return Gdk::Pixbuf.new(filename)
2681     rescue
2682         return nil
2683     end
2684 end
2685
2686 def theme_choose(current)
2687     dialog = Gtk::Dialog.new(utf8(_("Select your preferred theme")),
2688                              $main_window,
2689                              Gtk::Dialog::MODAL | Gtk::Dialog::DESTROY_WITH_PARENT,
2690                              [Gtk::Stock::OK, Gtk::Dialog::RESPONSE_OK],
2691                              [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
2692
2693     model = Gtk::ListStore.new(String, Gdk::Pixbuf, Gdk::Pixbuf, Gdk::Pixbuf)
2694     treeview = Gtk::TreeView.new(model).set_rules_hint(true)
2695     treeview.append_column(Gtk::TreeViewColumn.new(utf8(_("Theme name")), Gtk::CellRendererText.new, { :text => 0 }).set_alignment(0.5).set_spacing(5))
2696     treeview.append_column(Gtk::TreeViewColumn.new(utf8(_("Sub-albums page look")), Gtk::CellRendererPixbuf.new, { :pixbuf => 1 }).set_alignment(0.5).set_spacing(5))
2697     treeview.append_column(Gtk::TreeViewColumn.new(utf8(_("Thumbnails page look")), Gtk::CellRendererPixbuf.new, { :pixbuf => 2 }).set_alignment(0.5).set_spacing(5))
2698     treeview.append_column(Gtk::TreeViewColumn.new(utf8(_("Fullscreen page look")), Gtk::CellRendererPixbuf.new, { :pixbuf => 3 }).set_alignment(0.5).set_spacing(5))
2699     treeview.append_column(Gtk::TreeViewColumn.new('', Gtk::CellRendererText.new, {}))
2700     treeview.signal_connect('button-press-event') { |w, event|
2701         if event.event_type == Gdk::Event::BUTTON2_PRESS && event.button == 1
2702             dialog.response(Gtk::Dialog::RESPONSE_OK)
2703         end
2704     }
2705
2706     dialog.vbox.add(sw = Gtk::ScrolledWindow.new(nil, nil).add(treeview).set_policy(Gtk::POLICY_AUTOMATIC, Gtk::POLICY_AUTOMATIC))
2707
2708     `find '#{$FPATH}/themes' -mindepth 1 -maxdepth 1 -type d`.each { |dir|
2709         dir.chomp!
2710         iter = model.append
2711         iter[0] = File.basename(dir)
2712         iter[1] = pixbuf_or_nil("#{dir}/metadata/screenshot-1.png")
2713         iter[2] = pixbuf_or_nil("#{dir}/metadata/screenshot-2.png")
2714         iter[3] = pixbuf_or_nil("#{dir}/metadata/screenshot-3.png")
2715         if File.basename(dir) == current
2716             treeview.selection.select_iter(iter)
2717         end
2718     }
2719
2720     dialog.set_default_size(700, 400)
2721     dialog.vbox.show_all
2722     dialog.run { |response|
2723         iter = treeview.selection.selected
2724         dialog.destroy
2725         if response == Gtk::Dialog::RESPONSE_OK && iter
2726             return model.get_value(iter, 0)
2727         end
2728     }
2729     return nil
2730 end
2731
2732 def show_password_protections
2733     examine_dir_elem = proc { |parent_iter, xmldir, already_protected|
2734         child_iter = $albums_iters[xmldir.attributes['path']]
2735         if xmldir.attributes['password-protect']
2736             child_iter[2] = pixbuf_or_nil("#{$FPATH}/images/galeon-secure.png")
2737             already_protected = true
2738         elsif already_protected
2739             pix = pixbuf_or_nil("#{$FPATH}/images/galeon-secure-shadow.png")
2740             if pix
2741                 pix = pix.saturate_and_pixelate(1, true)
2742             end
2743             child_iter[2] = pix
2744         else
2745             child_iter[2] = nil
2746         end
2747         xmldir.elements.each('dir') { |elem|
2748             if !elem.attributes['deleted']
2749                 examine_dir_elem.call(child_iter, elem, already_protected)
2750             end
2751         }
2752     }
2753     examine_dir_elem.call(nil, $xmldoc.elements['//dir'], false)
2754 end
2755
2756 def populate_subalbums_treeview(select_first)
2757     $albums_ts.clear
2758     $autotable.clear
2759     $albums_iters = {}
2760     $subalbums_vb.children.each { |chld|
2761         $subalbums_vb.remove(chld)
2762     }
2763
2764     source = $xmldoc.root.attributes['source']
2765     msg 3, "source: #{source}"
2766
2767     xmldir = $xmldoc.elements['//dir']
2768     if !xmldir || xmldir.attributes['path'] != source
2769         msg 1, _("Corrupted booh file...")
2770         return
2771     end
2772
2773     append_dir_elem = proc { |parent_iter, xmldir|
2774         child_iter = $albums_ts.append(parent_iter)
2775         child_iter[0] = File.basename(xmldir.attributes['path'])
2776         child_iter[1] = xmldir.attributes['path']
2777         $albums_iters[xmldir.attributes['path']] = child_iter
2778         msg 3, "puttin location: #{xmldir.attributes['path']}"
2779         xmldir.elements.each('dir') { |elem|
2780             if !elem.attributes['deleted']
2781                 append_dir_elem.call(child_iter, elem)
2782             end
2783         }
2784     }
2785     append_dir_elem.call(nil, xmldir)
2786     show_password_protections
2787
2788     $albums_tv.expand_all
2789     if select_first
2790         $albums_tv.selection.select_iter($albums_ts.iter_first)
2791     end
2792 end
2793
2794 def select_current_theme
2795     select_theme($xmldoc.root.attributes['theme'],
2796                  $xmldoc.root.attributes['limit-sizes'],
2797                  !$xmldoc.root.attributes['optimize-for-32'].nil?,
2798                  $xmldoc.root.attributes['thumbnails-per-row'])
2799 end
2800
2801 def open_file(filename)
2802
2803     $filename = nil
2804     $modified = false
2805     $current_path = nil   #- invalidate
2806     $modified_pixbufs = {}
2807     $albums_ts.clear
2808     $autotable.clear
2809     $subalbums_vb.children.each { |chld|
2810         $subalbums_vb.remove(chld)
2811     }
2812
2813     if !File.exists?(filename)
2814         return utf8(_("File not found."))
2815     end
2816
2817     begin
2818         $xmldoc = REXML::Document.new File.new(filename)
2819     rescue Exception
2820         $xmldoc = nil
2821     end
2822
2823     if !$xmldoc || !$xmldoc.root || $xmldoc.root.name != 'booh'
2824         if entry2type(filename).nil?
2825             return utf8(_("Not a booh file!"))
2826         else
2827             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."))
2828         end
2829     end
2830
2831     if !source = $xmldoc.root.attributes['source']
2832         return utf8(_("Corrupted booh file..."))
2833     end
2834
2835     if !dest = $xmldoc.root.attributes['destination']
2836         return utf8(_("Corrupted booh file..."))
2837     end
2838
2839     if !theme = $xmldoc.root.attributes['theme']
2840         return utf8(_("Corrupted booh file..."))
2841     end
2842
2843     if $xmldoc.root.attributes['version'] < '0.8.99.2'
2844         msg 2, _("File's version %s, booh version now %s, marking dirty") % [ $xmldoc.root.attributes['version'], $VERSION ]
2845         mark_document_as_dirty
2846         if $xmldoc.root.attributes['version'] < '0.8.4'
2847             msg 1, _("File's version prior to 0.8.4, migrating directories and filenames in destination directory if needed")
2848             `find '#{source}' -type d -follow`.sort.collect { |v| v.chomp }.each { |dir|
2849                 old_dest_dir = make_dest_filename_old(dir.sub(/^#{Regexp.quote(source)}/, dest))
2850                 new_dest_dir = make_dest_filename(dir.sub(/^#{Regexp.quote(source)}/, dest))
2851                 if old_dest_dir != new_dest_dir
2852                     sys("mv '#{old_dest_dir}' '#{new_dest_dir}'")
2853                 end
2854                 if xmldir = $xmldoc.elements["//dir[@path='#{utf8(dir)}']"]
2855                     xmldir.elements.each { |element|
2856                         if %w(image video).include?(element.name) && !element.attributes['deleted']
2857                             old_name = new_dest_dir + '/' + make_dest_filename_old(from_utf8(element.attributes['filename'])).sub(/\.[^\.]+$/, '')
2858                             new_name = new_dest_dir + '/' + make_dest_filename(from_utf8(element.attributes['filename'])).sub(/\.[^\.]+$/, '')
2859                             Dir[old_name + '*'].each { |file|
2860                                 new_file = file.sub(/^#{Regexp.quote(old_name)}/, new_name)
2861                                 file != new_file and sys("mv '#{file}' '#{new_file}'")
2862                             }
2863                         end
2864                         if element.name == 'dir' && !element.attributes['deleted']
2865                             old_name = new_dest_dir + '/thumbnails-' + make_dest_filename_old(from_utf8(File.basename(element.attributes['path']))) + '.jpg'
2866                             new_name = new_dest_dir + '/thumbnails-' + make_dest_filename(from_utf8(File.basename(element.attributes['path']))) + '.jpg'
2867                             old_name != new_name and sys("mv '#{old_name}' '#{new_name}'")
2868                         end
2869                     }
2870                 else
2871                     msg 1, "could not find <dir> element by path '#{utf8(dir)}' in xml file"
2872                 end
2873             }
2874         end
2875         $xmldoc.root.add_attribute('version', $VERSION)
2876     end
2877
2878     select_current_theme
2879
2880     $filename = filename
2881     set_mainwindow_title(nil)
2882     $default_size['thumbnails'] =~ /(.*)x(.*)/
2883     $default_thumbnails = { :x => $1.to_i, :y => $2.to_i }
2884     $albums_thumbnail_size =~ /(.*)x(.*)/
2885     $default_albums_thumbnails = { :x => $1.to_i, :y => $2.to_i }
2886
2887     populate_subalbums_treeview(true)
2888
2889     $save.sensitive = $save_as.sensitive = $merge_current.sensitive = $merge_newsubs.sensitive = $merge.sensitive = $generate.sensitive = $view_wa.sensitive = $properties.sensitive = $remove_all_captions.sensitive = $sort_by_exif_date.sensitive = true
2890     return nil
2891 end
2892
2893 def open_file_user(filename)
2894     result = open_file(filename)
2895     if !result
2896         $config['last-opens'] ||= []
2897         if $config['last-opens'][-1] != utf8(filename)
2898             $config['last-opens'] << utf8(filename)
2899         end
2900         $orig_filename = $filename
2901         tmp = Tempfile.new("boohtemp")
2902         tmp.close!
2903         #- for security
2904         ios = File.open($filename = tmp.path, File::RDWR|File::CREAT|File::EXCL)
2905         ios.close
2906         $tempfiles << $filename << "#{$filename}.backup"
2907     else
2908         $orig_filename = nil
2909     end
2910     return result
2911 end
2912
2913 def open_file_popup
2914     if !ask_save_modifications(utf8(_("Save this album?")),
2915                                utf8(_("Do you want to save the changes to this album?")),
2916                                { :yes => Gtk::Stock::YES, :no => Gtk::Stock::NO })
2917         return
2918     end
2919     fc = Gtk::FileChooserDialog.new(utf8(_("Open file")),
2920                                     nil,
2921                                     Gtk::FileChooser::ACTION_OPEN,
2922                                     nil,
2923                                     [Gtk::Stock::OPEN, Gtk::Dialog::RESPONSE_ACCEPT], [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
2924     fc.add_shortcut_folder(File.expand_path("~/.booh"))
2925     fc.set_current_folder(File.expand_path("~/.booh"))
2926     fc.transient_for = $main_window
2927     ok = false
2928     while !ok
2929         if fc.run == Gtk::Dialog::RESPONSE_ACCEPT
2930             push_mousecursor_wait(fc)
2931             msg = open_file_user(fc.filename)
2932             pop_mousecursor(fc)
2933             if msg
2934                 show_popup(fc, msg)
2935                 ok = false
2936             else
2937                 ok = true
2938             end
2939         else
2940             ok = true
2941         end
2942     end
2943     fc.destroy
2944 end
2945
2946 def additional_booh_options
2947     options = ''
2948     if $config['mproc']
2949         options += "--mproc #{$config['mproc'].to_i} "
2950     end
2951     options += "--comments-format '#{$config['comments-format']}' "
2952     if $config['transcode-videos']
2953         options += "--transcode-videos '#{$config['transcode-videos']}' "
2954     end
2955     return options
2956 end
2957
2958 def ask_multi_languages(value)
2959     if ! value.nil?
2960         spl = value.split(',')
2961         value = [ spl[0..-2], spl[-1] ]
2962     end
2963
2964     dialog = Gtk::Dialog.new(utf8(_("Multi-languages support")),
2965                              $main_window,
2966                              Gtk::Dialog::MODAL,
2967                              [Gtk::Stock::OK, Gtk::Dialog::RESPONSE_OK],
2968                              [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
2969
2970     lbl = Gtk::Label.new
2971     lbl.markup = utf8(
2972 _("You can choose to activate <b>multi-languages</b> support for this web-album
2973 (it will work only if you publish your web-album on an Apache web-server). This will
2974 use the MultiViews feature of Apache; the pages will be served according to the
2975 value of the Accept-Language HTTP header sent by the web browsers, so that people
2976 with different languages preferences will be able to browse your web-album with
2977 navigation in their language (if language is available).
2978 "))
2979
2980     dialog.vbox.add(lbl)
2981     dialog.vbox.add(Gtk::Alignment.new(0.5, 0.5, 0, 0).add(Gtk::VBox.new.add(rb_no = Gtk::RadioButton.new(utf8(_("Disabled")))).
2982                                                                          add(Gtk::HBox.new(false, 5).add(rb_yes = Gtk::RadioButton.new(rb_no, utf8(_("Enabled")))).
2983                                                                                                      add(languages = Gtk::Button.new))))
2984
2985     pick_languages = proc {
2986         dialog2 = Gtk::Dialog.new(utf8(_("Pick languages to support")),
2987                                   $main_window,
2988                                   Gtk::Dialog::MODAL,
2989                                   [Gtk::Stock::OK, Gtk::Dialog::RESPONSE_OK],
2990                                   [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
2991
2992         dialog2.vbox.add(Gtk::Alignment.new(0.5, 0.5, 0, 0).add(hb1 = Gtk::HBox.new))
2993         hb1.add(Gtk::Label.new(utf8(_("Select the languages to support:"))))
2994         cbs = []
2995         SUPPORTED_LANGUAGES.each { |lang|
2996             hb1.add(cb = Gtk::CheckButton.new(utf8(langname(lang))))
2997             if ! value.nil? && value[0].include?(lang)
2998                 cb.active = true
2999             end
3000             cbs << [ lang, cb ]
3001         }
3002
3003         dialog2.vbox.add(Gtk::Alignment.new(0.5, 0.5, 0, 0).add(hb2 = Gtk::HBox.new))
3004         hb2.add(Gtk::Label.new(utf8(_("Select the fallback language:"))))
3005         fallback_language = nil
3006         hb2.add(fbl_rb = Gtk::RadioButton.new(utf8(langname(SUPPORTED_LANGUAGES[0]))))
3007         fbl_rb.signal_connect('clicked') { fallback_language = SUPPORTED_LANGUAGES[0] }
3008         if value.nil? || value[1] == SUPPORTED_LANGUAGES[0]
3009             fbl_rb.active = true
3010             fallback_language = SUPPORTED_LANGUAGES[0]
3011         end
3012         SUPPORTED_LANGUAGES[1..-1].each { |lang|
3013             hb2.add(rb = Gtk::RadioButton.new(fbl_rb, utf8(langname(lang))))
3014             rb.signal_connect('clicked') { fallback_language = lang }
3015             if ! value.nil? && value[1] == lang
3016                 rb.active = true
3017             end
3018         }
3019
3020         dialog2.window_position = Gtk::Window::POS_MOUSE
3021         dialog2.show_all
3022
3023         resp = nil
3024         dialog2.run { |response|
3025             resp = response
3026             if resp == Gtk::Dialog::RESPONSE_OK
3027                 value = []
3028                 value[0] = cbs.find_all { |e| e[1].active? }.collect { |e| e[0] }
3029                 value[1] = fallback_language
3030                 languages.label = utf8(_("Languages: %s. Fallback: %s.") % [ value[0].collect { |v| utf8(langname(v)) }.join(', '), utf8(langname(value[1])) ])
3031             end
3032             dialog2.destroy
3033         }
3034         resp
3035     }
3036
3037     languages.signal_connect('clicked') {
3038         pick_languages.call
3039     }
3040     dialog.window_position = Gtk::Window::POS_MOUSE
3041     if value.nil?
3042         rb_no.active = true
3043     else
3044         rb_yes.active = true
3045         languages.label = utf8(_("Languages: %s. Fallback: %s.") % [ value[0].collect { |v| utf8(langname(v)) }.join(', '), utf8(langname(value[1])) ])
3046     end
3047     rb_no.signal_connect('clicked') {
3048         if rb_no.active?
3049             languages.hide
3050         else
3051             if pick_languages.call == Gtk::Dialog::RESPONSE_CANCEL
3052                 rb_no.activate
3053             else
3054                 languages.show
3055             end
3056         end
3057     }
3058     oldval = value
3059     dialog.show_all
3060     if rb_no.active?
3061         languages.hide
3062     end
3063
3064     dialog.run { |response|
3065         if rb_no.active?
3066             value = nil
3067         end
3068         dialog.destroy
3069         if response == Gtk::Dialog::RESPONSE_OK && value != oldval
3070             if value.nil?
3071                 return [ true, nil ]
3072             else
3073                 return [ true, (value[0].size == 0 ? '' : value[0].join(',') + ',') + value[1] ]
3074             end
3075         else
3076             return [ false ]
3077         end
3078     }
3079 end
3080
3081 def new_album
3082     if !ask_save_modifications(utf8(_("Save this album?")),
3083                                utf8(_("Do you want to save the changes to this album?")),
3084                                { :yes => Gtk::Stock::YES, :no => Gtk::Stock::NO })
3085         return
3086     end
3087     dialog = Gtk::Dialog.new(utf8(_("Create a new album")),
3088                              $main_window,
3089                              Gtk::Dialog::MODAL | Gtk::Dialog::DESTROY_WITH_PARENT,
3090                              [Gtk::Stock::OK, Gtk::Dialog::RESPONSE_OK],
3091                              [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
3092     
3093     frame1 = Gtk::Frame.new(utf8(_("Locations"))).add(tbl = Gtk::Table.new(0, 0, false))
3094     tbl.attach(Gtk::Label.new(utf8(_("Directory of images/videos: "))),
3095                0, 1, 0, 1, Gtk::SHRINK, Gtk::SHRINK, 2, 2)
3096     tbl.attach(src = Gtk::Entry.new,
3097                1, 2, 0, 1, Gtk::FILL, Gtk::SHRINK, 2, 2)
3098     tbl.attach(src_browse = Gtk::Button.new(utf8(_("browse..."))),
3099                2, 3, 0, 1, Gtk::SHRINK, Gtk::SHRINK, 2, 2)
3100     tbl.attach(Gtk::Label.new.set_markup(utf8(_("<span size='small'><i>number of images/videos down this directory:</i></span> "))),
3101                0, 1, 1, 2, Gtk::SHRINK, Gtk::SHRINK, 2, 2)
3102     tbl.attach(src_nb = Gtk::Label.new.set_markup(utf8(_("<span size='small'><i>N/A</i></span>"))),
3103                1, 2, 1, 2, Gtk::FILL, Gtk::SHRINK, 2, 2)
3104     tbl.attach(Gtk::Label.new(utf8(_("Directory where to put the web-album: "))),
3105                0, 1, 2, 3, Gtk::SHRINK, Gtk::SHRINK, 2, 2)
3106     tbl.attach(dest = Gtk::Entry.new,
3107                1, 2, 2, 3, Gtk::FILL, Gtk::SHRINK, 2, 2)
3108     tbl.attach(dest_browse = Gtk::Button.new(utf8(_("browse..."))),
3109                2, 3, 2, 3, Gtk::SHRINK, Gtk::SHRINK, 2, 2)
3110     tbl.attach(Gtk::Label.new(utf8(_("Filename to store this album's properties: "))),
3111                0, 1, 3, 4, Gtk::SHRINK, Gtk::SHRINK, 2, 2)
3112     tbl.attach(conf = Gtk::Entry.new.set_size_request(250, -1),
3113                1, 2, 3, 4, Gtk::FILL, Gtk::SHRINK, 2, 2)
3114     tbl.attach(conf_browse = Gtk::Button.new(utf8(_("browse..."))),
3115                2, 3, 3, 4, Gtk::SHRINK, Gtk::SHRINK, 2, 2)
3116
3117     tooltips = Gtk::Tooltips.new
3118     frame2 = Gtk::Frame.new(utf8(_("Configuration"))).add(vb = Gtk::VBox.new)
3119     vb.add(Gtk::HBox.new(false, 3).pack_start(Gtk::Label.new(utf8(_("Theme: "))), false, false, 0).
3120                          pack_start(theme_button = Gtk::Button.new($config['default-theme'] || 'simple'), false, false, 0))
3121     vb.add(Gtk::HBox.new(false, 3).pack_start(Gtk::Label.new(utf8(_("Sizes of images to generate: "))), false, false, 0).
3122                                    pack_start(sizes = Gtk::HBox.new, false, false, 0))
3123     vb.add(optimize432 = Gtk::CheckButton.new(utf8(_("Optimize for 3/2 aspect ratio"))))
3124     tooltips.set_tip(optimize432, utf8(_("Resize images with optimized sizes for 3/2 aspect ratio rather than 4/3 (typical aspect ratio of pictures from non digital cameras are 3/2 when pictures from digital cameras are 4/3)")), nil)
3125     vb.add(Gtk::HBox.new(false, 3).pack_start(Gtk::Label.new(utf8(_("Number of thumbnails per row: "))), false, false, 0).
3126                                    pack_start(nperrowradios = Gtk::HBox.new, false, false, 0))
3127     nperpage_model = Gtk::ListStore.new(String, String)
3128     vb.add(Gtk::HBox.new(false, 3).pack_start(Gtk::Label.new(utf8(_("Number of thumbnails per page: "))), false, false, 0).
3129                                    pack_start(nperpagecombo = Gtk::ComboBox.new(nperpage_model), false, false, 0))
3130     nperpagecombo.pack_start(crt = Gtk::CellRendererText.new, false)
3131     nperpagecombo.set_attributes(crt, { :markup => 0 })
3132     iter = nperpage_model.append
3133     iter[0] = utf8(_("<i>None - all thumbnails in one page</i>"))
3134     iter[1] = nil
3135     [ 12, 20, 30, 40, 50 ].each { |v|
3136         iter = nperpage_model.append
3137         iter[0] = iter[1] = v.to_s
3138     }
3139     nperpagecombo.active = 0
3140
3141     multilanguages_value = nil
3142     vb.add(ml = Gtk::HBox.new(false, 3).pack_start(ml_label = Gtk::Label.new(utf8(_("Multi-languages: disabled."))), false, false, 0).
3143                                         pack_start(multilanguages = Gtk::Button.new(utf8(_("Configure multi-languages"))), false, false, 0))
3144     tooltips.set_tip(ml, utf8(_("When disabled, the web-album will be generated with navigation in your desktop language. When enabled, the web-album will be generated with navigation in all languages you select, but you have to publish your web-album on an Apache web-server for that feature to work.")), nil)
3145     multilanguages.signal_connect('clicked') {
3146         retval = ask_multi_languages(multilanguages_value)
3147         if retval[0] 
3148             multilanguages_value = retval[1]
3149         end
3150         if multilanguages_value
3151             ml_label.text = utf8(_("Multi-languages: enabled."))
3152         else
3153             ml_label.text = utf8(_("Multi-languages: disabled."))
3154         end
3155     }
3156
3157     vb.add(Gtk::HBox.new(false, 3).pack_start(Gtk::Label.new(utf8(_("'Return to your website' link on pages bottom: "))), false, false, 0).
3158                                    pack_start(indexlinkentry = Gtk::Entry.new, true, true, 0))
3159     tooltips.set_tip(indexlinkentry, utf8(_("Optional HTML markup to use on pages bottom for a small link returning to wherever you see fit in your website (or somewhere else)")), nil)
3160     vb.add(Gtk::HBox.new(false, 3).pack_start(Gtk::Label.new(utf8(_("'Made with' markup on pages bottom: "))), false, false, 0).
3161                                    pack_start(madewithentry = Gtk::Entry.new.set_text('made with <a href=%booh>booh</a>!'), true, true, 0))
3162     tooltips.set_tip(madewithentry, utf8(_("Optional HTML markup to use on pages bottom for a small 'made with' label; %booh is replaced by the website of booh;\nfor example: made with <a href=%booh>booh</a>!")), nil)
3163
3164     src_nb_calculated_for = ''
3165     src_nb_thread = nil
3166     process_src_nb = proc {
3167         if src.text != src_nb_calculated_for
3168             src_nb_calculated_for = src.text
3169             if src_nb_thread
3170                 Thread.kill(src_nb_thread)
3171                 src_nb_thread = nil
3172             end
3173             if src_nb_calculated_for != '' && from_utf8_safe(src_nb_calculated_for) == ''
3174                 src_nb.set_markup(utf8(_("<span size='small'><i>invalid source directory</i></span>")))
3175             else
3176                 if File.directory?(from_utf8_safe(src_nb_calculated_for)) && src_nb_calculated_for != '/'
3177                     if File.readable?(from_utf8_safe(src_nb_calculated_for))
3178                         src_nb_thread = Thread.new {
3179                             gtk_thread_protect { src_nb.set_markup(utf8(_("<span size='small'><i>processing...</i></span>"))) }
3180                             total = { 'image' => 0, 'video' => 0, nil => 0 }
3181                             `find '#{from_utf8_safe(src_nb_calculated_for)}' -type d -follow`.each { |dir|
3182                                 if File.basename(dir) =~ /^\./
3183                                     next
3184                                 else
3185                                     begin
3186                                         Dir.entries(dir.chomp).each { |file|
3187                                             total[entry2type(file)] += 1
3188                                         }
3189                                     rescue Errno::EACCES, Errno::ENOENT
3190                                     end
3191                                 end
3192                             }
3193                             gtk_thread_protect { src_nb.set_markup(utf8(_("<span size='small'><i>%s images and %s videos</i></span>") % [ total['image'], total['video'] ])) }
3194                             src_nb_thread = nil
3195                         }
3196                     else
3197                         src_nb.set_markup(utf8(_("<span size='small'><i>permission denied</i></span>")))
3198                     end
3199                 else
3200                     src_nb.set_markup(utf8(_("<span size='small'><i>N/A</i></span>")))
3201                 end
3202             end
3203         end
3204         true
3205     }
3206     timeout_src_nb = Gtk.timeout_add(100) {
3207         process_src_nb.call
3208     }
3209
3210     src_browse.signal_connect('clicked') {
3211         fc = Gtk::FileChooserDialog.new(utf8(_("Select the directory of images/videos")),
3212                                         nil,
3213                                         Gtk::FileChooser::ACTION_SELECT_FOLDER,
3214                                         nil,
3215                                         [Gtk::Stock::OPEN, Gtk::Dialog::RESPONSE_ACCEPT], [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
3216         fc.transient_for = $main_window
3217         if fc.run == Gtk::Dialog::RESPONSE_ACCEPT
3218             src.text = utf8(fc.filename)
3219             process_src_nb.call
3220             conf.text = File.expand_path("~/.booh/#{File.basename(src.text)}")
3221         end
3222         fc.destroy
3223     }
3224
3225     dest_browse.signal_connect('clicked') {
3226         fc = Gtk::FileChooserDialog.new(utf8(_("Select a new directory where to put the web-album")),
3227                                         nil,
3228                                         Gtk::FileChooser::ACTION_CREATE_FOLDER,
3229                                         nil,
3230                                         [Gtk::Stock::OPEN, Gtk::Dialog::RESPONSE_ACCEPT], [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
3231         fc.transient_for = $main_window
3232         if fc.run == Gtk::Dialog::RESPONSE_ACCEPT
3233             dest.text = utf8(fc.filename)
3234         end
3235         fc.destroy
3236     }
3237
3238     conf_browse.signal_connect('clicked') {
3239         fc = Gtk::FileChooserDialog.new(utf8(_("Select a new file to store this album's properties")),
3240                                         nil,
3241                                         Gtk::FileChooser::ACTION_SAVE,
3242                                         nil,
3243                                         [Gtk::Stock::SAVE, Gtk::Dialog::RESPONSE_ACCEPT], [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
3244         fc.transient_for = $main_window
3245         fc.add_shortcut_folder(File.expand_path("~/.booh"))
3246         fc.set_current_folder(File.expand_path("~/.booh"))
3247         if fc.run == Gtk::Dialog::RESPONSE_ACCEPT
3248             conf.text = utf8(fc.filename)
3249         end
3250         fc.destroy
3251     }
3252
3253     theme_sizes = []
3254     nperrows = []
3255     recreate_theme_config = proc {
3256         theme_sizes.each { |e| sizes.remove(e[:widget]) }
3257         theme_sizes = []
3258         select_theme(theme_button.label, 'all', optimize432.active?, nil)
3259         $images_size.each { |s|
3260             sizes.add(cb = Gtk::CheckButton.new(sizename(s['name'], true)))
3261             if !s['optional']
3262                 cb.active = true
3263             end
3264             tooltips.set_tip(cb, utf8(s['description']), nil)
3265             theme_sizes << { :widget => cb, :value => s['name'] }
3266         }
3267         sizes.add(cb = Gtk::CheckButton.new(utf8(_('original'))))
3268         tooltips = Gtk::Tooltips.new
3269         tooltips.set_tip(cb, utf8(_("Include original image in web-album")), nil)
3270         theme_sizes << { :widget => cb, :value => 'original' }
3271         sizes.show_all
3272
3273         nperrows.each { |e| nperrowradios.remove(e[:widget]) }
3274         nperrow_group = nil
3275         nperrows = []
3276         $allowed_N_values.each { |n|
3277             if nperrow_group
3278                 nperrowradios.add(rb = Gtk::RadioButton.new(nperrow_group, n.to_s, false))
3279             else
3280                 nperrowradios.add(nperrow_group = rb = Gtk::RadioButton.new(n.to_s))
3281             end
3282             tooltips.set_tip(rb, utf8(_("Set the number of thumbnails per row of the 'thumbnails' pages (if chosen theme uses a row arrangement)")), nil)
3283             if $default_N == n
3284                 rb.active = true
3285             end
3286             nperrows << { :widget => rb, :value => n }
3287         }
3288         nperrowradios.show_all
3289     }
3290     recreate_theme_config.call
3291
3292     theme_button.signal_connect('clicked') {
3293         if newtheme = theme_choose(theme_button.label)
3294             theme_button.label = newtheme
3295             recreate_theme_config.call
3296         end
3297     }
3298
3299     dialog.vbox.add(frame1)
3300     dialog.vbox.add(frame2)
3301     dialog.show_all
3302
3303     keepon = true
3304     ok = true
3305     while keepon
3306         dialog.run { |response|
3307             if response == Gtk::Dialog::RESPONSE_OK
3308                 srcdir = from_utf8_safe(src.text)
3309                 destdir = from_utf8_safe(dest.text)
3310                 confpath = from_utf8_safe(conf.text)
3311                 if src.text != '' && srcdir == ''
3312                     show_popup(dialog, utf8(_("The directory of images/videos is invalid. Please check your input.")))
3313                     src.grab_focus
3314                 elsif !File.directory?(srcdir)
3315                     show_popup(dialog, utf8(_("The directory of images/videos doesn't exist. Please check your input.")))
3316                     src.grab_focus
3317                 elsif dest.text != '' && destdir == ''
3318                     show_popup(dialog, utf8(_("The destination directory is invalid. Please check your input.")))
3319                     dest.grab_focus
3320                 elsif destdir != make_dest_filename(destdir)
3321                     show_popup(dialog, utf8(_("Sorry, destination directory can't contain non simple alphanumeric characters.")))
3322                     dest.grab_focus
3323                 elsif File.directory?(destdir) && Dir.entries(destdir).size > 2
3324                     keepon = !show_popup(dialog, utf8(_("The destination directory already exists. All existing files and directories
3325 inside it will be permanently removed before creating the web-album!
3326 Are you sure you want to continue?")), { :okcancel => true })
3327                     dest.grab_focus
3328                 elsif File.exists?(destdir) && !File.directory?(destdir)
3329                     show_popup(dialog, utf8(_("There is already a file by the name of the destination directory. Please choose another one.")))
3330                     dest.grab_focus
3331                 elsif conf.text == ''
3332                     show_popup(dialog, utf8(_("Please specify a filename to store the album's properties.")))
3333                     conf.grab_focus
3334                 elsif conf.text != '' && confpath == ''
3335                     show_popup(dialog, utf8(_("The filename to store the album's properties is invalid. Please check your input.")))
3336                     conf.grab_focus
3337                 elsif File.directory?(confpath)
3338                     show_popup(dialog, utf8(_("Sorry, the filename specified to store the album's properties is an existing directory. Please choose another one.")))
3339                     conf.grab_focus
3340                 elsif !theme_sizes.detect { |e| e[:value] != 'original' && e[:widget].active? }
3341                     show_popup(dialog, utf8(_("You need to select at least one size (not counting original).")))
3342                 else
3343                     system("mkdir '#{destdir}'")
3344                     if !File.directory?(destdir)
3345                         show_popup(dialog, utf8(_("Could not create destination directory. Permission denied?")))
3346                         dest.grab_focus
3347                     else
3348                         keepon = false
3349                     end
3350                 end
3351             else
3352                 keepon = ok = false
3353             end
3354         }
3355     end
3356     if ok
3357         srcdir = from_utf8(src.text)
3358         destdir = from_utf8(dest.text)
3359         configskel = File.expand_path(from_utf8(conf.text))
3360         theme = theme_button.label
3361         #- some sort of automatic theme preference
3362         $config['default-theme'] = theme
3363         sizes = theme_sizes.find_all { |e| e[:widget].active? }.collect { |e| e[:value] }.join(',')
3364         nperrow = nperrows.find { |e| e[:widget].active? }[:value]
3365         nperpage = nperpage_model.get_value(nperpagecombo.active_iter, 1)
3366         opt432 = optimize432.active?
3367         madewith = madewithentry.text.gsub('\'', '&#39;')  #- because the parameters to booh-backend are between apostrophes
3368         indexlink = indexlinkentry.text.gsub('\'', '&#39;')
3369     end
3370     if src_nb_thread
3371         Thread.kill(src_nb_thread)
3372         gtk_thread_flush  #- needed because we're about to destroy widgets in dialog, for which they may be some pending gtk calls
3373     end
3374     dialog.destroy
3375     Gtk.timeout_remove(timeout_src_nb)
3376
3377     if ok
3378         call_backend("booh-backend --source '#{srcdir}' --destination '#{destdir}' --config-skel '#{configskel}' --for-gui " +
3379                      "--verbose-level #{$verbose_level} --theme #{theme} --sizes #{sizes} --thumbnails-per-row #{nperrow} " +
3380                      (nperpage ? "--thumbnails-per-page #{nperpage} " : '') +
3381                      (multilanguages_value ? "--multi-languages #{multilanguages_value} " : '') +
3382                      "#{opt432 ? '--optimize-for-32' : ''} --made-with '#{madewith}' --index-link '#{indexlink}' #{additional_booh_options}",
3383                      utf8(_("Please wait while scanning source directory...")),
3384                      'full scan',
3385                      { :closure_after => proc { open_file_user(configskel) } })
3386     end
3387 end
3388
3389 def properties
3390     dialog = Gtk::Dialog.new(utf8(_("Properties of your album")),
3391                              $main_window,
3392                              Gtk::Dialog::MODAL | Gtk::Dialog::DESTROY_WITH_PARENT,
3393                              [Gtk::Stock::OK, Gtk::Dialog::RESPONSE_OK],
3394                              [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
3395     
3396     source = $xmldoc.root.attributes['source']
3397     dest = $xmldoc.root.attributes['destination']
3398     theme = $xmldoc.root.attributes['theme']
3399     opt432 = !$xmldoc.root.attributes['optimize-for-32'].nil?
3400     nperrow = $xmldoc.root.attributes['thumbnails-per-row']
3401     nperpage = $xmldoc.root.attributes['thumbnails-per-page']
3402     limit_sizes = $xmldoc.root.attributes['limit-sizes']
3403     if limit_sizes
3404         limit_sizes = limit_sizes.split(/,/)
3405     end
3406     madewith = ($xmldoc.root.attributes['made-with'] || '').gsub('&#39;', '\'')
3407     indexlink = ($xmldoc.root.attributes['index-link'] || '').gsub('&#39;', '\'')
3408     save_multilanguages_value = multilanguages_value = $xmldoc.root.attributes['multi-languages']
3409
3410     tooltips = Gtk::Tooltips.new
3411     frame1 = Gtk::Frame.new(utf8(_("Locations"))).add(tbl = Gtk::Table.new(0, 0, false))
3412     tbl.attach(Gtk::Label.new(utf8(_("Directory of source images/videos: "))),
3413                0, 1, 0, 1, Gtk::SHRINK, Gtk::SHRINK, 2, 2)
3414     tbl.attach(Gtk::Alignment.new(0, 0.5, 0, 0).add(Gtk::Label.new.set_markup('<i>' + source + '</i>')),
3415                1, 2, 0, 1, Gtk::FILL, Gtk::SHRINK, 2, 2)
3416     tbl.attach(Gtk::Label.new(utf8(_("Directory where the web-album is created: "))),
3417                0, 1, 2, 3, Gtk::SHRINK, Gtk::SHRINK, 2, 2)
3418     tbl.attach(Gtk::Alignment.new(0, 0.5, 0, 0).add(Gtk::Label.new.set_markup('<i>' + dest + '</i>').set_selectable(true)),
3419                1, 2, 2, 3, Gtk::FILL, Gtk::SHRINK, 2, 2)
3420     tbl.attach(Gtk::Label.new(utf8(_("Filename where this album's properties are stored: "))),
3421                0, 1, 3, 4, Gtk::SHRINK, Gtk::SHRINK, 2, 2)
3422     tbl.attach(Gtk::Alignment.new(0, 0.5, 0, 0).add(Gtk::Label.new.set_markup('<i>' + $orig_filename + '</i>').set_selectable(true)),
3423                1, 2, 3, 4, Gtk::FILL, Gtk::SHRINK, 2, 2)
3424
3425     frame2 = Gtk::Frame.new(utf8(_("Configuration"))).add(vb = Gtk::VBox.new)
3426     vb.add(Gtk::HBox.new(false, 3).pack_start(Gtk::Label.new(utf8(_("Theme: "))), false, false, 0).
3427                                    pack_start(theme_button = Gtk::Button.new(theme), false, false, 0))
3428     vb.add(Gtk::HBox.new(false, 3).pack_start(Gtk::Label.new(utf8(_("Sizes of images to generate: "))), false, false, 0).
3429                                    pack_start(sizes = Gtk::HBox.new, false, false, 0))
3430     vb.add(optimize432 = Gtk::CheckButton.new(utf8(_("Optimize for 3/2 aspect ratio"))).set_active(opt432))
3431     tooltips.set_tip(optimize432, utf8(_("Resize images with optimized sizes for 3/2 aspect ratio rather than 4/3 (typical aspect ratio of pictures from non digital cameras are 3/2 when pictures from digital cameras are 4/3)")), nil)
3432     vb.add(Gtk::HBox.new(false, 3).pack_start(Gtk::Label.new(utf8(_("Number of thumbnails per row: "))), false, false, 0).
3433                                    pack_start(nperrowradios = Gtk::HBox.new, false, false, 0))
3434     nperpage_model = Gtk::ListStore.new(String, String)
3435     vb.add(Gtk::HBox.new(false, 3).pack_start(Gtk::Label.new(utf8(_("Number of thumbnails per page: "))), false, false, 0).
3436                                    pack_start(nperpagecombo = Gtk::ComboBox.new(nperpage_model), false, false, 0))
3437     nperpagecombo.pack_start(crt = Gtk::CellRendererText.new, false)
3438     nperpagecombo.set_attributes(crt, { :markup => 0 })
3439     iter = nperpage_model.append
3440     iter[0] = utf8(_("<i>None - all thumbnails in one page</i>"))
3441     iter[1] = nil
3442     [ 12, 20, 30, 40, 50 ].each { |v|
3443         iter = nperpage_model.append
3444         iter[0] = iter[1] = v.to_s
3445         if nperpage && nperpage == v.to_s
3446             nperpagecombo.active_iter = iter
3447         end
3448     }
3449     if nperpagecombo.active_iter.nil?
3450         nperpagecombo.active = 0
3451     end
3452
3453     vb.add(ml = Gtk::HBox.new(false, 3).pack_start(ml_label = Gtk::Label.new, false, false, 0).
3454                                         pack_start(multilanguages = Gtk::Button.new(utf8(_("Configure multi-languages"))), false, false, 0))
3455     tooltips.set_tip(ml, utf8(_("When disabled, the web-album will be generated with navigation in your desktop language. When enabled, the web-album will be generated with navigation in all languages you select, but you have to publish your web-album on an Apache web-server for that feature to work.")), nil)
3456     ml_update = proc {
3457         if save_multilanguages_value
3458             ml_label.text = utf8(_("Multi-languages: enabled."))
3459         else
3460             ml_label.text = utf8(_("Multi-languages: disabled."))
3461         end
3462     }
3463     ml_update.call
3464     multilanguages.signal_connect('clicked') {
3465         retval = ask_multi_languages(save_multilanguages_value)
3466         if retval[0] 
3467             save_multilanguages_value = retval[1]
3468         end
3469         ml_update.call
3470     }
3471
3472     vb.add(Gtk::HBox.new(false, 3).pack_start(Gtk::Label.new(utf8(_("'Return to your website' link on pages bottom: "))), false, false, 0).
3473                                    pack_start(indexlinkentry = Gtk::Entry.new, true, true, 0))
3474     if indexlink
3475         indexlinkentry.text = indexlink
3476     end
3477     tooltips.set_tip(indexlinkentry, utf8(_("Optional HTML markup to use on pages bottom for a small link returning to wherever you see fit in your website (or somewhere else)")), nil)
3478     vb.add(Gtk::HBox.new(false, 3).pack_start(Gtk::Label.new(utf8(_("'Made with' markup on pages bottom: "))), false, false, 0).
3479                                    pack_start(madewithentry = Gtk::Entry.new, true, true, 0))
3480     if madewith
3481         madewithentry.text = madewith
3482     end
3483     tooltips.set_tip(madewithentry, utf8(_('Optional HTML markup to use on pages bottom for a small \'made with\' label; %booh is replaced by the website of booh;\nfor example: made with <a href=%booh>booh</a>!')), nil)
3484
3485     theme_sizes = []
3486     nperrows = []
3487     recreate_theme_config = proc {
3488         theme_sizes.each { |e| sizes.remove(e[:widget]) }
3489         theme_sizes = []
3490         select_theme(theme_button.label, 'all', optimize432.active?, nperrow)
3491
3492         $images_size.each { |s|
3493             sizes.add(cb = Gtk::CheckButton.new(sizename(s['name'], true)))
3494             if limit_sizes
3495                 if limit_sizes.include?(s['name'])
3496                     cb.active = true
3497                 end
3498             else
3499                 if !s['optional']
3500                     cb.active = true
3501                 end
3502             end
3503             tooltips.set_tip(cb, utf8(s['description']), nil)
3504             theme_sizes << { :widget => cb, :value => s['name'] }
3505         }
3506         sizes.add(cb = Gtk::CheckButton.new(utf8(_('original'))))
3507         tooltips = Gtk::Tooltips.new
3508         tooltips.set_tip(cb, utf8(_("Include original image in web-album")), nil)
3509         if limit_sizes && limit_sizes.include?('original')
3510             cb.active = true
3511         end
3512  &nbs