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