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