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