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