fix not emptying $todelete enough (on save file for example); do not abort on deletio...
[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         begin
1895             File.delete(f)
1896         rescue
1897             puts "Failed to delete #{f}: #{$!}"
1898         end
1899     }
1900     $todelete = []
1901 end
1902
1903 def mark_document_as_dirty
1904     $xmldoc.elements.each('//dir') { |elem|
1905         elem.delete_attribute('already-generated')
1906     }
1907 end
1908
1909 #- ret: true => ok  false => cancel
1910 def ask_save_modifications(msg1, msg2, *options)
1911     ret = true
1912     options = options.size > 0 ? options[0] : {}
1913     if $modified
1914         if options[:disallow_cancel]
1915             dialog = Gtk::Dialog.new(msg1,
1916                                      $main_window,
1917                                      Gtk::Dialog::MODAL | Gtk::Dialog::DESTROY_WITH_PARENT,
1918                                      [options[:no] || Gtk::Stock::CLOSE, Gtk::Dialog::RESPONSE_NO],
1919                                      [options[:yes] || Gtk::Stock::SAVE, Gtk::Dialog::RESPONSE_YES])
1920         else
1921             dialog = Gtk::Dialog.new(msg1,
1922                                      $main_window,
1923                                      Gtk::Dialog::MODAL | Gtk::Dialog::DESTROY_WITH_PARENT,
1924                                      [options[:cancel] || Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL],
1925                                      [options[:no] || Gtk::Stock::CLOSE, Gtk::Dialog::RESPONSE_NO],
1926                                      [options[:yes] || Gtk::Stock::SAVE, Gtk::Dialog::RESPONSE_YES])
1927         end
1928         dialog.default_response = Gtk::Dialog::RESPONSE_YES
1929         dialog.vbox.add(Gtk::Label.new(msg2))
1930         dialog.window_position = Gtk::Window::POS_CENTER
1931         dialog.show_all
1932         
1933         dialog.run { |response|
1934             dialog.destroy
1935             if response == Gtk::Dialog::RESPONSE_YES
1936                 if ! save_current_file_user
1937                     return ask_save_modifications(msg1, msg2, options)
1938                 end
1939             else
1940                 #- if we have generated an album but won't save modifications, we must remove 
1941                 #- already-generated markers in original file
1942                 if $generated_outofline
1943                     begin
1944                         $xmldoc = REXML::Document.new(File.new($orig_filename))
1945                         mark_document_as_dirty
1946                         ios = File.open($orig_filename, "w")
1947                         $xmldoc.write(ios)
1948                         ios.close
1949                     rescue Exception
1950                         puts "exception: #{$!}"
1951                     end
1952                 end
1953             end
1954             if response == Gtk::Dialog::RESPONSE_CANCEL
1955                 ret = false
1956             end
1957         }
1958     end
1959     return ret
1960 end
1961
1962 def try_quit(*options)
1963     if ask_save_modifications(utf8(_("Save before quitting?")),
1964                               utf8(_("Do you want to save your changes before quitting?")),
1965                               *options)
1966         Gtk.main_quit
1967     end
1968 end
1969
1970 def show_popup(parent, msg, *options)
1971     dialog = Gtk::Dialog.new
1972     if options[0] && options[0][:title]
1973         dialog.title = options[0][:title]
1974     else
1975         dialog.title = utf8(_("Booh message"))
1976     end
1977     lbl = Gtk::Label.new
1978     if options[0] && options[0][:nomarkup]
1979         lbl.text = msg
1980     else
1981         lbl.markup = msg
1982     end
1983     if options[0] && options[0][:centered]
1984         lbl.set_justify(Gtk::Justification::CENTER)
1985     end
1986     if options[0] && options[0][:selectable]
1987         lbl.selectable = true
1988     end
1989     if options[0] && options[0][:topwidget]
1990         dialog.vbox.add(options[0][:topwidget])
1991     end
1992     if options[0] && options[0][:scrolled]
1993         sw = Gtk::ScrolledWindow.new(nil, nil)
1994         sw.set_policy(Gtk::POLICY_AUTOMATIC, Gtk::POLICY_AUTOMATIC)
1995         sw.add_with_viewport(lbl)
1996         dialog.vbox.add(sw)
1997         dialog.set_default_size(500, 600)
1998     else
1999         dialog.vbox.add(lbl)
2000         dialog.set_default_size(200, 120)
2001     end
2002     if options[0] && options[0][:okcancel]
2003         dialog.add_button(Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL)
2004     end
2005     dialog.add_button(Gtk::Stock::OK, Gtk::Dialog::RESPONSE_OK)
2006
2007     if options[0] && options[0][:pos_centered]
2008         dialog.window_position = Gtk::Window::POS_CENTER
2009     else
2010         dialog.window_position = Gtk::Window::POS_MOUSE
2011     end
2012
2013     if options[0] && options[0][:linkurl]
2014         linkbut = Gtk::Button.new('')
2015         linkbut.child.markup = "<span foreground=\"#00000000FFFF\" underline=\"single\">#{options[0][:linkurl]}</span>"
2016         linkbut.signal_connect('clicked') {
2017             open_url(options[0][:linkurl])
2018             dialog.response(Gtk::Dialog::RESPONSE_OK)
2019             set_mousecursor_normal
2020         }
2021         linkbut.relief = Gtk::RELIEF_NONE
2022         linkbut.signal_connect('enter-notify-event') { set_mousecursor(Gdk::Cursor::HAND2, linkbut); false }
2023         linkbut.signal_connect('leave-notify-event') { set_mousecursor(nil, linkbut); false }
2024         dialog.vbox.add(Gtk::Alignment.new(0.5, 0.5, 0, 0).add(linkbut))
2025     end
2026
2027     dialog.show_all
2028
2029     if !options[0] || !options[0][:not_transient]
2030         dialog.transient_for = parent
2031         dialog.run { |response|
2032             dialog.destroy
2033             if options[0] && options[0][:okcancel]
2034                 return response == Gtk::Dialog::RESPONSE_OK
2035             end
2036         }
2037     else
2038         dialog.signal_connect('response') { dialog.destroy }
2039     end
2040 end
2041
2042 def set_mainwindow_title(progress)
2043     filename = $orig_filename || $filename
2044     if progress
2045         if filename
2046             $main_window.title = 'booh [' + (progress * 100).to_i.to_s + '%] - ' + File.basename(filename)
2047         else
2048             $main_window.title = 'booh [' + (progress * 100).to_i.to_s + '%] '
2049         end
2050     else
2051         if filename
2052             $main_window.title = 'booh - ' + File.basename(filename)
2053         else
2054             $main_window.title = 'booh'
2055         end
2056     end
2057 end
2058
2059 def backend_wait_message(parent, msg, infopipe_path, mode)
2060     w = create_window
2061     w.set_transient_for(parent)
2062     w.modal = true
2063
2064     vb = Gtk::VBox.new(false, 5).set_border_width(5)
2065     vb.pack_start(Gtk::Label.new(msg), false, false)
2066
2067     vb.pack_start(frame1 = Gtk::Frame.new(utf8(_("Thumbnails"))).add(vb1 = Gtk::VBox.new(false, 5)))
2068     vb1.pack_start(pb1_1 = Gtk::ProgressBar.new.set_text(utf8(_("Scanning photos and videos..."))), false, false)
2069     if mode != 'one dir scan'
2070         vb1.pack_start(pb1_2 = Gtk::ProgressBar.new.set_text(utf8(_("not started"))), false, false)
2071     end
2072     if mode == 'web-album'
2073         vb.pack_start(frame2 = Gtk::Frame.new(utf8(_("HTML pages"))).add(vb1 = Gtk::VBox.new(false, 5)))
2074         vb1.pack_start(pb2 = Gtk::ProgressBar.new.set_text(utf8(_("not started"))), false, false)
2075     end
2076     vb.pack_start(Gtk::HSeparator.new, false, false)
2077
2078     bottom = Gtk::Alignment.new(0.5, 0.5, 0, 0).add(b = Gtk::Button.new(utf8(_("_Abort"))))
2079     b.image = Gtk::Image.new("#{$FPATH}/images/stock-close-24.png")
2080     vb.pack_end(bottom, false, false)
2081
2082     directories = nil
2083     update_progression_title_pb1 = proc {
2084         if mode == 'web-album'
2085             set_mainwindow_title((pb1_2.fraction + pb1_1.fraction / directories) * 9 / 10)
2086         elsif mode != 'one dir scan'
2087             set_mainwindow_title(pb1_2.fraction + pb1_1.fraction / directories)
2088         else
2089             set_mainwindow_title(pb1_1.fraction)
2090         end
2091     }
2092
2093     infopipe = File.open(infopipe_path, File::RDONLY | File::NONBLOCK)
2094     refresh_thread = Thread.new {
2095         directories_counter = 0
2096         while line = infopipe.gets
2097             msg 3, "infopipe got data: #{line}"
2098             if line =~ /^directories: (\d+), sizes: (\d+)/
2099                 directories = $1.to_f + 1
2100                 sizes = $2.to_f
2101             elsif line =~ /^walking: (.+)\|(.+), (\d+) elements$/
2102                 elements = $3.to_f + 1
2103                 if mode == 'web-album'
2104                     elements += sizes
2105                 end
2106                 element_counter = 0
2107                 gtk_thread_protect { pb1_1.fraction = 0 }
2108                 if mode != 'one dir scan'
2109                     newtext = utf8(full_src_dir_to_rel($1, $2))
2110                     newtext = '/' if newtext == ''
2111                     gtk_thread_protect { pb1_2.text = newtext }
2112                     directories_counter += 1
2113                     gtk_thread_protect {
2114                         pb1_2.fraction = directories_counter / directories
2115                         update_progression_title_pb1.call
2116                     }
2117                 end
2118             elsif line =~ /^processing element$/
2119                 element_counter += 1
2120                 gtk_thread_protect {
2121                     pb1_1.fraction = element_counter / elements
2122                     update_progression_title_pb1.call
2123                 }
2124             elsif line =~ /^processing size$/
2125                 element_counter += 1
2126                 gtk_thread_protect {
2127                     pb1_1.fraction = element_counter / elements
2128                     update_progression_title_pb1.call
2129                 }
2130             elsif line =~ /^finished processing sizes$/
2131                 gtk_thread_protect { pb1_1.fraction = 1 }
2132             elsif line =~ /^creating index.html$/
2133                 gtk_thread_protect { pb1_2.text = utf8(_("finished")) }
2134                 gtk_thread_protect { pb1_1.fraction = pb1_2.fraction = 1 }
2135                 directories_counter = 0
2136             elsif line =~ /^index.html: (.+)\|(.+)/
2137                 newtext = utf8(full_src_dir_to_rel($1, $2))
2138                 newtext = '/' if newtext == ''
2139                 gtk_thread_protect { pb2.text = newtext }
2140                 directories_counter += 1
2141                 gtk_thread_protect {
2142                     pb2.fraction = directories_counter / directories
2143                     set_mainwindow_title(0.9 + pb2.fraction / 10)
2144                 }
2145             elsif line =~ /^die: (.*)$/
2146                 $diemsg = $1
2147             end
2148         end
2149     }
2150
2151     w.add(vb)
2152     w.signal_connect('delete-event') { w.destroy }
2153     w.signal_connect('destroy') {
2154         Thread.kill(refresh_thread)
2155         gtk_thread_flush  #- needed because we're about to destroy widgets in w, for which they may be some pending gtk calls
2156         if infopipe_path
2157             infopipe.close
2158             File.delete(infopipe_path)
2159         end
2160         set_mainwindow_title(nil)
2161     }
2162     w.window_position = Gtk::Window::POS_CENTER
2163     w.show_all
2164
2165     return [ b, w ]
2166 end
2167
2168 def call_backend(cmd, waitmsg, mode, params)
2169     pipe = Tempfile.new("boohpipe")
2170     Thread.critical = true
2171     path = pipe.path
2172     pipe.close!
2173     system("mkfifo #{path}")
2174     Thread.critical = false
2175     cmd += " --info-pipe #{path}"
2176     button, w8 = backend_wait_message($main_window, waitmsg, path, mode)
2177     pid = nil
2178     Thread.new {
2179         msg 2, cmd
2180         if pid = fork
2181             id, exitstatus = Process.waitpid2(pid)
2182             gtk_thread_protect { w8.destroy }
2183             if exitstatus == 0
2184                 if params[:successmsg]
2185                     gtk_thread_protect { show_popup($main_window, params[:successmsg], { :linkurl => params[:successmsg_linkurl] }) }
2186                 end
2187                 if params[:closure_after]
2188                     gtk_thread_protect(&params[:closure_after])
2189                 end
2190             elsif exitstatus == 15
2191                 #- say nothing, user aborted
2192             else
2193                 gtk_thread_protect { show_popup($main_window,
2194                                                 utf8(_("There was something wrong, sorry:\n\n%s") % $diemsg)) }
2195             end
2196         else
2197             exec(cmd)
2198         end
2199     }
2200     button.signal_connect('clicked') {
2201         Process.kill('SIGTERM', pid)
2202     }
2203 end
2204
2205 def save_changes(*forced)
2206     if forced.empty? && (!$current_path || !$undo_tb.sensitive?)
2207         return
2208     end
2209
2210     $xmldir.delete_attribute('already-generated')
2211
2212     propagate_children = proc { |xmldir|
2213         if xmldir.attributes['subdirs-caption']
2214             xmldir.delete_attribute('already-generated')
2215         end
2216         xmldir.elements.each('dir') { |element|
2217             propagate_children.call(element)
2218         }
2219     }
2220
2221     if $xmldir.child_byname_notattr('dir', 'deleted')
2222         new_title = $subalbums_title.buffer.text
2223         if new_title != $xmldir.attributes['subdirs-caption']
2224             parent = $xmldir.parent
2225             if parent.name == 'dir'
2226                 parent.delete_attribute('already-generated')
2227             end
2228             propagate_children.call($xmldir)
2229         end
2230         $xmldir.add_attribute('subdirs-caption', new_title)
2231         $xmldir.elements.each('dir') { |element|
2232             if !element.attributes['deleted']
2233                 path = element.attributes['path']
2234                 newtext = $subalbums_edits[path][:editzone].buffer.text
2235                 if element.attributes['subdirs-caption']
2236                     if element.attributes['subdirs-caption'] != newtext
2237                         propagate_children.call(element)
2238                     end
2239                     element.add_attribute('subdirs-caption',     newtext)
2240                     element.add_attribute('subdirs-captionfile', utf8($subalbums_edits[path][:captionfile]))
2241                 else
2242                     if element.attributes['thumbnails-caption'] != newtext
2243                         element.delete_attribute('already-generated')
2244                     end
2245                     element.add_attribute('thumbnails-caption',     newtext)
2246                     element.add_attribute('thumbnails-captionfile', utf8($subalbums_edits[path][:captionfile]))
2247                 end
2248             end
2249         }
2250     end
2251
2252     if $notebook.page == 0 && $xmldir.child_byname_notattr('dir', 'deleted')
2253         if $xmldir.attributes['thumbnails-caption']
2254             path = $xmldir.attributes['path']
2255             $xmldir.add_attribute('thumbnails-caption', $subalbums_edits[path][:editzone].buffer.text)
2256         end
2257     elsif $xmldir.attributes['thumbnails-caption']
2258         $xmldir.add_attribute('thumbnails-caption', $thumbnails_title.buffer.text)
2259     end
2260
2261     if $xmldir.attributes['thumbnails-caption']
2262         if edit = $subalbums_edits[$xmldir.attributes['path']]
2263             $xmldir.add_attribute('thumbnails-captionfile', edit[:captionfile])
2264         end
2265     end
2266
2267     #- remove and reinsert elements to reflect new ordering
2268     saves = {}
2269     cpt = 0
2270     $xmldir.elements.each { |element|
2271         if element.name == 'image' || element.name == 'video'
2272             saves[element.attributes['filename']] = element.remove
2273             cpt += 1
2274         end
2275     }
2276     $autotable.current_order.each { |path|
2277         chld = $xmldir.add_element(saves[path].name, saves[path].attributes)
2278         chld.add_attribute('caption', $name2widgets[File.basename(path)][:textview].buffer.text)
2279         saves.delete(path)
2280     }
2281     saves.each_key { |path|
2282         chld = $xmldir.add_element(saves[path].name, saves[path].attributes)
2283         chld.add_attribute('deleted', 'true')
2284     }
2285 end
2286
2287 def sort_by_exif_date
2288     $modified = true
2289     save_changes
2290     current_order = []
2291     rexml_thread_protect {
2292         $xmldir.elements.each { |element|
2293             if element.name == 'image' || element.name == 'video'
2294                 current_order << element.attributes['filename']
2295             end
2296         }
2297     }
2298
2299     #- look for EXIF dates
2300     dates = {}
2301
2302     if current_order.size > 20
2303         w = create_window
2304         w.set_transient_for($main_window)
2305         w.modal = true
2306         vb = Gtk::VBox.new(false, 5).set_border_width(5)
2307         vb.pack_start(Gtk::Label.new(utf8(_("Scanning sub-album looking for EXIF dates..."))), false, false)
2308         vb.pack_start(pb = Gtk::ProgressBar.new, false, false)
2309         bottom = Gtk::Alignment.new(0.5, 0.5, 0, 0).add(b = Gtk::Button.new(utf8(_("_Abort"))))
2310         b.image = Gtk::Image.new("#{$FPATH}/images/stock-close-24.png")
2311         vb.pack_end(bottom, false, false)
2312         w.add(vb)
2313         w.signal_connect('delete-event') { w.destroy }
2314         w.window_position = Gtk::Window::POS_CENTER
2315         w.show_all
2316
2317         aborted = false
2318         b.signal_connect('clicked') { aborted = true }
2319         i = 0
2320         current_order.each { |f|
2321             i += 1
2322             if entry2type(f) == 'image'
2323                 pb.text = f
2324                 pb.fraction = i.to_f / current_order.size
2325                 Gtk.main_iteration while Gtk.events_pending?
2326                 date_time = Exif.datetimeoriginal(from_utf8($current_path + "/" + f))
2327                 if ! date_time.nil?
2328                     dates[f] = date_time
2329                 end
2330             end
2331             if aborted
2332                 break
2333             end
2334         }
2335         w.destroy
2336         if aborted
2337             return
2338         end
2339
2340     else
2341         current_order.each { |f|
2342             date_time = Exif.datetimeoriginal(from_utf8($current_path + "/" + f))
2343             if ! date_time.nil?
2344                 dates[f] = date_time
2345             end
2346         }
2347     end
2348
2349     saves = {}
2350     rexml_thread_protect {
2351         $xmldir.elements.each { |element|
2352             if element.name == 'image' || element.name == 'video'
2353                 saves[element.attributes['filename']] = element.remove
2354             end
2355         }
2356     }
2357
2358     neworder = smartsort(current_order, dates)
2359
2360     rexml_thread_protect {
2361         neworder.each { |f|
2362             $xmldir.add_element(saves[f].name, saves[f].attributes)
2363         }
2364     }
2365
2366     #- let the auto-table reflect new ordering
2367     change_dir
2368 end
2369
2370 def remove_all_captions
2371     $modified = true
2372     texts = {}
2373     $autotable.current_order.each { |path|
2374         texts[File.basename(path) ] = $name2widgets[File.basename(path)][:textview].buffer.text
2375         $name2widgets[File.basename(path)][:textview].buffer.text = ''
2376     }
2377     save_undo(_("remove all captions"),
2378               proc { |texts|
2379                   texts.each_key { |key|
2380                       $name2widgets[key][:textview].buffer.text = texts[key]
2381                   }
2382                   $notebook.set_page(1)
2383                   proc {
2384                       texts.each_key { |key|
2385                           $name2widgets[key][:textview].buffer.text = ''
2386                       }
2387                       $notebook.set_page(1)
2388                   }
2389               }, texts)
2390 end
2391
2392 def change_dir
2393     $selected_elements.each_key { |path|
2394         $name2widgets[path][:img].pixbuf = $selected_elements[path][:pixbuf]
2395     }
2396     $autotable.clear
2397     $vbox2widgets = {}
2398     $name2widgets = {}
2399     $name2closures = {}
2400     $selected_elements = {}
2401     $cuts = []
2402     $multiple_dnd = []
2403     UndoHandler.cleanup
2404     $undo_tb.sensitive = $undo_mb.sensitive = false
2405     $redo_tb.sensitive = $redo_mb.sensitive = false
2406
2407     if !$current_path
2408         return
2409     end
2410
2411     $subalbums_vb.children.each { |chld|
2412         $subalbums_vb.remove(chld)
2413     }
2414     $subalbums = Gtk::Table.new(0, 0, true)
2415     current_y_sub_albums = 0
2416
2417     $xmldir = $xmldoc.elements["//dir[@path='#{$current_path}']"]
2418     $subalbums_edits = {}
2419     subalbums_counter = 0
2420     subalbums_edits_bypos = {}
2421
2422     add_subalbum = proc { |xmldir, counter|
2423         $subalbums_edits[xmldir.attributes['path']] = { :position => counter }
2424         subalbums_edits_bypos[counter] = $subalbums_edits[xmldir.attributes['path']]
2425         if xmldir == $xmldir
2426             thumbnail_file = "#{current_dest_dir}/thumbnails-thumbnail.jpg"
2427             captionfile = from_utf8(xmldir.attributes['thumbnails-captionfile'])
2428             caption = xmldir.attributes['thumbnails-caption']
2429             infotype = 'thumbnails'
2430         else
2431             thumbnail_file = "#{current_dest_dir}/thumbnails-#{make_dest_filename(from_utf8(File.basename(xmldir.attributes['path'])))}.jpg"
2432             captionfile, caption = find_subalbum_caption_info(xmldir)
2433             infotype = find_subalbum_info_type(xmldir)
2434         end
2435         msg 3, "add subdir: #{xmldir.attributes['path']} with file: #{thumbnail_file}"
2436         hbox = Gtk::HBox.new
2437         hbox.pack_start(Gtk::Alignment.new(1, 0.5, 0, 0).add(Gtk::Label.new.set_markup('<i>' + File.basename(xmldir.attributes['path']) + ':</i>')))
2438         f = Gtk::Frame.new
2439         f.set_shadow_type(Gtk::SHADOW_ETCHED_OUT)
2440
2441         img = nil
2442         my_gen_real_thumbnail = proc {
2443             gen_real_thumbnail('subdir', captionfile, thumbnail_file, xmldir, $albums_thumbnail_size, img, infotype)
2444         }
2445
2446         if !$modified_pixbufs[thumbnail_file] && !File.exists?(thumbnail_file)
2447             f.add(img = Gtk::Image.new)
2448             my_gen_real_thumbnail.call
2449         else
2450             f.add(img = Gtk::Image.new($modified_pixbufs[thumbnail_file] ? $modified_pixbufs[thumbnail_file][:pixbuf] : thumbnail_file))
2451         end
2452         hbox.pack_end(Gtk::Alignment.new(0.5, 0.5, 0, 0).add(evtbox = Gtk::EventBox.new.add(f)), false, false)
2453         $subalbums.attach(hbox,
2454                           0, 1, current_y_sub_albums, current_y_sub_albums + 1, Gtk::FILL, Gtk::FILL, 2, 2)
2455
2456         frame, textview = create_editzone($subalbums_sw, 0, img)
2457         textview.buffer.text = caption
2458         $subalbums.attach(Gtk::Alignment.new(0, 0.5, 0.5, 0).add(frame),
2459                           1, 2, current_y_sub_albums, current_y_sub_albums + 1, Gtk::FILL, Gtk::FILL, 2, 2)
2460
2461         change_image = proc {
2462             fc = Gtk::FileChooserDialog.new(utf8(_("Select image for caption")),
2463                                             nil,
2464                                             Gtk::FileChooser::ACTION_OPEN,
2465                                             nil,
2466                                             [Gtk::Stock::OK, Gtk::Dialog::RESPONSE_ACCEPT], [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
2467             fc.set_current_folder(from_utf8(xmldir.attributes['path']))
2468             fc.transient_for = $main_window
2469             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))
2470             f.add(preview_img = Gtk::Image.new)
2471             preview.show_all
2472             fc.signal_connect('update-preview') { |w|
2473                 if fc.preview_filename
2474                     if entry2type(fc.preview_filename) == 'video'
2475                         image_path = nil
2476                         tmpdir = nil
2477                         begin
2478                             tmpdir = gen_video_thumbnail(fc.preview_filename, false, 0)
2479                             if tmpdir.nil?
2480                                 fc.preview_widget_active = false
2481                             else
2482                                 tmpimage = "#{tmpdir}/00000001.jpg"
2483                                 begin
2484                                     preview_img.pixbuf = Gdk::Pixbuf.new(tmpimage, 240, 180)
2485                                     fc.preview_widget_active = true
2486                                 rescue Gdk::PixbufError
2487                                     fc.preview_widget_active = false
2488                                 ensure
2489                                     File.delete(tmpimage)
2490                                     Dir.rmdir(tmpdir)
2491                                 end
2492                             end
2493                         end
2494                     else
2495                         begin
2496                             preview_img.pixbuf = rotate_pixbuf(Gdk::Pixbuf.new(fc.preview_filename, 240, 180), guess_rotate(fc.preview_filename))
2497                             fc.preview_widget_active = true
2498                         rescue Gdk::PixbufError
2499                             fc.preview_widget_active = false
2500                         end
2501                     end
2502                 end
2503             }
2504             if fc.run == Gtk::Dialog::RESPONSE_ACCEPT
2505                 $modified = true
2506                 old_file = captionfile
2507                 old_rotate = xmldir.attributes["#{infotype}-rotate"]
2508                 old_color_swap = xmldir.attributes["#{infotype}-color-swap"]
2509                 old_enhance = xmldir.attributes["#{infotype}-enhance"]
2510                 old_seektime = xmldir.attributes["#{infotype}-seektime"]
2511
2512                 new_file = fc.filename
2513                 msg 3, "new captionfile is: #{fc.filename}"
2514                 perform_changefile = proc {
2515                     $subalbums_edits[xmldir.attributes['path']][:captionfile] = captionfile = new_file
2516                     $modified_pixbufs.delete(thumbnail_file)
2517                     xmldir.delete_attribute("#{infotype}-rotate")
2518                     xmldir.delete_attribute("#{infotype}-color-swap")
2519                     xmldir.delete_attribute("#{infotype}-enhance")
2520                     xmldir.delete_attribute("#{infotype}-seektime")
2521                     my_gen_real_thumbnail.call
2522                 }
2523                 perform_changefile.call
2524
2525                 save_undo(_("change caption file for sub-album"),
2526                           proc {
2527                               $subalbums_edits[xmldir.attributes['path']][:captionfile] = captionfile = old_file
2528                               xmldir.add_attribute("#{infotype}-rotate", old_rotate)
2529                               xmldir.add_attribute("#{infotype}-color-swap", old_color_swap)
2530                               xmldir.add_attribute("#{infotype}-enhance", old_enhance)
2531                               xmldir.add_attribute("#{infotype}-seektime", old_seektime)
2532                               my_gen_real_thumbnail.call
2533                               $notebook.set_page(0)
2534                               proc {
2535                                   perform_changefile.call
2536                                   $notebook.set_page(0)
2537                               }
2538                           })
2539             end
2540             fc.destroy
2541         }
2542
2543         refresh = proc {
2544             if File.exists?(thumbnail_file)
2545                 File.delete(thumbnail_file)
2546             end
2547             my_gen_real_thumbnail.call
2548         }
2549
2550         rotate_and_cleanup = proc { |angle|
2551             rotate(angle, thumbnail_file, img, xmldir, "#{infotype}-", $default_albums_thumbnails[:x], $default_albums_thumbnails[:y])
2552             if File.exists?(thumbnail_file)
2553                 File.delete(thumbnail_file)
2554             end
2555         }
2556
2557         move = proc { |direction|
2558             $modified = true
2559
2560             save_changes('forced')
2561             oldpos = $subalbums_edits[xmldir.attributes['path']][:position]
2562             if direction == 'up'
2563                 $subalbums_edits[xmldir.attributes['path']][:position] -= 1
2564                 subalbums_edits_bypos[oldpos - 1][:position] += 1
2565             end
2566             if direction == 'down'
2567                 $subalbums_edits[xmldir.attributes['path']][:position] += 1
2568                 subalbums_edits_bypos[oldpos + 1][:position] -= 1
2569             end
2570             if direction == 'top'
2571                 for i in 1 .. oldpos - 1
2572                     subalbums_edits_bypos[i][:position] += 1
2573                 end
2574                 $subalbums_edits[xmldir.attributes['path']][:position] = 1
2575             end
2576             if direction == 'bottom'
2577                 for i in oldpos + 1 .. subalbums_counter
2578                     subalbums_edits_bypos[i][:position] -= 1
2579                 end
2580                 $subalbums_edits[xmldir.attributes['path']][:position] = subalbums_counter
2581             end
2582
2583             elems = []
2584             $xmldir.elements.each('dir') { |element|
2585                 if (!element.attributes['deleted'])
2586                     elems << [ element.attributes['path'], element.remove ]
2587                 end
2588             }
2589             elems.sort { |a,b| $subalbums_edits[a[0]][:position] <=> $subalbums_edits[b[0]][:position] }.
2590                   each { |e| $xmldir.add_element(e[1]) }
2591             #- need to remove the already-generated tag to all subdirs because of "previous/next albums" links completely changed
2592             $xmldir.elements.each('descendant::dir') { |elem|
2593                 elem.delete_attribute('already-generated')
2594             }
2595
2596             sel = $albums_tv.selection.selected_rows
2597             change_dir
2598             populate_subalbums_treeview(false)
2599             $albums_tv.selection.select_path(sel[0])
2600         }
2601
2602         color_swap_and_cleanup = proc {
2603             perform_color_swap_and_cleanup = proc {
2604                 color_swap(xmldir, "#{infotype}-")
2605                 my_gen_real_thumbnail.call
2606             }
2607             perform_color_swap_and_cleanup.call
2608
2609             save_undo(_("color swap"),
2610                       proc {
2611                           perform_color_swap_and_cleanup.call
2612                           $notebook.set_page(0)
2613                           proc {
2614                               perform_color_swap_and_cleanup.call
2615                               $notebook.set_page(0)
2616                           }
2617                       })
2618         }
2619
2620         change_seektime_and_cleanup = proc {
2621             if values = ask_new_seektime(xmldir, "#{infotype}-")
2622                 perform_change_seektime_and_cleanup = proc { |val|
2623                     change_seektime(xmldir, "#{infotype}-", val)
2624                     my_gen_real_thumbnail.call
2625                 }
2626                 perform_change_seektime_and_cleanup.call(values[:new])
2627
2628                 save_undo(_("specify seektime"),
2629                           proc {
2630                               perform_change_seektime_and_cleanup.call(values[:old])
2631                               $notebook.set_page(0)
2632                               proc {
2633                                   perform_change_seektime_and_cleanup.call(values[:new])
2634                                   $notebook.set_page(0)
2635                               }
2636                           })
2637             end
2638         }
2639
2640         whitebalance_and_cleanup = proc {
2641             if values = ask_whitebalance(captionfile, thumbnail_file, img, xmldir, "#{infotype}-",
2642                                          $default_albums_thumbnails[:x], $default_albums_thumbnails[:y], infotype)
2643                 perform_change_whitebalance_and_cleanup = proc { |val|
2644                     change_whitebalance(xmldir, "#{infotype}-", val)
2645                     recalc_whitebalance(val, captionfile, thumbnail_file, img, xmldir, "#{infotype}-",
2646                                         $default_albums_thumbnails[:x], $default_albums_thumbnails[:y], infotype)
2647                     if File.exists?(thumbnail_file)
2648                         File.delete(thumbnail_file)
2649                     end
2650                 }
2651                 perform_change_whitebalance_and_cleanup.call(values[:new])
2652                 
2653                 save_undo(_("fix white balance"),
2654                           proc {
2655                               perform_change_whitebalance_and_cleanup.call(values[:old])
2656                               $notebook.set_page(0)
2657                               proc {
2658                                   perform_change_whitebalance_and_cleanup.call(values[:new])
2659                                   $notebook.set_page(0)
2660                               }
2661                           })
2662             end
2663         }
2664
2665         gammacorrect_and_cleanup = proc {
2666             if values = ask_gammacorrect(captionfile, thumbnail_file, img, xmldir, "#{infotype}-",
2667                                          $default_albums_thumbnails[:x], $default_albums_thumbnails[:y], infotype)
2668                 perform_change_gammacorrect_and_cleanup = proc { |val|
2669                     change_gammacorrect(xmldir, "#{infotype}-", val)
2670                     recalc_gammacorrect(val, captionfile, thumbnail_file, img, xmldir, "#{infotype}-",
2671                                         $default_albums_thumbnails[:x], $default_albums_thumbnails[:y], infotype)
2672                     if File.exists?(thumbnail_file)
2673                         File.delete(thumbnail_file)
2674                     end
2675                 }
2676                 perform_change_gammacorrect_and_cleanup.call(values[:new])
2677                 
2678                 save_undo(_("gamma correction"),
2679                           proc {
2680                               perform_change_gammacorrect_and_cleanup.call(values[:old])
2681                               $notebook.set_page(0)
2682                               proc {
2683                                   perform_change_gammacorrect_and_cleanup.call(values[:new])
2684                                   $notebook.set_page(0)
2685                               }
2686                           })
2687             end
2688         }
2689
2690         enhance_and_cleanup = proc {
2691             perform_enhance_and_cleanup = proc {
2692                 enhance(xmldir, "#{infotype}-")
2693                 my_gen_real_thumbnail.call
2694             }
2695             
2696             perform_enhance_and_cleanup.call
2697             
2698             save_undo(_("enhance"),
2699                       proc {
2700                           perform_enhance_and_cleanup.call
2701                           $notebook.set_page(0)
2702                           proc {
2703                               perform_enhance_and_cleanup.call
2704                               $notebook.set_page(0)
2705                           }
2706                       })
2707         }
2708
2709         evtbox.signal_connect('button-press-event') { |w, event|
2710             if event.event_type == Gdk::Event::BUTTON_PRESS && event.button == 1
2711                 if $r90.active?
2712                     rotate_and_cleanup.call(90)
2713                 elsif $r270.active?
2714                     rotate_and_cleanup.call(-90)
2715                 elsif $enhance.active?
2716                     enhance_and_cleanup.call
2717                 end
2718             end
2719             if event.event_type == Gdk::Event::BUTTON_PRESS && event.button == 3
2720                 popup_thumbnail_menu(event, ['change_image', 'move_top', 'move_bottom'], captionfile, entry2type(captionfile), xmldir, "#{infotype}-",
2721                                      { :forbid_left => true, :forbid_right => true,
2722                                        :can_up => counter > 1, :can_down => counter > 0 && counter < subalbums_counter,
2723                                        :can_top => counter > 1, :can_bottom => counter > 0 && counter < subalbums_counter },
2724                                      { :change => change_image, :move => move, :rotate => rotate_and_cleanup, :enhance => enhance_and_cleanup,
2725                                        :color_swap => color_swap_and_cleanup, :seektime => change_seektime_and_cleanup, :whitebalance => whitebalance_and_cleanup,
2726                                        :gammacorrect => gammacorrect_and_cleanup, :refresh => refresh })
2727             end
2728             if event.event_type == Gdk::Event::BUTTON2_PRESS && event.button == 1
2729                 change_image.call
2730                 true   #- handled
2731             end
2732         }
2733         evtbox.signal_connect('button-press-event') { |w, event|
2734             $gesture_press = { :filename => thumbnail_file, :x => event.x, :y => event.y }
2735             false
2736         }
2737
2738         evtbox.signal_connect('button-release-event') { |w, event|
2739             if !$r90.active? && !$r270.active? && $gesture_press && $gesture_press[:filename] == thumbnail_file
2740                 msg 3, "press: #{$gesture_press[:x]} release: #{event.x}"
2741                 if (($gesture_press[:x]-event.x)/($gesture_press[:y]-event.y)).abs > 2 && ($gesture_press[:x]-event.x).abs > 5
2742                     angle = event.x-$gesture_press[:x] > 0 ? 90 : -90
2743                     msg 3, "gesture rotate: #{angle}"
2744                     rotate_and_cleanup.call(angle)
2745                 end
2746             end
2747             $gesture_press = nil
2748         }
2749                 
2750         $subalbums_edits[xmldir.attributes['path']][:editzone] = textview
2751         $subalbums_edits[xmldir.attributes['path']][:captionfile] = captionfile
2752         current_y_sub_albums += 1
2753     }
2754
2755     if $xmldir.child_byname_notattr('dir', 'deleted')
2756         #- title edition
2757         frame, $subalbums_title = create_editzone($subalbums_sw, 0, nil)
2758         $subalbums_title.buffer.text = $xmldir.attributes['subdirs-caption'] || ''
2759         $subalbums_title.set_justification(Gtk::Justification::CENTER)
2760         $subalbums_vb.pack_start(Gtk::Alignment.new(0.5, 0.5, 0.5, 0).add(frame), false, false)
2761         #- this album image/caption
2762         if $xmldir.attributes['thumbnails-caption']
2763             add_subalbum.call($xmldir, 0)
2764         end
2765     end
2766     total = { 'image' => 0, 'video' => 0, 'dir' => 0 }
2767     $xmldir.elements.each { |element|
2768         if (element.name == 'image' || element.name == 'video') && !element.attributes['deleted']
2769             #- element (image or video) of this album
2770             dest_img = build_full_dest_filename(element.attributes['filename']).sub(/\.[^\.]+$/, '') + "-#{$default_size['thumbnails']}.jpg"
2771             msg 3, "dest_img: #{dest_img}"
2772             add_thumbnail($autotable, element.attributes['filename'], element.name, dest_img, element.attributes['caption'])
2773             total[element.name] += 1
2774         end
2775         if element.name == 'dir' && !element.attributes['deleted']
2776             #- sub-album image/caption
2777             add_subalbum.call(element, subalbums_counter += 1)
2778             total[element.name] += 1
2779         end
2780     }
2781     $statusbar.push(0, utf8(_("%s: %s photos and %s videos, %s sub-albums") % [ File.basename(from_utf8($xmldir.attributes['path'])),
2782                                                                                 total['image'], total['video'], total['dir'] ]))
2783     $subalbums_vb.add($subalbums)
2784     $subalbums_vb.show_all
2785
2786     if !$xmldir.child_byname_notattr('image', 'deleted') && !$xmldir.child_byname_notattr('video', 'deleted')
2787         $notebook.get_tab_label($autotable_sw).sensitive = false
2788         $notebook.set_page(0)
2789         $thumbnails_title.buffer.text = ''
2790     else
2791         $notebook.get_tab_label($autotable_sw).sensitive = true
2792         $thumbnails_title.buffer.text = $xmldir.attributes['thumbnails-caption']
2793     end
2794
2795     if !$xmldir.child_byname_notattr('dir', 'deleted')
2796         $notebook.get_tab_label($subalbums_sw).sensitive = false
2797         $notebook.set_page(1)
2798     else
2799         $notebook.get_tab_label($subalbums_sw).sensitive = true
2800     end
2801 end
2802
2803 def pixbuf_or_nil(filename)
2804     begin
2805         return Gdk::Pixbuf.new(filename)
2806     rescue
2807         return nil
2808     end
2809 end
2810
2811 def theme_choose(current)
2812     dialog = Gtk::Dialog.new(utf8(_("Select your preferred theme")),
2813                              $main_window,
2814                              Gtk::Dialog::MODAL | Gtk::Dialog::DESTROY_WITH_PARENT,
2815                              [Gtk::Stock::OK, Gtk::Dialog::RESPONSE_OK],
2816                              [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
2817
2818     model = Gtk::ListStore.new(String, Gdk::Pixbuf, Gdk::Pixbuf, Gdk::Pixbuf)
2819     treeview = Gtk::TreeView.new(model).set_rules_hint(true)
2820     treeview.append_column(Gtk::TreeViewColumn.new(utf8(_("Theme name")), Gtk::CellRendererText.new, { :text => 0 }).set_alignment(0.5).set_spacing(5))
2821     treeview.append_column(Gtk::TreeViewColumn.new(utf8(_("Sub-albums page look")), Gtk::CellRendererPixbuf.new, { :pixbuf => 1 }).set_alignment(0.5).set_spacing(5))
2822     treeview.append_column(Gtk::TreeViewColumn.new(utf8(_("Thumbnails page look")), Gtk::CellRendererPixbuf.new, { :pixbuf => 2 }).set_alignment(0.5).set_spacing(5))
2823     treeview.append_column(Gtk::TreeViewColumn.new(utf8(_("Fullscreen page look")), Gtk::CellRendererPixbuf.new, { :pixbuf => 3 }).set_alignment(0.5).set_spacing(5))
2824     treeview.append_column(Gtk::TreeViewColumn.new('', Gtk::CellRendererText.new, {}))
2825     treeview.signal_connect('button-press-event') { |w, event|
2826         if event.event_type == Gdk::Event::BUTTON2_PRESS && event.button == 1
2827             dialog.response(Gtk::Dialog::RESPONSE_OK)
2828         end
2829     }
2830
2831     dialog.vbox.add(sw = Gtk::ScrolledWindow.new(nil, nil).add(treeview).set_policy(Gtk::POLICY_NEVER, Gtk::POLICY_AUTOMATIC))
2832
2833     ([ $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|
2834         dir.chomp!
2835         iter = model.append
2836         iter[0] = File.basename(dir)
2837         iter[1] = pixbuf_or_nil("#{dir}/metadata/screenshot-1.png")
2838         iter[2] = pixbuf_or_nil("#{dir}/metadata/screenshot-2.png")
2839         iter[3] = pixbuf_or_nil("#{dir}/metadata/screenshot-3.png")
2840         if File.basename(dir) == current
2841             treeview.selection.select_iter(iter)
2842         end
2843     }
2844     dialog.set_default_size(-1, 500)
2845     dialog.vbox.show_all
2846
2847     dialog.run { |response|
2848         iter = treeview.selection.selected
2849         dialog.destroy
2850         if response == Gtk::Dialog::RESPONSE_OK && iter
2851             return model.get_value(iter, 0)
2852         end
2853     }
2854     return nil
2855 end
2856
2857 def show_password_protections
2858     examine_dir_elem = proc { |parent_iter, xmldir, already_protected|
2859         child_iter = $albums_iters[xmldir.attributes['path']]
2860         if xmldir.attributes['password-protect']
2861             child_iter[2] = pixbuf_or_nil("#{$FPATH}/images/galeon-secure.png")
2862             already_protected = true
2863         elsif already_protected
2864             pix = pixbuf_or_nil("#{$FPATH}/images/galeon-secure-shadow.png")
2865             if pix
2866                 pix = pix.saturate_and_pixelate(1, true)
2867             end
2868             child_iter[2] = pix
2869         else
2870             child_iter[2] = nil
2871         end
2872         xmldir.elements.each('dir') { |elem|
2873             if !elem.attributes['deleted']
2874                 examine_dir_elem.call(child_iter, elem, already_protected)
2875             end
2876         }
2877     }
2878     examine_dir_elem.call(nil, $xmldoc.elements['//dir'], false)
2879 end
2880
2881 def populate_subalbums_treeview(select_first)
2882     $albums_ts.clear
2883     $autotable.clear
2884     $albums_iters = {}
2885     $subalbums_vb.children.each { |chld|
2886         $subalbums_vb.remove(chld)
2887     }
2888
2889     source = $xmldoc.root.attributes['source']
2890     msg 3, "source: #{source}"
2891
2892     xmldir = $xmldoc.elements['//dir']
2893     if !xmldir || xmldir.attributes['path'] != source
2894         msg 1, _("Corrupted booh file...")
2895         return
2896     end
2897
2898     append_dir_elem = proc { |parent_iter, xmldir|
2899         child_iter = $albums_ts.append(parent_iter)
2900         child_iter[0] = File.basename(xmldir.attributes['path'])
2901         child_iter[1] = xmldir.attributes['path']
2902         $albums_iters[xmldir.attributes['path']] = child_iter
2903         msg 3, "puttin location: #{xmldir.attributes['path']}"
2904         xmldir.elements.each('dir') { |elem|
2905             if !elem.attributes['deleted']
2906                 append_dir_elem.call(child_iter, elem)
2907             end
2908         }
2909     }
2910     append_dir_elem.call(nil, xmldir)
2911     show_password_protections
2912
2913     $albums_tv.expand_all
2914     if select_first
2915         $albums_tv.selection.select_iter($albums_ts.iter_first)
2916     end
2917 end
2918
2919 def select_current_theme
2920     select_theme($xmldoc.root.attributes['theme'],
2921                  $xmldoc.root.attributes['limit-sizes'],
2922                  !$xmldoc.root.attributes['optimize-for-32'].nil?,
2923                  $xmldoc.root.attributes['thumbnails-per-row'])
2924 end
2925
2926 def open_file(filename)
2927
2928     $filename = nil
2929     $modified = false
2930     $current_path = nil   #- invalidate
2931     $modified_pixbufs = {}
2932     $albums_ts.clear
2933     $autotable.clear
2934     $subalbums_vb.children.each { |chld|
2935         $subalbums_vb.remove(chld)
2936     }
2937
2938     if !File.exists?(filename)
2939         return utf8(_("File not found."))
2940     end
2941
2942     begin
2943         $xmldoc = REXML::Document.new(File.new(filename))
2944     rescue Exception
2945         $xmldoc = nil
2946     end
2947
2948     if !$xmldoc || !$xmldoc.root || $xmldoc.root.name != 'booh'
2949         if entry2type(filename).nil?
2950             return utf8(_("Not a booh file!"))
2951         else
2952             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."))
2953         end
2954     end
2955
2956     if !source = $xmldoc.root.attributes['source']
2957         return utf8(_("Corrupted booh file..."))
2958     end
2959
2960     if !dest = $xmldoc.root.attributes['destination']
2961         return utf8(_("Corrupted booh file..."))
2962     end
2963
2964     if !theme = $xmldoc.root.attributes['theme']
2965         return utf8(_("Corrupted booh file..."))
2966     end
2967
2968     if $xmldoc.root.attributes['version'] < $VERSION
2969         msg 2, _("File's version %s, booh version now %s, marking dirty") % [ $xmldoc.root.attributes['version'], $VERSION ]
2970         mark_document_as_dirty
2971         if $xmldoc.root.attributes['version'] < '0.8.4'
2972             msg 1, _("File's version prior to 0.8.4, migrating directories and filenames in destination directory if needed")
2973             `find '#{source}' -type d -follow`.sort.collect { |v| v.chomp }.each { |dir|
2974                 old_dest_dir = make_dest_filename_old(dir.sub(/^#{Regexp.quote(source)}/, dest))
2975                 new_dest_dir = make_dest_filename(dir.sub(/^#{Regexp.quote(source)}/, dest))
2976                 if old_dest_dir != new_dest_dir
2977                     sys("mv '#{old_dest_dir}' '#{new_dest_dir}'")
2978                 end
2979                 if xmldir = $xmldoc.elements["//dir[@path='#{utf8(dir)}']"]
2980                     xmldir.elements.each { |element|
2981                         if %w(image video).include?(element.name) && !element.attributes['deleted']
2982                             old_name = new_dest_dir + '/' + make_dest_filename_old(from_utf8(element.attributes['filename'])).sub(/\.[^\.]+$/, '')
2983                             new_name = new_dest_dir + '/' + make_dest_filename(from_utf8(element.attributes['filename'])).sub(/\.[^\.]+$/, '')
2984                             Dir[old_name + '*'].each { |file|
2985                                 new_file = file.sub(/^#{Regexp.quote(old_name)}/, new_name)
2986                                 file != new_file and sys("mv '#{file}' '#{new_file}'")
2987                             }
2988                         end
2989                         if element.name == 'dir' && !element.attributes['deleted']
2990                             old_name = new_dest_dir + '/thumbnails-' + make_dest_filename_old(from_utf8(File.basename(element.attributes['path']))) + '.jpg'
2991                             new_name = new_dest_dir + '/thumbnails-' + make_dest_filename(from_utf8(File.basename(element.attributes['path']))) + '.jpg'
2992                             old_name != new_name and sys("mv '#{old_name}' '#{new_name}'")
2993                         end
2994                     }
2995                 else
2996                     msg 1, "could not find <dir> element by path '#{utf8(dir)}' in xml file"
2997                 end
2998             }
2999         end
3000         $xmldoc.root.add_attribute('version', $VERSION)
3001     end
3002
3003     select_current_theme
3004
3005     $filename = filename
3006     set_mainwindow_title(nil)
3007     $default_size['thumbnails'] =~ /(.*)x(.*)/
3008     $default_thumbnails = { :x => $1.to_i, :y => $2.to_i }
3009     $albums_thumbnail_size =~ /(.*)x(.*)/
3010     $default_albums_thumbnails = { :x => $1.to_i, :y => $2.to_i }
3011
3012     populate_subalbums_treeview(true)
3013
3014     $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
3015     return nil
3016 end
3017
3018 def open_file_user(filename)
3019     result = open_file(filename)
3020     if !result
3021         $config['last-opens'] ||= []
3022         if $config['last-opens'][-1] != utf8(filename)
3023             $config['last-opens'] << utf8(filename)
3024         end
3025         $orig_filename = $filename
3026         $main_window.title = 'booh - ' + File.basename($orig_filename)
3027         tmp = Tempfile.new("boohtemp")
3028         Thread.critical = true
3029         $filename = tmp.path
3030         tmp.close!
3031         #- for security
3032         ios = File.open($filename, File::RDWR|File::CREAT|File::EXCL)
3033         Thread.critical = false
3034         ios.close
3035         $tempfiles << $filename << "#{$filename}.backup"
3036     else
3037         $orig_filename = nil
3038     end
3039     return result
3040 end
3041
3042 def open_file_popup
3043     if !ask_save_modifications(utf8(_("Save this album?")),
3044                                utf8(_("Do you want to save the changes to this album?")),
3045                                { :yes => Gtk::Stock::YES, :no => Gtk::Stock::NO })
3046         return
3047     end
3048     fc = Gtk::FileChooserDialog.new(utf8(_("Open file")),
3049                                     nil,
3050                                     Gtk::FileChooser::ACTION_OPEN,
3051                                     nil,
3052                                     [Gtk::Stock::OPEN, Gtk::Dialog::RESPONSE_ACCEPT], [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
3053     fc.add_shortcut_folder(File.expand_path("~/.booh"))
3054     fc.set_current_folder(File.expand_path("~/.booh"))
3055     fc.transient_for = $main_window
3056     fc.preview_widget = previewlabel = Gtk::Label.new.show
3057     fc.signal_connect('update-preview') { |w|
3058         if fc.preview_filename
3059             begin
3060                 push_mousecursor_wait(fc)
3061                 xmldoc = REXML::Document.new(File.new(fc.preview_filename))
3062                 subalbums = 0
3063                 images = 0
3064                 videos = 0
3065                 xmldoc.elements.each('//*') { |elem|
3066                     if elem.name == 'dir'
3067                         subalbums += 1
3068                     elsif elem.name == 'image'
3069                         images += 1
3070                     elsif elem.name == 'video'
3071                         videos += 1
3072                     end
3073                 }
3074             rescue Exception
3075             ensure
3076                 pop_mousecursor(fc)
3077             end
3078             if !xmldoc || !xmldoc.root || xmldoc.root.name != 'booh'
3079                 fc.preview_widget_active = false
3080             else
3081                 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") %
3082                                            [ xmldoc.root.attributes['source'], xmldoc.root.attributes['destination'], subalbums, images, videos ])
3083                 fc.preview_widget_active = true
3084             end
3085         end
3086     }
3087     ok = false
3088     while !ok
3089         if fc.run == Gtk::Dialog::RESPONSE_ACCEPT
3090             push_mousecursor_wait(fc)
3091             msg = open_file_user(fc.filename)
3092             pop_mousecursor(fc)
3093             if msg
3094                 show_popup(fc, msg)
3095                 ok = false
3096             else
3097                 ok = true
3098             end
3099         else
3100             ok = true
3101         end
3102     end
3103     fc.destroy
3104 end
3105
3106 def additional_booh_options
3107     options = ''
3108     if $config['mproc']
3109         options += "--mproc #{$config['mproc'].to_i} "
3110     end
3111     options += "--comments-format '#{$config['comments-format']}' "
3112     if $config['transcode-videos']
3113         options += "--transcode-videos '#{$config['transcode-videos']}' "
3114     end
3115     return options
3116 end
3117
3118 def ask_multi_languages(value)
3119     if ! value.nil?
3120         spl = value.split(',')
3121         value = [ spl[0..-2], spl[-1] ]
3122     end
3123
3124     dialog = Gtk::Dialog.new(utf8(_("Multi-languages support")),
3125                              $main_window,
3126                              Gtk::Dialog::MODAL,
3127                              [Gtk::Stock::OK, Gtk::Dialog::RESPONSE_OK],
3128                              [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
3129
3130     lbl = Gtk::Label.new
3131     lbl.markup = utf8(
3132 _("You can choose to activate <b>multi-languages</b> support for this web-album
3133 (it will work only if you publish your web-album on an Apache web-server). This will
3134 use the MultiViews feature of Apache; the pages will be served according to the
3135 value of the Accept-Language HTTP header sent by the web browsers, so that people
3136 with different languages preferences will be able to browse your web-album with
3137 navigation in their language (if language is available).
3138 "))
3139
3140     dialog.vbox.add(lbl)
3141     dialog.vbox.add(Gtk::Alignment.new(0.5, 0.5, 0, 0).add(Gtk::VBox.new.add(rb_no = Gtk::RadioButton.new(utf8(_("Disabled")))).
3142                                                                          add(Gtk::HBox.new(false, 5).add(rb_yes = Gtk::RadioButton.new(rb_no, utf8(_("Enabled")))).
3143                                                                                                      add(languages = Gtk::Button.new))))
3144
3145     pick_languages = proc {
3146         dialog2 = Gtk::Dialog.new(utf8(_("Pick languages to support")),
3147                                   $main_window,
3148                                   Gtk::Dialog::MODAL,
3149                                   [Gtk::Stock::OK, Gtk::Dialog::RESPONSE_OK],
3150                                   [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
3151
3152         dialog2.vbox.add(Gtk::Alignment.new(0.5, 0.5, 0, 0).add(hb1 = Gtk::HBox.new))
3153         hb1.add(Gtk::Label.new(utf8(_("Select the languages to support:"))))
3154         cbs = []
3155         SUPPORTED_LANGUAGES.each { |lang|
3156             hb1.add(cb = Gtk::CheckButton.new(utf8(langname(lang))))
3157             if ! value.nil? && value[0].include?(lang)
3158                 cb.active = true
3159             end
3160             cbs << [ lang, cb ]
3161         }
3162
3163         dialog2.vbox.add(Gtk::Alignment.new(0.5, 0.5, 0, 0).add(hb2 = Gtk::HBox.new))
3164         hb2.add(Gtk::Label.new(utf8(_("Select the fallback language:"))))
3165         fallback_language = nil
3166         hb2.add(fbl_rb = Gtk::RadioButton.new(utf8(langname(SUPPORTED_LANGUAGES[0]))))
3167         fbl_rb.signal_connect('clicked') { fallback_language = SUPPORTED_LANGUAGES[0] }
3168         if value.nil? || value[1] == SUPPORTED_LANGUAGES[0]
3169             fbl_rb.active = true
3170             fallback_language = SUPPORTED_LANGUAGES[0]
3171         end
3172         SUPPORTED_LANGUAGES[1..-1].each { |lang|
3173             hb2.add(rb = Gtk::RadioButton.new(fbl_rb, utf8(langname(lang))))
3174             rb.signal_connect('clicked') { fallback_language = lang }
3175             if ! value.nil? && value[1] == lang
3176                 rb.active = true
3177             end
3178         }
3179
3180         dialog2.window_position = Gtk::Window::POS_MOUSE
3181         dialog2.show_all
3182
3183         resp = nil
3184         dialog2.run { |response|
3185             resp = response
3186             if resp == Gtk::Dialog::RESPONSE_OK
3187                 value = []
3188                 value[0] = cbs.find_all { |e| e[1].active? }.collect { |e| e[0] }
3189                 value[1] = fallback_language
3190                 languages.label = utf8(_("Languages: %s. Fallback: %s.") % [ value[0].collect { |v| langname(v) }.join(', '), langname(value[1]) ])
3191             end
3192             dialog2.destroy
3193         }
3194         resp
3195     }
3196
3197     languages.signal_connect('clicked') {
3198         pick_languages.call
3199     }
3200     dialog.window_position = Gtk::Window::POS_MOUSE
3201     if value.nil?
3202         rb_no.active = true
3203     else
3204         rb_yes.active = true
3205         languages.label = utf8(_("Languages: %s. Fallback: %s.") % [ value[0].collect { |v| langname(v) }.join(', '), langname(value[1]) ])
3206     end
3207     rb_no.signal_connect('clicked') {
3208         if rb_no.active?
3209             languages.hide
3210         else
3211             if pick_languages.call == Gtk::Dialog::RESPONSE_CANCEL
3212                 rb_no.activate
3213             else
3214                 languages.show
3215             end
3216         end
3217     }
3218     oldval = value
3219     dialog.show_all
3220     if rb_no.active?
3221         languages.hide
3222     end
3223
3224     dialog.run { |response|
3225         if rb_no.active?
3226             value = nil
3227         end
3228         dialog.destroy
3229         if response == Gtk::Dialog::RESPONSE_OK && value != oldval
3230             if value.nil?
3231                 return [ true, nil ]
3232             else
3233                 return [ true, (value[0].size == 0 ? '' : value[0].join(',') + ',') + value[1] ]
3234             end
3235         else
3236             return [ false ]
3237         end
3238     }
3239 end
3240
3241 def new_album
3242     if !ask_save_modifications(utf8(_("Save this album?")),
3243                                utf8(_("Do you want to save the changes to this album?")),
3244                                { :yes => Gtk::Stock::YES, :no => Gtk::Stock::NO })
3245         return
3246     end
3247     dialog = Gtk::Dialog.new(utf8(_("Create a new album")),
3248                              $main_window,
3249                              Gtk::Dialog::MODAL | Gtk::Dialog::DESTROY_WITH_PARENT,
3250                              [Gtk::Stock::OK, Gtk::Dialog::RESPONSE_OK],
3251                              [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
3252     
3253     frame1 = Gtk::Frame.new(utf8(_("Locations"))).add(tbl = Gtk::Table.new(0, 0, false))
3254     tbl.attach(Gtk::Label.new(utf8(_("Directory of photos/videos: "))),
3255                0, 1, 0, 1, Gtk::SHRINK, Gtk::SHRINK, 2, 2)
3256     tbl.attach(src = Gtk::Entry.new,
3257                1, 2, 0, 1, Gtk::FILL, Gtk::SHRINK, 2, 2)
3258     tbl.attach(src_browse = Gtk::Button.new(utf8(_("browse..."))),
3259                2, 3, 0, 1, Gtk::SHRINK, Gtk::SHRINK, 2, 2)
3260     tbl.attach(Gtk::Label.new.set_markup(utf8(_("<span size='small'><i>number of photos/videos down this directory:</i></span> "))),
3261                0, 1, 1, 2, Gtk::SHRINK, Gtk::SHRINK, 2, 2)
3262     tbl.attach(src_nb = Gtk::Label.new.set_markup(utf8(_("<span size='small'><i>N/A</i></span>"))),
3263                1, 2, 1, 2, Gtk::FILL, Gtk::SHRINK, 2, 2)
3264     tbl.attach(Gtk::Label.new(utf8(_("Directory where to put the web-album: "))),
3265                0, 1, 2, 3, Gtk::SHRINK, Gtk::SHRINK, 2, 2)
3266     tbl.attach(dest = Gtk::Entry.new,
3267                1, 2, 2, 3, Gtk::FILL, Gtk::SHRINK, 2, 2)
3268     tbl.attach(dest_browse = Gtk::Button.new(utf8(_("browse..."))),
3269                2, 3, 2, 3, Gtk::SHRINK, Gtk::SHRINK, 2, 2)
3270     tbl.attach(Gtk::Label.new(utf8(_("Filename to store this album's properties: "))),
3271                0, 1, 3, 4, Gtk::SHRINK, Gtk::SHRINK, 2, 2)
3272     tbl.attach(conf = Gtk::Entry.new.set_size_request(250, -1),
3273                1, 2, 3, 4, Gtk::FILL, Gtk::SHRINK, 2, 2)
3274     tbl.attach(conf_browse = Gtk::Button.new(utf8(_("browse..."))),
3275                2, 3, 3, 4, Gtk::SHRINK, Gtk::SHRINK, 2, 2)
3276
3277     tooltips = Gtk::Tooltips.new
3278     frame2 = Gtk::Frame.new(utf8(_("Configuration"))).add(vb = Gtk::VBox.new)
3279     vb.add(Gtk::HBox.new(false, 3).pack_start(Gtk::Label.new(utf8(_("Theme: "))), false, false, 0).
3280                          pack_start(theme_button = Gtk::Button.new($config['default-theme'] || 'simple'), false, false, 0))
3281     vb.add(Gtk::HBox.new(false, 3).pack_start(Gtk::Label.new(utf8(_("Sizes of images to generate: "))), false, false, 0).
3282                                    pack_start(sizes = Gtk::HBox.new, false, false, 0))
3283     vb.add(optimize432 = Gtk::CheckButton.new(utf8(_("Optimize for 3/2 aspect ratio"))).set_active($config['default-optimize32'].to_b))
3284     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)
3285     vb.add(Gtk::HBox.new(false, 3).pack_start(Gtk::Label.new(utf8(_("Number of thumbnails per row: "))), false, false, 0).
3286                                    pack_start(nperrowradios = Gtk::HBox.new, false, false, 0))
3287     nperpage_model = Gtk::ListStore.new(String, String)
3288     vb.add(Gtk::HBox.new(false, 3).pack_start(Gtk::Label.new(utf8(_("Number of thumbnails per page: "))), false, false, 0).
3289                                    pack_start(nperpagecombo = Gtk::ComboBox.new(nperpage_model), false, false, 0))
3290     nperpagecombo.pack_start(crt = Gtk::CellRendererText.new, false)
3291     nperpagecombo.set_attributes(crt, { :markup => 0 })
3292     iter = nperpage_model.append
3293     iter[0] = utf8(_("<i>None - all thumbnails in one page</i>"))
3294     iter[1] = nil
3295     [ 12, 20, 30, 40, 50 ].each { |v|
3296         iter = nperpage_model.append
3297         iter[0] = iter[1] = v.to_s
3298     }
3299     nperpagecombo.active = 0
3300
3301     multilanguages_value = nil
3302     vb.add(ml = Gtk::HBox.new(false, 3).pack_start(ml_label = Gtk::Label.new, false, false, 0).
3303                                         pack_start(multilanguages = Gtk::Button.new(utf8(_("Configure multi-languages"))), false, false, 0))
3304     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)
3305     multilanguages.signal_connect('clicked') {
3306         retval = ask_multi_languages(multilanguages_value)
3307         if retval[0] 
3308             multilanguages_value = retval[1]
3309         end
3310         if multilanguages_value
3311             ml_label.text = utf8(_("Multi-languages: enabled."))
3312         else
3313             ml_label.text = utf8(_("Multi-languages: disabled."))
3314         end
3315     }
3316     if $config['default-multi-languages']
3317         multilanguages_value = $config['default-multi-languages']
3318         ml_label.text = utf8(_("Multi-languages: enabled."))
3319     else
3320         ml_label.text = utf8(_("Multi-languages: disabled."))
3321     end
3322
3323     vb.add(Gtk::HBox.new(false, 3).pack_start(Gtk::Label.new(utf8(_("'Return to your website' link on pages bottom: "))), false, false, 0).
3324                                    pack_start(indexlinkentry = Gtk::Entry.new, true, true, 0))
3325     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)
3326     vb.add(Gtk::HBox.new(false, 3).pack_start(Gtk::Label.new(utf8(_("'Made with' markup on pages bottom: "))), false, false, 0).
3327                                    pack_start(madewithentry = Gtk::Entry.new.set_text(utf8(_("made with <a href=%booh>booh</a>!"))), true, true, 0))
3328     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)
3329     vb.add(addthis = Gtk::CheckButton.new(utf8(_("Include the 'addthis' bookmarking and sharing button"))).set_active($config['default-addthis'].to_b))
3330     vb.add(quotehtml = Gtk::CheckButton.new(utf8(_("Quote HTML markup in captions"))).set_active($config['default-quotehtml'].to_b))
3331     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)
3332
3333     src_nb_calculated_for = ''
3334     src_nb_process = nil
3335     process_src_nb = proc {
3336         if src.text != src_nb_calculated_for
3337             src_nb_calculated_for = src.text
3338             if src_nb_process
3339                 begin
3340                     Process.kill(9, src_nb_process)
3341                 rescue Errno::ESRCH
3342                     #- process doesn't exist anymore - race condition
3343                 end
3344             end
3345             if src_nb_calculated_for != '' && from_utf8_safe(src_nb_calculated_for) == ''
3346                 src_nb.set_markup(utf8(_("<span size='small'><i>invalid source directory</i></span>")))
3347             else
3348                 if File.directory?(from_utf8_safe(src_nb_calculated_for)) && src_nb_calculated_for != '/'
3349                     if File.readable?(from_utf8_safe(src_nb_calculated_for))
3350                         rd, wr = IO.pipe
3351                         if src_nb_process
3352                             while src_nb_process
3353                                 msg 3, "sleeping for completion of previous process"
3354                                 sleep 0.05
3355                             end
3356                             gtk_thread_flush  #- flush to avoid race condition in src_nb markup update
3357                         end
3358                         src_nb.set_markup(utf8(_("<span size='small'><i>processing...</i></span>")))
3359                         total = { 'image' => 0, 'video' => 0, nil => 0 }
3360                         if src_nb_process = fork
3361                             msg 3, "spawned #{src_nb_process} for #{src_nb_calculated_for}"
3362                             #- parent
3363                             wr.close
3364                             Thread.new {
3365                                 rd.readlines.each { |dir|
3366                                     if File.basename(dir) =~ /^\./
3367                                         next
3368                                     else
3369                                         begin
3370                                             Dir.entries(dir.chomp).each { |file|
3371                                                 total[entry2type(file)] += 1
3372                                             }
3373                                         rescue Errno::EACCES, Errno::ENOENT
3374                                         end
3375                                     end
3376                                 }
3377                                 rd.close
3378                                 msg 3, "ripping #{src_nb_process}"
3379                                 dummy, exitstatus = Process.waitpid2(src_nb_process)
3380                                 if exitstatus == 0
3381                                     gtk_thread_protect { src_nb.set_markup(utf8(_("<span size='small'><i>%s photos and %s videos</i></span>") % [ total['image'], total['video'] ])) }
3382                                 end
3383                                 src_nb_process = nil
3384                             }
3385                             
3386                         else
3387                             #- child
3388                             rd.close
3389                             wr.write(`find '#{from_utf8_safe(src_nb_calculated_for)}' -type d -follow`)
3390                             Process.exit!(0)  #- _exit
3391                         end                       
3392                     else
3393                         src_nb.set_markup(utf8(_("<span size='small'><i>permission denied</i></span>")))
3394                     end
3395                 else
3396                     src_nb.set_markup(utf8(_("<span size='small'><i>N/A</i></span>")))
3397                 end
3398             end
3399         end
3400         true
3401     }
3402     timeout_src_nb = Gtk.timeout_add(100) {
3403         process_src_nb.call
3404     }
3405
3406     src_browse.signal_connect('clicked') {
3407         fc = Gtk::FileChooserDialog.new(utf8(_("Select the directory of photos/videos")),
3408                                         nil,
3409                                         Gtk::FileChooser::ACTION_SELECT_FOLDER,
3410                                         nil,
3411                                         [Gtk::Stock::OPEN, Gtk::Dialog::RESPONSE_ACCEPT], [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
3412         fc.transient_for = $main_window
3413         if fc.run == Gtk::Dialog::RESPONSE_ACCEPT
3414             src.text = utf8(fc.filename)
3415             process_src_nb.call
3416             conf.text = File.expand_path("~/.booh/#{File.basename(src.text)}")
3417         end
3418         fc.destroy
3419     }
3420
3421     dest_browse.signal_connect('clicked') {
3422         fc = Gtk::FileChooserDialog.new(utf8(_("Select a new directory where to put the web-album")),
3423                                         nil,
3424                                         Gtk::FileChooser::ACTION_CREATE_FOLDER,
3425                                         nil,
3426                                         [Gtk::Stock::OPEN, Gtk::Dialog::RESPONSE_ACCEPT], [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
3427         fc.transient_for = $main_window
3428         if fc.run == Gtk::Dialog::RESPONSE_ACCEPT
3429             dest.text = utf8(fc.filename)
3430         end
3431         fc.destroy
3432     }
3433
3434     conf_browse.signal_connect('clicked') {
3435         fc = Gtk::FileChooserDialog.new(utf8(_("Select a new file to store this album's properties")),
3436                                         nil,
3437                                         Gtk::FileChooser::ACTION_SAVE,
3438                                         nil,
3439                                         [Gtk::Stock::SAVE, Gtk::Dialog::RESPONSE_ACCEPT], [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
3440         fc.transient_for = $main_window
3441         fc.add_shortcut_folder(File.expand_path("~/.booh"))
3442         fc.set_current_folder(File.expand_path("~/.booh"))
3443         if fc.run == Gtk::Dialog::RESPONSE_ACCEPT
3444             conf.text = utf8(fc.filename)
3445         end
3446         fc.destroy
3447     }
3448
3449     theme_sizes = []
3450     nperrows = []
3451     recreate_theme_config = proc {
3452         theme_sizes.each { |e| sizes.remove(e[:widget]) }
3453         theme_sizes = []
3454         select_theme(theme_button.label, 'all', optimize432.active?, nil)
3455         $images_size.each { |s|
3456             sizes.add(cb = Gtk::CheckButton.new(sizename(s['name'], true)))
3457             if !s['optional']
3458                 cb.active = true
3459             end
3460             tooltips.set_tip(cb, utf8(s['description']), nil)
3461             theme_sizes << { :widget => cb, :value => s['name'] }
3462         }
3463         sizes.add(cb = Gtk::CheckButton.new(utf8(_('original'))))
3464         tooltips = Gtk::Tooltips.new
3465         tooltips.set_tip(cb, utf8(_("Include original photo in web-album")), nil)
3466         theme_sizes << { :widget => cb, :value => 'original' }
3467         sizes.show_all
3468
3469         nperrows.each { |e| nperrowradios.remove(e[:widget]) }
3470         nperrow_group = nil
3471         nperrows = []
3472         $allowed_N_values.each { |n|
3473             if nperrow_group
3474                 nperrowradios.add(rb = Gtk::RadioButton.new(nperrow_group, n.to_s, false))
3475             else
3476                 nperrowradios.add(nperrow_group = rb = Gtk::RadioButton.new(n.to_s))
3477             end
3478             tooltips.set_tip(rb, utf8(_("Set the number of thumbnails per row of the 'thumbnails' pages (if chosen theme uses a row arrangement)")), nil)
3479             if $default_N == n
3480                 rb.active = true
3481             end
3482             nperrows << { :widget => rb, :value => n }
3483         }
3484         nperrowradios.show_all
3485     }
3486     recreate_theme_config.call
3487
3488     theme_button.signal_connect('clicked') {
3489         if newtheme = theme_choose(theme_button.label)
3490             theme_button.label = newtheme
3491             recreate_theme_config.call
3492         end
3493     }
3494
3495     dialog.vbox.add(frame1)
3496     dialog.vbox.add(frame2)
3497     dialog.show_all
3498
3499     keepon = true
3500     ok = true
3501     while keepon
3502         dialog.run { |response|
3503             if response == Gtk::Dialog::RESPONSE_OK
3504                 srcdir = from_utf8_safe(src.text)
3505                 destdir = from_utf8_safe(dest.text)
3506                 confpath = from_utf8_safe(conf.text)
3507                 if src.text != '' && srcdir == ''
3508                     show_popup(dialog, utf8(_("The directory of photos/videos is invalid. Please check your input.")))
3509                     src.grab_focus
3510                 elsif !File.directory?(srcdir)
3511                     show_pop