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