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