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