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