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