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