a86bb630fe0e8a2e9996da720e548bf650622dd7
[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-2008 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-2008 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     rexml_thread_protect {
548         if xmldir
549             value = xmldir.attributes["#{attributes_prefix}seektime"]
550         else
551             value = ''
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     rexml_thread_protect {
609         if xmldir
610             value = xmldir.attributes["#{attributes_prefix}pano-amount"]
611         else
612             value = 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                     $name2widgets[path][:img].pixbuf = $name2widgets[path][:img].pixbuf.saturate_and_pixelate(1, true)
1734                 end
1735             end
1736             if $selected_elements[path] && ! $selected_elements[path][:keep]
1737                 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))
1738                     $name2widgets[path][:img].pixbuf = $selected_elements[path][:pixbuf]
1739                     $selected_elements.delete(path)
1740                 end
1741             end
1742         }
1743     }
1744     $autotable.signal_connect('realize') { |w,e|
1745         gc = Gdk::GC.new($autotable.window)
1746         gc.set_line_attributes(1, Gdk::GC::LINE_ON_OFF_DASH, Gdk::GC::CAP_PROJECTING, Gdk::GC::JOIN_ROUND)
1747         gc.function = Gdk::GC::INVERT
1748         #- autoscroll handling for DND and multiple selections
1749         Gtk.timeout_add(100) {
1750             if ! $autotable.window.nil?
1751                 w, x, y, mask = $autotable.window.pointer
1752                 if mask & Gdk::Window::BUTTON1_MASK != 0
1753                     if y < $autotable_sw.vadjustment.value
1754                         if pos_x
1755                             $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]])
1756                         end
1757                         if $button1_pressed_autotable || press_x
1758                             scroll_upper($autotable_sw, y)
1759                         end
1760                         if not press_x.nil?
1761                             w, pos_x, pos_y = $autotable.window.pointer
1762                             $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]])
1763                             update_selected.call
1764                         end
1765                     end
1766                     if y > $autotable_sw.vadjustment.value + $autotable_sw.vadjustment.page_size
1767                         if pos_x
1768                             $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]])
1769                         end
1770                         if $button1_pressed_autotable || press_x
1771                             scroll_lower($autotable_sw, y)
1772                         end
1773                         if not press_x.nil?
1774                             w, pos_x, pos_y = $autotable.window.pointer
1775                             $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]])
1776                             update_selected.call
1777                         end
1778                     end
1779                 end
1780             end
1781             ! $autotable.window.nil?
1782         }
1783     }
1784
1785     $autotable.signal_connect('button-press-event') { |w,e|
1786         if e.button == 1
1787             if !$button1_pressed_autotable
1788                 press_x = e.x
1789                 press_y = e.y
1790                 if e.state & Gdk::Window::SHIFT_MASK == 0
1791                     $selected_elements.each_key { |path| $name2widgets[path][:img].pixbuf = $selected_elements[path][:pixbuf] }
1792                     $selected_elements = {}
1793                     $statusbar.push(0, utf8(_("Nothing selected.")))
1794                 else
1795                     $selected_elements.each_key { |path| $selected_elements[path][:keep] = true }
1796                 end
1797                 set_mousecursor(Gdk::Cursor::TCROSS)
1798             end
1799         end
1800     }
1801     $autotable.signal_connect('button-release-event') { |w,e|
1802         if e.button == 1
1803             if $button1_pressed_autotable
1804                 #- unselect all only now
1805                 $multiple_dnd = $selected_elements.keys
1806                 $selected_elements.each_key { |path| $name2widgets[path][:img].pixbuf = $selected_elements[path][:pixbuf] }
1807                 $selected_elements = {}
1808                 $button1_pressed_autotable = false
1809             else
1810                 if pos_x
1811                     $autotable.window.draw_lines(gc, [[press_x, press_y], [pos_x, press_y], [pos_x, pos_y], [press_x, pos_y], [press_x, press_y]])
1812                     if $selected_elements.length > 0
1813                         $statusbar.push(0, utf8(_("%s elements selected.") % $selected_elements.length))
1814                     end
1815                 end
1816                 press_x = press_y = pos_x = pos_y = nil
1817                 set_mousecursor(Gdk::Cursor::LEFT_PTR)
1818             end
1819         end
1820     }
1821     $autotable.signal_connect('motion-notify-event') { |w,e|
1822         if ! press_x.nil?
1823             if pos_x
1824                 $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]])
1825             end
1826             pos_x = e.x
1827             pos_y = e.y
1828             $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]])
1829             update_selected.call
1830         end
1831     }
1832
1833 end
1834
1835 def create_subalbums_page
1836
1837     subalbums_hb = Gtk::HBox.new
1838     $subalbums_vb = Gtk::VBox.new(false, 5)
1839     subalbums_hb.pack_start($subalbums_vb, false, false)
1840     $subalbums_sw = Gtk::ScrolledWindow.new(nil, nil)
1841     $subalbums_sw.set_policy(Gtk::POLICY_AUTOMATIC, Gtk::POLICY_AUTOMATIC)
1842     $subalbums_sw.add_with_viewport(subalbums_hb)
1843 end
1844
1845 def save_current_file
1846     save_changes
1847
1848     if $filename
1849         begin
1850             begin
1851                 ios = File.open($filename, "w")
1852                 $xmldoc.write(ios, 0)
1853                 ios.close
1854             rescue Iconv::IllegalSequence
1855                 #- user might have entered text which cannot be encoded in his default encoding. retry in UTF-8.
1856                 if ! ios.nil? && ! ios.closed?
1857                     ios.close
1858                 end
1859                 $xmldoc.xml_decl.encoding = 'UTF-8'
1860                 ios = File.open($filename, "w")
1861                 $xmldoc.write(ios, 0)
1862                 ios.close
1863             end
1864             return true
1865         rescue Exception
1866             puts $!
1867             return false
1868         end
1869     end
1870 end
1871
1872 def save_current_file_user
1873     save_tempfilename = $filename
1874     $filename = $orig_filename
1875     if ! save_current_file
1876         show_popup($main_window, utf8(_("Save failed! Try another location/name.")))
1877         $filename = save_tempfilename
1878         return
1879     end
1880     $modified = false
1881     $generated_outofline = false
1882     $filename = save_tempfilename
1883
1884     msg 3, "performing actual deletion of: " + $todelete.join(', ')
1885     $todelete.each { |f|
1886         File.delete(f)
1887     }
1888 end
1889
1890 def mark_document_as_dirty
1891     $xmldoc.elements.each('//dir') { |elem|
1892         elem.delete_attribute('already-generated')
1893     }
1894 end
1895
1896 #- ret: true => ok  false => cancel
1897 def ask_save_modifications(msg1, msg2, *options)
1898     ret = true
1899     options = options.size > 0 ? options[0] : {}
1900     if $modified
1901         if options[:disallow_cancel]
1902             dialog = Gtk::Dialog.new(msg1,
1903                                      $main_window,
1904                                      Gtk::Dialog::MODAL | Gtk::Dialog::DESTROY_WITH_PARENT,
1905                                      [options[:no] || Gtk::Stock::CLOSE, Gtk::Dialog::RESPONSE_NO],
1906                                      [options[:yes] || Gtk::Stock::SAVE, Gtk::Dialog::RESPONSE_YES])
1907         else
1908             dialog = Gtk::Dialog.new(msg1,
1909                                      $main_window,
1910                                      Gtk::Dialog::MODAL | Gtk::Dialog::DESTROY_WITH_PARENT,
1911                                      [options[:cancel] || Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL],
1912                                      [options[:no] || Gtk::Stock::CLOSE, Gtk::Dialog::RESPONSE_NO],
1913                                      [options[:yes] || Gtk::Stock::SAVE, Gtk::Dialog::RESPONSE_YES])
1914         end
1915         dialog.default_response = Gtk::Dialog::RESPONSE_YES
1916         dialog.vbox.add(Gtk::Label.new(msg2))
1917         dialog.window_position = Gtk::Window::POS_CENTER
1918         dialog.show_all
1919         
1920         dialog.run { |response|
1921             dialog.destroy
1922             if response == Gtk::Dialog::RESPONSE_YES
1923                 if ! save_current_file_user
1924                     return ask_save_modifications(msg1, msg2, options)
1925                 end
1926             else
1927                 #- if we have generated an album but won't save modifications, we must remove 
1928                 #- already-generated markers in original file
1929                 if $generated_outofline
1930                     begin
1931                         $xmldoc = REXML::Document.new(File.new($orig_filename))
1932                         mark_document_as_dirty
1933                         ios = File.open($orig_filename, "w")
1934                         $xmldoc.write(ios, 0)
1935                         ios.close
1936                     rescue Exception
1937                         puts "exception: #{$!}"
1938                     end
1939                 end
1940             end
1941             if response == Gtk::Dialog::RESPONSE_CANCEL
1942                 ret = false
1943             end
1944             $todelete = []  #- unconditionally clear the list of images/videos to delete
1945         }
1946     end
1947     return ret
1948 end
1949
1950 def try_quit(*options)
1951     if ask_save_modifications(utf8(_("Save before quitting?")),
1952                               utf8(_("Do you want to save your changes before quitting?")),
1953                               *options)
1954         Gtk.main_quit
1955     end
1956 end
1957
1958 def show_popup(parent, msg, *options)
1959     dialog = Gtk::Dialog.new
1960     if options[0] && options[0][:title]
1961         dialog.title = options[0][:title]
1962     else
1963         dialog.title = utf8(_("Booh message"))
1964     end
1965     lbl = Gtk::Label.new
1966     if options[0] && options[0][:nomarkup]
1967         lbl.text = msg
1968     else
1969         lbl.markup = msg
1970     end
1971     if options[0] && options[0][:centered]
1972         lbl.set_justify(Gtk::Justification::CENTER)
1973     end
1974     if options[0] && options[0][:selectable]
1975         lbl.selectable = true
1976     end
1977     if options[0] && options[0][:topwidget]
1978         dialog.vbox.add(options[0][:topwidget])
1979     end
1980     if options[0] && options[0][:scrolled]
1981         sw = Gtk::ScrolledWindow.new(nil, nil)
1982         sw.set_policy(Gtk::POLICY_AUTOMATIC, Gtk::POLICY_AUTOMATIC)
1983         sw.add_with_viewport(lbl)
1984         dialog.vbox.add(sw)
1985         dialog.set_default_size(500, 600)
1986     else
1987         dialog.vbox.add(lbl)
1988         dialog.set_default_size(200, 120)
1989     end
1990     if options[0] && options[0][:okcancel]
1991         dialog.add_button(Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL)
1992     end
1993     dialog.add_button(Gtk::Stock::OK, Gtk::Dialog::RESPONSE_OK)
1994
1995     if options[0] && options[0][:pos_centered]
1996         dialog.window_position = Gtk::Window::POS_CENTER
1997     else
1998         dialog.window_position = Gtk::Window::POS_MOUSE
1999     end
2000
2001     if options[0] && options[0][:linkurl]
2002         linkbut = Gtk::Button.new('')
2003         linkbut.child.markup = "<span foreground=\"#00000000FFFF\" underline=\"single\">#{options[0][:linkurl]}</span>"
2004         linkbut.signal_connect('clicked') {
2005             open_url(options[0][:linkurl])
2006             dialog.response(Gtk::Dialog::RESPONSE_OK)
2007             set_mousecursor_normal
2008         }
2009         linkbut.relief = Gtk::RELIEF_NONE
2010         linkbut.signal_connect('enter-notify-event') { set_mousecursor(Gdk::Cursor::HAND2, linkbut); false }
2011         linkbut.signal_connect('leave-notify-event') { set_mousecursor(nil, linkbut); false }
2012         dialog.vbox.add(Gtk::Alignment.new(0.5, 0.5, 0, 0).add(linkbut))
2013     end
2014
2015     dialog.show_all
2016
2017     if !options[0] || !options[0][:not_transient]
2018         dialog.transient_for = parent
2019         dialog.run { |response|
2020             dialog.destroy
2021             if options[0] && options[0][:okcancel]
2022                 return response == Gtk::Dialog::RESPONSE_OK
2023             end
2024         }
2025     else
2026         dialog.signal_connect('response') { dialog.destroy }
2027     end
2028 end
2029
2030 def set_mainwindow_title(progress)
2031     filename = $orig_filename || $filename
2032     if progress
2033         if filename
2034             $main_window.title = 'booh [' + (progress * 100).to_i.to_s + '%] - ' + File.basename(filename)
2035         else
2036             $main_window.title = 'booh [' + (progress * 100).to_i.to_s + '%] '
2037         end
2038     else
2039         if filename
2040             $main_window.title = 'booh - ' + File.basename(filename)
2041         else
2042             $main_window.title = 'booh'
2043         end
2044     end
2045 end
2046
2047 def backend_wait_message(parent, msg, infopipe_path, mode)
2048     w = create_window
2049     w.set_transient_for(parent)
2050     w.modal = true
2051
2052     vb = Gtk::VBox.new(false, 5).set_border_width(5)
2053     vb.pack_start(Gtk::Label.new(msg), false, false)
2054
2055     vb.pack_start(frame1 = Gtk::Frame.new(utf8(_("Thumbnails"))).add(vb1 = Gtk::VBox.new(false, 5)))
2056     vb1.pack_start(pb1_1 = Gtk::ProgressBar.new.set_text(utf8(_("Scanning photos and videos..."))), false, false)
2057     if mode != 'one dir scan'
2058         vb1.pack_start(pb1_2 = Gtk::ProgressBar.new.set_text(utf8(_("not started"))), false, false)
2059     end
2060     if mode == 'web-album'
2061         vb.pack_start(frame2 = Gtk::Frame.new(utf8(_("HTML pages"))).add(vb1 = Gtk::VBox.new(false, 5)))
2062         vb1.pack_start(pb2 = Gtk::ProgressBar.new.set_text(utf8(_("not started"))), false, false)
2063     end
2064     vb.pack_start(Gtk::HSeparator.new, false, false)
2065
2066     bottom = Gtk::Alignment.new(0.5, 0.5, 0, 0).add(b = Gtk::Button.new(utf8(_("_Abort"))))
2067     b.image = Gtk::Image.new("#{$FPATH}/images/stock-close-24.png")
2068     vb.pack_end(bottom, false, false)
2069
2070     directories = nil
2071     update_progression_title_pb1 = proc {
2072         if mode == 'web-album'
2073             set_mainwindow_title((pb1_2.fraction + pb1_1.fraction / directories) * 9 / 10)
2074         elsif mode != 'one dir scan'
2075             set_mainwindow_title(pb1_2.fraction + pb1_1.fraction / directories)
2076         else
2077             set_mainwindow_title(pb1_1.fraction)
2078         end
2079     }
2080
2081     infopipe = File.open(infopipe_path, File::RDONLY | File::NONBLOCK)
2082     refresh_thread = Thread.new {
2083         directories_counter = 0
2084         while line = infopipe.gets
2085             msg 3, "infopipe got data: #{line}"
2086             if line =~ /^directories: (\d+), sizes: (\d+)/
2087                 directories = $1.to_f + 1
2088                 sizes = $2.to_f
2089             elsif line =~ /^walking: (.+)\|(.+), (\d+) elements$/
2090                 elements = $3.to_f + 1
2091                 if mode == 'web-album'
2092                     elements += sizes
2093                 end
2094                 element_counter = 0
2095                 gtk_thread_protect { pb1_1.fraction = 0 }
2096                 if mode != 'one dir scan'
2097                     newtext = utf8(full_src_dir_to_rel($1, $2))
2098                     newtext = '/' if newtext == ''
2099                     gtk_thread_protect { pb1_2.text = newtext }
2100                     directories_counter += 1
2101                     gtk_thread_protect {
2102                         pb1_2.fraction = directories_counter / directories
2103                         update_progression_title_pb1.call
2104                     }
2105                 end
2106             elsif line =~ /^processing element$/
2107                 element_counter += 1
2108                 gtk_thread_protect {
2109                     pb1_1.fraction = element_counter / elements
2110                     update_progression_title_pb1.call
2111                 }
2112             elsif line =~ /^processing size$/
2113                 element_counter += 1
2114                 gtk_thread_protect {
2115                     pb1_1.fraction = element_counter / elements
2116                     update_progression_title_pb1.call
2117                 }
2118             elsif line =~ /^finished processing sizes$/
2119                 gtk_thread_protect { pb1_1.fraction = 1 }
2120             elsif line =~ /^creating index.html$/
2121                 gtk_thread_protect { pb1_2.text = utf8(_("finished")) }
2122                 gtk_thread_protect { pb1_1.fraction = pb1_2.fraction = 1 }
2123                 directories_counter = 0
2124             elsif line =~ /^index.html: (.+)\|(.+)/
2125                 newtext = utf8(full_src_dir_to_rel($1, $2))
2126                 newtext = '/' if newtext == ''
2127                 gtk_thread_protect { pb2.text = newtext }
2128                 directories_counter += 1
2129                 gtk_thread_protect {
2130                     pb2.fraction = directories_counter / directories
2131                     set_mainwindow_title(0.9 + pb2.fraction / 10)
2132                 }
2133             elsif line =~ /^die: (.*)$/
2134                 $diemsg = $1
2135             end
2136         end
2137     }
2138
2139     w.add(vb)
2140     w.signal_connect('delete-event') { w.destroy }
2141     w.signal_connect('destroy') {
2142         Thread.kill(refresh_thread)
2143         gtk_thread_flush  #- needed because we're about to destroy widgets in w, for which they may be some pending gtk calls
2144         if infopipe_path
2145             infopipe.close
2146             File.delete(infopipe_path)
2147         end
2148         set_mainwindow_title(nil)
2149     }
2150     w.window_position = Gtk::Window::POS_CENTER
2151     w.show_all
2152
2153     return [ b, w ]
2154 end
2155
2156 def call_backend(cmd, waitmsg, mode, params)
2157     pipe = Tempfile.new("boohpipe")
2158     Thread.critical = true
2159     path = pipe.path
2160     pipe.close!
2161     system("mkfifo #{path}")
2162     Thread.critical = false
2163     cmd += " --info-pipe #{path}"
2164     button, w8 = backend_wait_message($main_window, waitmsg, path, mode)
2165     pid = nil
2166     Thread.new {
2167         msg 2, cmd
2168         if pid = fork
2169             id, exitstatus = Process.waitpid2(pid)
2170             gtk_thread_protect { w8.destroy }
2171             if exitstatus == 0
2172                 if params[:successmsg]
2173                     gtk_thread_protect { show_popup($main_window, params[:successmsg], { :linkurl => params[:successmsg_linkurl] }) }
2174                 end
2175                 if params[:closure_after]
2176                     gtk_thread_protect(&params[:closure_after])
2177                 end
2178             elsif exitstatus == 15
2179                 #- say nothing, user aborted
2180             else
2181                 gtk_thread_protect { show_popup($main_window,
2182                                                 utf8(_("There was something wrong, sorry:\n\n%s") % $diemsg)) }
2183             end
2184         else
2185             exec(cmd)
2186         end
2187     }
2188     button.signal_connect('clicked') {
2189         Process.kill('SIGTERM', pid)
2190     }
2191 end
2192
2193 def save_changes(*forced)
2194     if forced.empty? && (!$current_path || !$undo_tb.sensitive?)
2195         return
2196     end
2197
2198     $xmldir.delete_attribute('already-generated')
2199
2200     propagate_children = proc { |xmldir|
2201         if xmldir.attributes['subdirs-caption']
2202             xmldir.delete_attribute('already-generated')
2203         end
2204         xmldir.elements.each('dir') { |element|
2205             propagate_children.call(element)
2206         }
2207     }
2208
2209     if $xmldir.child_byname_notattr('dir', 'deleted')
2210         new_title = $subalbums_title.buffer.text
2211         if new_title != $xmldir.attributes['subdirs-caption']
2212             parent = $xmldir.parent
2213             if parent.name == 'dir'
2214                 parent.delete_attribute('already-generated')
2215             end
2216             propagate_children.call($xmldir)
2217         end
2218         $xmldir.add_attribute('subdirs-caption', new_title)
2219         $xmldir.elements.each('dir') { |element|
2220             if !element.attributes['deleted']
2221                 path = element.attributes['path']
2222                 newtext = $subalbums_edits[path][:editzone].buffer.text
2223                 if element.attributes['subdirs-caption']
2224                     if element.attributes['subdirs-caption'] != newtext
2225                         propagate_children.call(element)
2226                     end
2227                     element.add_attribute('subdirs-caption',     newtext)
2228                     element.add_attribute('subdirs-captionfile', utf8($subalbums_edits[path][:captionfile]))
2229                 else
2230                     if element.attributes['thumbnails-caption'] != newtext
2231                         element.delete_attribute('already-generated')
2232                     end
2233                     element.add_attribute('thumbnails-caption',     newtext)
2234                     element.add_attribute('thumbnails-captionfile', utf8($subalbums_edits[path][:captionfile]))
2235                 end
2236             end
2237         }
2238     end
2239
2240     if $notebook.page == 0 && $xmldir.child_byname_notattr('dir', 'deleted')
2241         if $xmldir.attributes['thumbnails-caption']
2242             path = $xmldir.attributes['path']
2243             $xmldir.add_attribute('thumbnails-caption', $subalbums_edits[path][:editzone].buffer.text)
2244         end
2245     elsif $xmldir.attributes['thumbnails-caption']
2246         $xmldir.add_attribute('thumbnails-caption', $thumbnails_title.buffer.text)
2247     end
2248
2249     if $xmldir.attributes['thumbnails-caption']
2250         if edit = $subalbums_edits[$xmldir.attributes['path']]
2251             $xmldir.add_attribute('thumbnails-captionfile', edit[:captionfile])
2252         end
2253     end
2254
2255     #- remove and reinsert elements to reflect new ordering
2256     saves = {}
2257     cpt = 0
2258     $xmldir.elements.each { |element|
2259         if element.name == 'image' || element.name == 'video'
2260             saves[element.attributes['filename']] = element.remove
2261             cpt += 1
2262         end
2263     }
2264     $autotable.current_order.each { |path|
2265         chld = $xmldir.add_element(saves[path].name, saves[path].attributes)
2266         chld.add_attribute('caption', $name2widgets[File.basename(path)][:textview].buffer.text)
2267         saves.delete(path)
2268     }
2269     saves.each_key { |path|
2270         chld = $xmldir.add_element(saves[path].name, saves[path].attributes)
2271         chld.add_attribute('deleted', 'true')
2272     }
2273 end
2274
2275 def sort_by_exif_date
2276     $modified = true
2277     save_changes
2278     current_order = []
2279     rexml_thread_protect {
2280         $xmldir.elements.each { |element|
2281             if element.name == 'image' || element.name == 'video'
2282                 current_order << element.attributes['filename']
2283             end
2284         }
2285     }
2286
2287     #- look for EXIF dates
2288     dates = {}
2289
2290     if current_order.size > 20
2291         w = create_window
2292         w.set_transient_for($main_window)
2293         w.modal = true
2294         vb = Gtk::VBox.new(false, 5).set_border_width(5)
2295         vb.pack_start(Gtk::Label.new(utf8(_("Scanning sub-album looking for EXIF dates..."))), false, false)
2296         vb.pack_start(pb = Gtk::ProgressBar.new, false, false)
2297         bottom = Gtk::Alignment.new(0.5, 0.5, 0, 0).add(b = Gtk::Button.new(utf8(_("_Abort"))))
2298         b.image = Gtk::Image.new("#{$FPATH}/images/stock-close-24.png")
2299         vb.pack_end(bottom, false, false)
2300         w.add(vb)
2301         w.signal_connect('delete-event') { w.destroy }
2302         w.window_position = Gtk::Window::POS_CENTER
2303         w.show_all
2304
2305         aborted = false
2306         b.signal_connect('clicked') { aborted = true }
2307         i = 0
2308         current_order.each { |f|
2309             i += 1
2310             if entry2type(f) == 'image'
2311                 pb.text = f
2312                 pb.fraction = i.to_f / current_order.size
2313                 Gtk.main_iteration while Gtk.events_pending?
2314                 date_time = Exif.datetimeoriginal(from_utf8($current_path + "/" + f))
2315                 if ! date_time.nil?
2316                     dates[f] = date_time
2317                 end
2318             end
2319             if aborted
2320                 break
2321             end
2322         }
2323         w.destroy
2324         if aborted
2325             return
2326         end
2327
2328     else
2329         current_order.each { |f|
2330             date_time = Exif.datetimeoriginal(from_utf8($current_path + "/" + f))
2331             if ! date_time.nil?
2332                 dates[f] = date_time
2333             end
2334         }
2335     end
2336
2337     saves = {}
2338     rexml_thread_protect {
2339         $xmldir.elements.each { |element|
2340             if element.name == 'image' || element.name == 'video'
2341                 saves[element.attributes['filename']] = element.remove
2342             end
2343         }
2344     }
2345
2346     neworder = smartsort(current_order, dates)
2347
2348     rexml_thread_protect {
2349         neworder.each { |f|
2350             $xmldir.add_element(saves[f].name, saves[f].attributes)
2351         }
2352     }
2353
2354     #- let the auto-table reflect new ordering
2355     change_dir
2356 end
2357
2358 def remove_all_captions
2359     $modified = true
2360     texts = {}
2361     $autotable.current_order.each { |path|
2362         texts[File.basename(path) ] = $name2widgets[File.basename(path)][:textview].buffer.text
2363         $name2widgets[File.basename(path)][:textview].buffer.text = ''
2364     }
2365     save_undo(_("remove all captions"),
2366               proc { |texts|
2367                   texts.each_key { |key|
2368                       $name2widgets[key][:textview].buffer.text = texts[key]
2369                   }
2370                   $notebook.set_page(1)
2371                   proc {
2372                       texts.each_key { |key|
2373                           $name2widgets[key][:textview].buffer.text = ''
2374                       }
2375                       $notebook.set_page(1)
2376                   }
2377               }, texts)
2378 end
2379
2380 def change_dir
2381     $selected_elements.each_key { |path|
2382         $name2widgets[path][:img].pixbuf = $selected_elements[path][:pixbuf]
2383     }
2384     $autotable.clear
2385     $vbox2widgets = {}
2386     $name2widgets = {}
2387     $name2closures = {}
2388     $selected_elements = {}
2389     $cuts = []
2390     $multiple_dnd = []
2391     UndoHandler.cleanup
2392     $undo_tb.sensitive = $undo_mb.sensitive = false
2393     $redo_tb.sensitive = $redo_mb.sensitive = false
2394
2395     if !$current_path
2396         return
2397     end
2398
2399     $subalbums_vb.children.each { |chld|
2400         $subalbums_vb.remove(chld)
2401     }
2402     $subalbums = Gtk::Table.new(0, 0, true)
2403     current_y_sub_albums = 0
2404
2405     $xmldir = $xmldoc.elements["//dir[@path='#{$current_path}']"]
2406     $subalbums_edits = {}
2407     subalbums_counter = 0
2408     subalbums_edits_bypos = {}
2409
2410     add_subalbum = proc { |xmldir, counter|
2411         $subalbums_edits[xmldir.attributes['path']] = { :position => counter }
2412         subalbums_edits_bypos[counter] = $subalbums_edits[xmldir.attributes['path']]
2413         if xmldir == $xmldir
2414             thumbnail_file = "#{current_dest_dir}/thumbnails-thumbnail.jpg"
2415             captionfile = from_utf8(xmldir.attributes['thumbnails-captionfile'])
2416             caption = xmldir.attributes['thumbnails-caption']
2417             infotype = 'thumbnails'
2418         else
2419             thumbnail_file = "#{current_dest_dir}/thumbnails-#{make_dest_filename(from_utf8(File.basename(xmldir.attributes['path'])))}.jpg"
2420             captionfile, caption = find_subalbum_caption_info(xmldir)
2421             infotype = find_subalbum_info_type(xmldir)
2422         end
2423         msg 3, "add subdir: #{xmldir.attributes['path']} with file: #{thumbnail_file}"
2424         hbox = Gtk::HBox.new
2425         hbox.pack_start(Gtk::Alignment.new(1, 0.5, 0, 0).add(Gtk::Label.new.set_markup('<i>' + File.basename(xmldir.attributes['path']) + ':</i>')))
2426         f = Gtk::Frame.new
2427         f.set_shadow_type(Gtk::SHADOW_ETCHED_OUT)
2428
2429         img = nil
2430         my_gen_real_thumbnail = proc {
2431             gen_real_thumbnail('subdir', captionfile, thumbnail_file, xmldir, $albums_thumbnail_size, img, infotype)
2432         }
2433
2434         if !$modified_pixbufs[thumbnail_file] && !File.exists?(thumbnail_file)
2435             f.add(img = Gtk::Image.new)
2436             my_gen_real_thumbnail.call
2437         else
2438             f.add(img = Gtk::Image.new($modified_pixbufs[thumbnail_file] ? $modified_pixbufs[thumbnail_file][:pixbuf] : thumbnail_file))
2439         end
2440         hbox.pack_end(Gtk::Alignment.new(0.5, 0.5, 0, 0).add(evtbox = Gtk::EventBox.new.add(f)), false, false)
2441         $subalbums.attach(hbox,
2442                           0, 1, current_y_sub_albums, current_y_sub_albums + 1, Gtk::FILL, Gtk::FILL, 2, 2)
2443
2444         frame, textview = create_editzone($subalbums_sw, 0, img)
2445         textview.buffer.text = caption
2446         $subalbums.attach(Gtk::Alignment.new(0, 0.5, 0.5, 0).add(frame),
2447                           1, 2, current_y_sub_albums, current_y_sub_albums + 1, Gtk::FILL, Gtk::FILL, 2, 2)
2448
2449         change_image = proc {
2450             fc = Gtk::FileChooserDialog.new(utf8(_("Select image for caption")),
2451                                             nil,
2452                                             Gtk::FileChooser::ACTION_OPEN,
2453                                             nil,
2454                                             [Gtk::Stock::OK, Gtk::Dialog::RESPONSE_ACCEPT], [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
2455             fc.set_current_folder(from_utf8(xmldir.attributes['path']))
2456             fc.transient_for = $main_window
2457             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))
2458             f.add(preview_img = Gtk::Image.new)
2459             preview.show_all
2460             fc.signal_connect('update-preview') { |w|
2461                 begin
2462                     if fc.preview_filename
2463                         preview_img.pixbuf = rotate_pixbuf(Gdk::Pixbuf.new(fc.preview_filename, 240, 180), guess_rotate(fc.preview_filename))
2464                         fc.preview_widget_active = true
2465                     end
2466                 rescue Gdk::PixbufError
2467                     fc.preview_widget_active = false
2468                 end
2469             }
2470             if fc.run == Gtk::Dialog::RESPONSE_ACCEPT
2471                 $modified = true
2472                 old_file = captionfile
2473                 old_rotate = xmldir.attributes["#{infotype}-rotate"]
2474                 old_color_swap = xmldir.attributes["#{infotype}-color-swap"]
2475                 old_enhance = xmldir.attributes["#{infotype}-enhance"]
2476                 old_seektime = xmldir.attributes["#{infotype}-seektime"]
2477
2478                 new_file = fc.filename
2479                 msg 3, "new captionfile is: #{fc.filename}"
2480                 perform_changefile = proc {
2481                     $subalbums_edits[xmldir.attributes['path']][:captionfile] = captionfile = new_file
2482                     $modified_pixbufs.delete(thumbnail_file)
2483                     xmldir.delete_attribute("#{infotype}-rotate")
2484                     xmldir.delete_attribute("#{infotype}-color-swap")
2485                     xmldir.delete_attribute("#{infotype}-enhance")
2486                     xmldir.delete_attribute("#{infotype}-seektime")
2487                     my_gen_real_thumbnail.call
2488                 }
2489                 perform_changefile.call
2490
2491                 save_undo(_("change caption file for sub-album"),
2492                           proc {
2493                               $subalbums_edits[xmldir.attributes['path']][:captionfile] = captionfile = old_file
2494                               xmldir.add_attribute("#{infotype}-rotate", old_rotate)
2495                               xmldir.add_attribute("#{infotype}-color-swap", old_color_swap)
2496                               xmldir.add_attribute("#{infotype}-enhance", old_enhance)
2497                               xmldir.add_attribute("#{infotype}-seektime", old_seektime)
2498                               my_gen_real_thumbnail.call
2499                               $notebook.set_page(0)
2500                               proc {
2501                                   perform_changefile.call
2502                                   $notebook.set_page(0)
2503                               }
2504                           })
2505             end
2506             fc.destroy
2507         }
2508
2509         refresh = proc {
2510             if File.exists?(thumbnail_file)
2511                 File.delete(thumbnail_file)
2512             end
2513             my_gen_real_thumbnail.call
2514         }
2515
2516         rotate_and_cleanup = proc { |angle|
2517             rotate(angle, thumbnail_file, img, xmldir, "#{infotype}-", $default_albums_thumbnails[:x], $default_albums_thumbnails[:y])
2518             if File.exists?(thumbnail_file)
2519                 File.delete(thumbnail_file)
2520             end
2521         }
2522
2523         move = proc { |direction|
2524             $modified = true
2525
2526             save_changes('forced')
2527             oldpos = $subalbums_edits[xmldir.attributes['path']][:position]
2528             if direction == 'up'
2529                 $subalbums_edits[xmldir.attributes['path']][:position] -= 1
2530                 subalbums_edits_bypos[oldpos - 1][:position] += 1
2531             end
2532             if direction == 'down'
2533                 $subalbums_edits[xmldir.attributes['path']][:position] += 1
2534                 subalbums_edits_bypos[oldpos + 1][:position] -= 1
2535             end
2536             if direction == 'top'
2537                 for i in 1 .. oldpos - 1
2538                     subalbums_edits_bypos[i][:position] += 1
2539                 end
2540                 $subalbums_edits[xmldir.attributes['path']][:position] = 1
2541             end
2542             if direction == 'bottom'
2543                 for i in oldpos + 1 .. subalbums_counter
2544                     subalbums_edits_bypos[i][:position] -= 1
2545                 end
2546                 $subalbums_edits[xmldir.attributes['path']][:position] = subalbums_counter
2547             end
2548
2549             elems = []
2550             $xmldir.elements.each('dir') { |element|
2551                 if (!element.attributes['deleted'])
2552                     elems << [ element.attributes['path'], element.remove ]
2553                 end
2554             }
2555             elems.sort { |a,b| $subalbums_edits[a[0]][:position] <=> $subalbums_edits[b[0]][:position] }.
2556                   each { |e| $xmldir.add_element(e[1]) }
2557             #- need to remove the already-generated tag to all subdirs because of "previous/next albums" links completely changed
2558             $xmldir.elements.each('descendant::dir') { |elem|
2559                 elem.delete_attribute('already-generated')
2560             }
2561
2562             sel = $albums_tv.selection.selected_rows
2563             change_dir
2564             populate_subalbums_treeview(false)
2565             $albums_tv.selection.select_path(sel[0])
2566         }
2567
2568         color_swap_and_cleanup = proc {
2569             perform_color_swap_and_cleanup = proc {
2570                 color_swap(xmldir, "#{infotype}-")
2571                 my_gen_real_thumbnail.call
2572             }
2573             perform_color_swap_and_cleanup.call
2574
2575             save_undo(_("color swap"),
2576                       proc {
2577                           perform_color_swap_and_cleanup.call
2578                           $notebook.set_page(0)
2579                           proc {
2580                               perform_color_swap_and_cleanup.call
2581                               $notebook.set_page(0)
2582                           }
2583                       })
2584         }
2585
2586         change_seektime_and_cleanup = proc {
2587             if values = ask_new_seektime(xmldir, "#{infotype}-")
2588                 perform_change_seektime_and_cleanup = proc { |val|
2589                     change_seektime(xmldir, "#{infotype}-", val)
2590                     my_gen_real_thumbnail.call
2591                 }
2592                 perform_change_seektime_and_cleanup.call(values[:new])
2593
2594                 save_undo(_("specify seektime"),
2595                           proc {
2596                               perform_change_seektime_and_cleanup.call(values[:old])
2597                               $notebook.set_page(0)
2598                               proc {
2599                                   perform_change_seektime_and_cleanup.call(values[:new])
2600                                   $notebook.set_page(0)
2601                               }
2602                           })
2603             end
2604         }
2605
2606         whitebalance_and_cleanup = proc {
2607             if values = ask_whitebalance(captionfile, thumbnail_file, img, xmldir, "#{infotype}-",
2608                                          $default_albums_thumbnails[:x], $default_albums_thumbnails[:y], infotype)
2609                 perform_change_whitebalance_and_cleanup = proc { |val|
2610                     change_whitebalance(xmldir, "#{infotype}-", val)
2611                     recalc_whitebalance(val, captionfile, thumbnail_file, img, xmldir, "#{infotype}-",
2612                                         $default_albums_thumbnails[:x], $default_albums_thumbnails[:y], infotype)
2613                     if File.exists?(thumbnail_file)
2614                         File.delete(thumbnail_file)
2615                     end
2616                 }
2617                 perform_change_whitebalance_and_cleanup.call(values[:new])
2618                 
2619                 save_undo(_("fix white balance"),
2620                           proc {
2621                               perform_change_whitebalance_and_cleanup.call(values[:old])
2622                               $notebook.set_page(0)
2623                               proc {
2624                                   perform_change_whitebalance_and_cleanup.call(values[:new])
2625                                   $notebook.set_page(0)
2626                               }
2627                           })
2628             end
2629         }
2630
2631         gammacorrect_and_cleanup = proc {
2632             if values = ask_gammacorrect(captionfile, thumbnail_file, img, xmldir, "#{infotype}-",
2633                                          $default_albums_thumbnails[:x], $default_albums_thumbnails[:y], infotype)
2634                 perform_change_gammacorrect_and_cleanup = proc { |val|
2635                     change_gammacorrect(xmldir, "#{infotype}-", val)
2636                     recalc_gammacorrect(val, captionfile, thumbnail_file, img, xmldir, "#{infotype}-",
2637                                         $default_albums_thumbnails[:x], $default_albums_thumbnails[:y], infotype)
2638                     if File.exists?(thumbnail_file)
2639                         File.delete(thumbnail_file)
2640                     end
2641                 }
2642                 perform_change_gammacorrect_and_cleanup.call(values[:new])
2643                 
2644                 save_undo(_("gamma correction"),
2645                           proc {
2646                               perform_change_gammacorrect_and_cleanup.call(values[:old])
2647                               $notebook.set_page(0)
2648                               proc {
2649                                   perform_change_gammacorrect_and_cleanup.call(values[:new])
2650                                   $notebook.set_page(0)
2651                               }
2652                           })
2653             end
2654         }
2655
2656         enhance_and_cleanup = proc {
2657             perform_enhance_and_cleanup = proc {
2658                 enhance(xmldir, "#{infotype}-")
2659                 my_gen_real_thumbnail.call
2660             }
2661             
2662             perform_enhance_and_cleanup.call
2663             
2664             save_undo(_("enhance"),
2665                       proc {
2666                           perform_enhance_and_cleanup.call
2667                           $notebook.set_page(0)
2668                           proc {
2669                               perform_enhance_and_cleanup.call
2670                               $notebook.set_page(0)
2671                           }
2672                       })
2673         }
2674
2675         evtbox.signal_connect('button-press-event') { |w, event|
2676             if event.event_type == Gdk::Event::BUTTON_PRESS && event.button == 1
2677                 if $r90.active?
2678                     rotate_and_cleanup.call(90)
2679                 elsif $r270.active?
2680                     rotate_and_cleanup.call(-90)
2681                 elsif $enhance.active?
2682                     enhance_and_cleanup.call
2683                 end
2684             end
2685             if event.event_type == Gdk::Event::BUTTON_PRESS && event.button == 3
2686                 popup_thumbnail_menu(event, ['change_image', 'move_top', 'move_bottom'], captionfile, entry2type(captionfile), xmldir, "#{infotype}-",
2687                                      { :forbid_left => true, :forbid_right => true,
2688                                        :can_up => counter > 1, :can_down => counter > 0 && counter < subalbums_counter,
2689                                        :can_top => counter > 1, :can_bottom => counter > 0 && counter < subalbums_counter },
2690                                      { :change => change_image, :move => move, :rotate => rotate_and_cleanup, :enhance => enhance_and_cleanup,
2691                                        :color_swap => color_swap_and_cleanup, :seektime => change_seektime_and_cleanup, :whitebalance => whitebalance_and_cleanup,
2692                                        :gammacorrect => gammacorrect_and_cleanup, :refresh => refresh })
2693             end
2694             if event.event_type == Gdk::Event::BUTTON2_PRESS && event.button == 1
2695                 change_image.call
2696                 true   #- handled
2697             end
2698         }
2699         evtbox.signal_connect('button-press-event') { |w, event|
2700             $gesture_press = { :filename => thumbnail_file, :x => event.x, :y => event.y }
2701             false
2702         }
2703
2704         evtbox.signal_connect('button-release-event') { |w, event|
2705             if !$r90.active? && !$r270.active? && $gesture_press && $gesture_press[:filename] == thumbnail_file
2706                 msg 3, "press: #{$gesture_press[:x]} release: #{event.x}"
2707                 if (($gesture_press[:x]-event.x)/($gesture_press[:y]-event.y)).abs > 2 && ($gesture_press[:x]-event.x).abs > 5
2708                     angle = event.x-$gesture_press[:x] > 0 ? 90 : -90
2709                     msg 3, "gesture rotate: #{angle}"
2710                     rotate_and_cleanup.call(angle)
2711                 end
2712             end
2713             $gesture_press = nil
2714         }
2715                 
2716         $subalbums_edits[xmldir.attributes['path']][:editzone] = textview
2717         $subalbums_edits[xmldir.attributes['path']][:captionfile] = captionfile
2718         current_y_sub_albums += 1
2719     }
2720
2721     if $xmldir.child_byname_notattr('dir', 'deleted')
2722         #- title edition
2723         frame, $subalbums_title = create_editzone($subalbums_sw, 0, nil)
2724         $subalbums_title.buffer.text = $xmldir.attributes['subdirs-caption'] || ''
2725         $subalbums_title.set_justification(Gtk::Justification::CENTER)
2726         $subalbums_vb.pack_start(Gtk::Alignment.new(0.5, 0.5, 0.5, 0).add(frame), false, false)
2727         #- this album image/caption
2728         if $xmldir.attributes['thumbnails-caption']
2729             add_subalbum.call($xmldir, 0)
2730         end
2731     end
2732     total = { 'image' => 0, 'video' => 0, 'dir' => 0 }
2733     $xmldir.elements.each { |element|
2734         if (element.name == 'image' || element.name == 'video') && !element.attributes['deleted']
2735             #- element (image or video) of this album
2736             dest_img = build_full_dest_filename(element.attributes['filename']).sub(/\.[^\.]+$/, '') + "-#{$default_size['thumbnails']}.jpg"
2737             msg 3, "dest_img: #{dest_img}"
2738             add_thumbnail($autotable, element.attributes['filename'], element.name, dest_img, element.attributes['caption'])
2739             total[element.name] += 1
2740         end
2741         if element.name == 'dir' && !element.attributes['deleted']
2742             #- sub-album image/caption
2743             add_subalbum.call(element, subalbums_counter += 1)
2744             total[element.name] += 1
2745         end
2746     }
2747     $statusbar.push(0, utf8(_("%s: %s photos and %s videos, %s sub-albums") % [ File.basename(from_utf8($xmldir.attributes['path'])),
2748                                                                                 total['image'], total['video'], total['dir'] ]))
2749     $subalbums_vb.add($subalbums)
2750     $subalbums_vb.show_all
2751
2752     if !$xmldir.child_byname_notattr('image', 'deleted') && !$xmldir.child_byname_notattr('video', 'deleted')
2753         $notebook.get_tab_label($autotable_sw).sensitive = false
2754         $notebook.set_page(0)
2755         $thumbnails_title.buffer.text = ''
2756     else
2757         $notebook.get_tab_label($autotable_sw).sensitive = true
2758         $thumbnails_title.buffer.text = $xmldir.attributes['thumbnails-caption']
2759     end
2760
2761     if !$xmldir.child_byname_notattr('dir', 'deleted')
2762         $notebook.get_tab_label($subalbums_sw).sensitive = false
2763         $notebook.set_page(1)
2764     else
2765         $notebook.get_tab_label($subalbums_sw).sensitive = true
2766     end
2767 end
2768
2769 def pixbuf_or_nil(filename)
2770     begin
2771         return Gdk::Pixbuf.new(filename)
2772     rescue
2773         return nil
2774     end
2775 end
2776
2777 def theme_choose(current)
2778     dialog = Gtk::Dialog.new(utf8(_("Select your preferred theme")),
2779                              $main_window,
2780                              Gtk::Dialog::MODAL | Gtk::Dialog::DESTROY_WITH_PARENT,
2781                              [Gtk::Stock::OK, Gtk::Dialog::RESPONSE_OK],
2782                              [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
2783
2784     model = Gtk::ListStore.new(String, Gdk::Pixbuf, Gdk::Pixbuf, Gdk::Pixbuf)
2785     treeview = Gtk::TreeView.new(model).set_rules_hint(true)
2786     treeview.append_column(Gtk::TreeViewColumn.new(utf8(_("Theme name")), Gtk::CellRendererText.new, { :text => 0 }).set_alignment(0.5).set_spacing(5))
2787     treeview.append_column(Gtk::TreeViewColumn.new(utf8(_("Sub-albums page look")), Gtk::CellRendererPixbuf.new, { :pixbuf => 1 }).set_alignment(0.5).set_spacing(5))
2788     treeview.append_column(Gtk::TreeViewColumn.new(utf8(_("Thumbnails page look")), Gtk::CellRendererPixbuf.new, { :pixbuf => 2 }).set_alignment(0.5).set_spacing(5))
2789     treeview.append_column(Gtk::TreeViewColumn.new(utf8(_("Fullscreen page look")), Gtk::CellRendererPixbuf.new, { :pixbuf => 3 }).set_alignment(0.5).set_spacing(5))
2790     treeview.append_column(Gtk::TreeViewColumn.new('', Gtk::CellRendererText.new, {}))
2791     treeview.signal_connect('button-press-event') { |w, event|
2792         if event.event_type == Gdk::Event::BUTTON2_PRESS && event.button == 1
2793             dialog.response(Gtk::Dialog::RESPONSE_OK)
2794         end
2795     }
2796
2797     dialog.vbox.add(sw = Gtk::ScrolledWindow.new(nil, nil).add(treeview).set_policy(Gtk::POLICY_NEVER, Gtk::POLICY_AUTOMATIC))
2798
2799     ([ $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|
2800         dir.chomp!
2801         iter = model.append
2802         iter[0] = File.basename(dir)
2803         iter[1] = pixbuf_or_nil("#{dir}/metadata/screenshot-1.png")
2804         iter[2] = pixbuf_or_nil("#{dir}/metadata/screenshot-2.png")
2805         iter[3] = pixbuf_or_nil("#{dir}/metadata/screenshot-3.png")
2806         if File.basename(dir) == current
2807             treeview.selection.select_iter(iter)
2808         end
2809     }
2810     dialog.set_default_size(-1, 500)
2811     dialog.vbox.show_all
2812
2813     dialog.run { |response|
2814         iter = treeview.selection.selected
2815         dialog.destroy
2816         if response == Gtk::Dialog::RESPONSE_OK && iter
2817             return model.get_value(iter, 0)
2818         end
2819     }
2820     return nil
2821 end
2822
2823 def show_password_protections
2824     examine_dir_elem = proc { |parent_iter, xmldir, already_protected|
2825         child_iter = $albums_iters[xmldir.attributes['path']]
2826         if xmldir.attributes['password-protect']
2827             child_iter[2] = pixbuf_or_nil("#{$FPATH}/images/galeon-secure.png")
2828             already_protected = true
2829         elsif already_protected
2830             pix = pixbuf_or_nil("#{$FPATH}/images/galeon-secure-shadow.png")
2831             if pix
2832                 pix = pix.saturate_and_pixelate(1, true)
2833             end
2834             child_iter[2] = pix
2835         else
2836             child_iter[2] = nil
2837         end
2838         xmldir.elements.each('dir') { |elem|
2839             if !elem.attributes['deleted']
2840                 examine_dir_elem.call(child_iter, elem, already_protected)
2841             end
2842         }
2843     }
2844     examine_dir_elem.call(nil, $xmldoc.elements['//dir'], false)
2845 end
2846
2847 def populate_subalbums_treeview(select_first)
2848     $albums_ts.clear
2849     $autotable.clear
2850     $albums_iters = {}
2851     $subalbums_vb.children.each { |chld|
2852         $subalbums_vb.remove(chld)
2853     }
2854
2855     source = $xmldoc.root.attributes['source']
2856     msg 3, "source: #{source}"
2857
2858     xmldir = $xmldoc.elements['//dir']
2859     if !xmldir || xmldir.attributes['path'] != source
2860         msg 1, _("Corrupted booh file...")
2861         return
2862     end
2863
2864     append_dir_elem = proc { |parent_iter, xmldir|
2865         child_iter = $albums_ts.append(parent_iter)
2866         child_iter[0] = File.basename(xmldir.attributes['path'])
2867         child_iter[1] = xmldir.attributes['path']
2868         $albums_iters[xmldir.attributes['path']] = child_iter
2869         msg 3, "puttin location: #{xmldir.attributes['path']}"
2870         xmldir.elements.each('dir') { |elem|
2871             if !elem.attributes['deleted']
2872                 append_dir_elem.call(child_iter, elem)
2873             end
2874         }
2875     }
2876     append_dir_elem.call(nil, xmldir)
2877     show_password_protections
2878
2879     $albums_tv.expand_all
2880     if select_first
2881         $albums_tv.selection.select_iter($albums_ts.iter_first)
2882     end
2883 end
2884
2885 def select_current_theme
2886     select_theme($xmldoc.root.attributes['theme'],
2887                  $xmldoc.root.attributes['limit-sizes'],
2888                  !$xmldoc.root.attributes['optimize-for-32'].nil?,
2889                  $xmldoc.root.attributes['thumbnails-per-row'])
2890 end
2891
2892 def open_file(filename)
2893
2894     $filename = nil
2895     $modified = false
2896     $current_path = nil   #- invalidate
2897     $modified_pixbufs = {}
2898     $albums_ts.clear
2899     $autotable.clear
2900     $subalbums_vb.children.each { |chld|
2901         $subalbums_vb.remove(chld)
2902     }
2903
2904     if !File.exists?(filename)
2905         return utf8(_("File not found."))
2906     end
2907
2908     begin
2909         $xmldoc = REXML::Document.new(File.new(filename))
2910     rescue Exception
2911         $xmldoc = nil
2912     end
2913
2914     if !$xmldoc || !$xmldoc.root || $xmldoc.root.name != 'booh'
2915         if entry2type(filename).nil?
2916             return utf8(_("Not a booh file!"))
2917         else
2918             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."))
2919         end
2920     end
2921
2922     if !source = $xmldoc.root.attributes['source']
2923         return utf8(_("Corrupted booh file..."))
2924     end
2925
2926     if !dest = $xmldoc.root.attributes['destination']
2927         return utf8(_("Corrupted booh file..."))
2928     end
2929
2930     if !theme = $xmldoc.root.attributes['theme']
2931         return utf8(_("Corrupted booh file..."))
2932     end
2933
2934     if $xmldoc.root.attributes['version'] < '0.9.0'
2935         msg 2, _("File's version %s, booh version now %s, marking dirty") % [ $xmldoc.root.attributes['version'], $VERSION ]
2936         mark_document_as_dirty
2937         if $xmldoc.root.attributes['version'] < '0.8.4'
2938             msg 1, _("File's version prior to 0.8.4, migrating directories and filenames in destination directory if needed")
2939             `find '#{source}' -type d -follow`.sort.collect { |v| v.chomp }.each { |dir|
2940                 old_dest_dir = make_dest_filename_old(dir.sub(/^#{Regexp.quote(source)}/, dest))
2941                 new_dest_dir = make_dest_filename(dir.sub(/^#{Regexp.quote(source)}/, dest))
2942                 if old_dest_dir != new_dest_dir
2943                     sys("mv '#{old_dest_dir}' '#{new_dest_dir}'")
2944                 end
2945                 if xmldir = $xmldoc.elements["//dir[@path='#{utf8(dir)}']"]
2946                     xmldir.elements.each { |element|
2947                         if %w(image video).include?(element.name) && !element.attributes['deleted']
2948                             old_name = new_dest_dir + '/' + make_dest_filename_old(from_utf8(element.attributes['filename'])).sub(/\.[^\.]+$/, '')
2949                             new_name = new_dest_dir + '/' + make_dest_filename(from_utf8(element.attributes['filename'])).sub(/\.[^\.]+$/, '')
2950                             Dir[old_name + '*'].each { |file|
2951                                 new_file = file.sub(/^#{Regexp.quote(old_name)}/, new_name)
2952                                 file != new_file and sys("mv '#{file}' '#{new_file}'")
2953                             }
2954                         end
2955                         if element.name == 'dir' && !element.attributes['deleted']
2956                             old_name = new_dest_dir + '/thumbnails-' + make_dest_filename_old(from_utf8(File.basename(element.attributes['path']))) + '.jpg'
2957                             new_name = new_dest_dir + '/thumbnails-' + make_dest_filename(from_utf8(File.basename(element.attributes['path']))) + '.jpg'
2958                             old_name != new_name and sys("mv '#{old_name}' '#{new_name}'")
2959                         end
2960                     }
2961                 else
2962                     msg 1, "could not find <dir> element by path '#{utf8(dir)}' in xml file"
2963                 end
2964             }
2965         end
2966         $xmldoc.root.add_attribute('version', $VERSION)
2967     end
2968
2969     select_current_theme
2970
2971     $filename = filename
2972     set_mainwindow_title(nil)
2973     $default_size['thumbnails'] =~ /(.*)x(.*)/
2974     $default_thumbnails = { :x => $1.to_i, :y => $2.to_i }
2975     $albums_thumbnail_size =~ /(.*)x(.*)/
2976     $default_albums_thumbnails = { :x => $1.to_i, :y => $2.to_i }
2977
2978     populate_subalbums_treeview(true)
2979
2980     $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
2981     return nil
2982 end
2983
2984 def open_file_user(filename)
2985     result = open_file(filename)
2986     if !result
2987         $config['last-opens'] ||= []
2988         if $config['last-opens'][-1] != utf8(filename)
2989             $config['last-opens'] << utf8(filename)
2990         end
2991         $orig_filename = $filename
2992         $main_window.title = 'booh - ' + File.basename($orig_filename)
2993         tmp = Tempfile.new("boohtemp")
2994         Thread.critical = true
2995         $filename = tmp.path
2996         tmp.close!
2997         #- for security
2998         ios = File.open($filename, File::RDWR|File::CREAT|File::EXCL)
2999         Thread.critical = false
3000         ios.close
3001         $tempfiles << $filename << "#{$filename}.backup"
3002     else
3003         $orig_filename = nil
3004     end
3005     return result
3006 end
3007
3008 def open_file_popup
3009     if !ask_save_modifications(utf8(_("Save this album?")),
3010                                utf8(_("Do you want to save the changes to this album?")),
3011                                { :yes => Gtk::Stock::YES, :no => Gtk::Stock::NO })
3012         return
3013     end
3014     fc = Gtk::FileChooserDialog.new(utf8(_("Open file")),
3015                                     nil,
3016                                     Gtk::FileChooser::ACTION_OPEN,
3017                                     nil,
3018                                     [Gtk::Stock::OPEN, Gtk::Dialog::RESPONSE_ACCEPT], [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
3019     fc.add_shortcut_folder(File.expand_path("~/.booh"))
3020     fc.set_current_folder(File.expand_path("~/.booh"))
3021     fc.transient_for = $main_window
3022     ok = false
3023     while !ok
3024         if fc.run == Gtk::Dialog::RESPONSE_ACCEPT
3025             push_mousecursor_wait(fc)
3026             msg = open_file_user(fc.filename)
3027             pop_mousecursor(fc)
3028             if msg
3029                 show_popup(fc, msg)
3030                 ok = false
3031             else
3032                 ok = true
3033             end
3034         else
3035             ok = true
3036         end
3037     end
3038     fc.destroy
3039 end
3040
3041 def additional_booh_options
3042     options = ''
3043     if $config['mproc']
3044         options += "--mproc #{$config['mproc'].to_i} "
3045     end
3046     options += "--comments-format '#{$config['comments-format']}' "
3047     if $config['transcode-videos']
3048         options += "--transcode-videos '#{$config['transcode-videos']}' "
3049     end
3050     return options
3051 end
3052
3053 def ask_multi_languages(value)
3054     if ! value.nil?
3055         spl = value.split(',')
3056         value = [ spl[0..-2], spl[-1] ]
3057     end
3058
3059     dialog = Gtk::Dialog.new(utf8(_("Multi-languages support")),
3060                              $main_window,
3061                              Gtk::Dialog::MODAL,
3062                              [Gtk::Stock::OK, Gtk::Dialog::RESPONSE_OK],
3063                              [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
3064
3065     lbl = Gtk::Label.new
3066     lbl.markup = utf8(
3067 _("You can choose to activate <b>multi-languages</b> support for this web-album
3068 (it will work only if you publish your web-album on an Apache web-server). This will
3069 use the MultiViews feature of Apache; the pages will be served according to the
3070 value of the Accept-Language HTTP header sent by the web browsers, so that people
3071 with different languages preferences will be able to browse your web-album with
3072 navigation in their language (if language is available).
3073 "))
3074
3075     dialog.vbox.add(lbl)
3076     dialog.vbox.add(Gtk::Alignment.new(0.5, 0.5, 0, 0).add(Gtk::VBox.new.add(rb_no = Gtk::RadioButton.new(utf8(_("Disabled")))).
3077                                                                          add(Gtk::HBox.new(false, 5).add(rb_yes = Gtk::RadioButton.new(rb_no, utf8(_("Enabled")))).
3078                                                                                                      add(languages = Gtk::Button.new))))
3079
3080     pick_languages = proc {
3081         dialog2 = Gtk::Dialog.new(utf8(_("Pick languages to support")),
3082                                   $main_window,
3083                                   Gtk::Dialog::MODAL,
3084                                   [Gtk::Stock::OK, Gtk::Dialog::RESPONSE_OK],
3085                                   [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
3086
3087         dialog2.vbox.add(Gtk::Alignment.new(0.5, 0.5, 0, 0).add(hb1 = Gtk::HBox.new))
3088         hb1.add(Gtk::Label.new(utf8(_("Select the languages to support:"))))
3089         cbs = []
3090         SUPPORTED_LANGUAGES.each { |lang|
3091             hb1.add(cb = Gtk::CheckButton.new(utf8(langname(lang))))
3092             if ! value.nil? && value[0].include?(lang)
3093                 cb.active = true
3094             end
3095             cbs << [ lang, cb ]
3096         }
3097
3098         dialog2.vbox.add(Gtk::Alignment.new(0.5, 0.5, 0, 0).add(hb2 = Gtk::HBox.new))
3099         hb2.add(Gtk::Label.new(utf8(_("Select the fallback language:"))))
3100         fallback_language = nil
3101         hb2.add(fbl_rb = Gtk::RadioButton.new(utf8(langname(SUPPORTED_LANGUAGES[0]))))
3102         fbl_rb.signal_connect('clicked') { fallback_language = SUPPORTED_LANGUAGES[0] }
3103         if value.nil? || value[1] == SUPPORTED_LANGUAGES[0]
3104             fbl_rb.active = true
3105             fallback_language = SUPPORTED_LANGUAGES[0]
3106         end
3107         SUPPORTED_LANGUAGES[1..-1].each { |lang|
3108             hb2.add(rb = Gtk::RadioButton.new(fbl_rb, utf8(langname(lang))))
3109             rb.signal_connect('clicked') { fallback_language = lang }
3110             if ! value.nil? && value[1] == lang
3111                 rb.active = true
3112             end
3113         }
3114
3115         dialog2.window_position = Gtk::Window::POS_MOUSE
3116         dialog2.show_all
3117
3118         resp = nil
3119         dialog2.run { |response|
3120             resp = response
3121             if resp == Gtk::Dialog::RESPONSE_OK
3122                 value = []
3123                 value[0] = cbs.find_all { |e| e[1].active? }.collect { |e| e[0] }
3124                 value[1] = fallback_language
3125                 languages.label = utf8(_("Languages: %s. Fallback: %s.") % [ value[0].collect { |v| langname(v) }.join(', '), langname(value[1]) ])
3126             end
3127             dialog2.destroy
3128         }
3129         resp
3130     }
3131
3132     languages.signal_connect('clicked') {
3133         pick_languages.call
3134     }
3135     dialog.window_position = Gtk::Window::POS_MOUSE
3136     if value.nil?
3137         rb_no.active = true
3138     else
3139         rb_yes.active = true
3140         languages.label = utf8(_("Languages: %s. Fallback: %s.") % [ value[0].collect { |v| langname(v) }.join(', '), langname(value[1]) ])
3141     end
3142     rb_no.signal_connect('clicked') {
3143         if rb_no.active?
3144             languages.hide
3145         else
3146             if pick_languages.call == Gtk::Dialog::RESPONSE_CANCEL
3147                 rb_no.activate
3148             else
3149                 languages.show
3150             end
3151         end
3152     }
3153     oldval = value
3154     dialog.show_all
3155     if rb_no.active?
3156         languages.hide
3157     end
3158
3159     dialog.run { |response|
3160         if rb_no.active?
3161             value = nil
3162         end
3163         dialog.destroy
3164         if response == Gtk::Dialog::RESPONSE_OK && value != oldval
3165             if value.nil?
3166                 return [ true, nil ]
3167             else
3168                 return [ true, (value[0].size == 0 ? '' : value[0].join(',') + ',') + value[1] ]
3169             end
3170         else
3171             return [ false ]
3172         end
3173     }
3174 end
3175
3176 def new_album
3177     if !ask_save_modifications(utf8(_("Save this album?")),
3178                                utf8(_("Do you want to save the changes to this album?")),
3179                                { :yes => Gtk::Stock::YES, :no => Gtk::Stock::NO })
3180         return
3181     end
3182     dialog = Gtk::Dialog.new(utf8(_("Create a new album")),
3183                              $main_window,
3184                              Gtk::Dialog::MODAL | Gtk::Dialog::DESTROY_WITH_PARENT,
3185                              [Gtk::Stock::OK, Gtk::Dialog::RESPONSE_OK],
3186                              [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
3187     
3188     frame1 = Gtk::Frame.new(utf8(_("Locations"))).add(tbl = Gtk::Table.new(0, 0, false))
3189     tbl.attach(Gtk::Label.new(utf8(_("Directory of photos/videos: "))),
3190                0, 1, 0, 1, Gtk::SHRINK, Gtk::SHRINK, 2, 2)
3191     tbl.attach(src = Gtk::Entry.new,
3192                1, 2, 0, 1, Gtk::FILL, Gtk::SHRINK, 2, 2)
3193     tbl.attach(src_browse = Gtk::Button.new(utf8(_("browse..."))),
3194                2, 3, 0, 1, Gtk::SHRINK, Gtk::SHRINK, 2, 2)
3195     tbl.attach(Gtk::Label.new.set_markup(utf8(_("<span size='small'><i>number of photos/videos down this directory:</i></span> "))),
3196                0, 1, 1, 2, Gtk::SHRINK, Gtk::SHRINK, 2, 2)
3197     tbl.attach(src_nb = Gtk::Label.new.set_markup(utf8(_("<span size='small'><i>N/A</i></span>"))),
3198                1, 2, 1, 2, Gtk::FILL, Gtk::SHRINK, 2, 2)
3199     tbl.attach(Gtk::Label.new(utf8(_("Directory where to put the web-album: "))),
3200                0, 1, 2, 3, Gtk::SHRINK, Gtk::SHRINK, 2, 2)
3201     tbl.attach(dest = Gtk::Entry.new,
3202                1, 2, 2, 3, Gtk::FILL, Gtk::SHRINK, 2, 2)
3203     tbl.attach(dest_browse = Gtk::Button.new(utf8(_("browse..."))),
3204                2, 3, 2, 3, Gtk::SHRINK, Gtk::SHRINK, 2, 2)
3205     tbl.attach(Gtk::Label.new(utf8(_("Filename to store this album's properties: "))),
3206                0, 1, 3, 4, Gtk::SHRINK, Gtk::SHRINK, 2, 2)
3207     tbl.attach(conf = Gtk::Entry.new.set_size_request(250, -1),
3208                1, 2, 3, 4, Gtk::FILL, Gtk::SHRINK, 2, 2)
3209     tbl.attach(conf_browse = Gtk::Button.new(utf8(_("browse..."))),
3210                2, 3, 3, 4, Gtk::SHRINK, Gtk::SHRINK, 2, 2)
3211
3212     tooltips = Gtk::Tooltips.new
3213     frame2 = Gtk::Frame.new(utf8(_("Configuration"))).add(vb = Gtk::VBox.new)
3214     vb.add(Gtk::HBox.new(false, 3).pack_start(Gtk::Label.new(utf8(_("Theme: "))), false, false, 0).
3215                          pack_start(theme_button = Gtk::Button.new($config['default-theme'] || 'simple'), false, false, 0))
3216     vb.add(Gtk::HBox.new(false, 3).pack_start(Gtk::Label.new(utf8(_("Sizes of images to generate: "))), false, false, 0).
3217                                    pack_start(sizes = Gtk::HBox.new, false, false, 0))
3218     vb.add(optimize432 = Gtk::CheckButton.new(utf8(_("Optimize for 3/2 aspect ratio"))).set_active($config['default-optimize32'].to_b))
3219     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)
3220     vb.add(Gtk::HBox.new(false, 3).pack_start(Gtk::Label.new(utf8(_("Number of thumbnails per row: "))), false, false, 0).
3221                                    pack_start(nperrowradios = Gtk::HBox.new, false, false, 0))
3222     nperpage_model = Gtk::ListStore.new(String, String)
3223     vb.add(Gtk::HBox.new(false, 3).pack_start(Gtk::Label.new(utf8(_("Number of thumbnails per page: "))), false, false, 0).
3224                                    pack_start(nperpagecombo = Gtk::ComboBox.new(nperpage_model), false, false, 0))
3225     nperpagecombo.pack_start(crt = Gtk::CellRendererText.new, false)
3226     nperpagecombo.set_attributes(crt, { :markup => 0 })
3227     iter = nperpage_model.append
3228     iter[0] = utf8(_("<i>None - all thumbnails in one page</i>"))
3229     iter[1] = nil
3230     [ 12, 20, 30, 40, 50 ].each { |v|
3231         iter = nperpage_model.append
3232         iter[0] = iter[1] = v.to_s
3233     }
3234     nperpagecombo.active = 0
3235
3236     multilanguages_value = nil
3237     vb.add(ml = Gtk::HBox.new(false, 3).pack_start(ml_label = Gtk::Label.new, false, false, 0).
3238                                         pack_start(multilanguages = Gtk::Button.new(utf8(_("Configure multi-languages"))), false, false, 0))
3239     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)
3240     multilanguages.signal_connect('clicked') {
3241         retval = ask_multi_languages(multilanguages_value)
3242         if retval[0] 
3243             multilanguages_value = retval[1]
3244         end
3245         if multilanguages_value
3246             ml_label.text = utf8(_("Multi-languages: enabled."))
3247         else
3248             ml_label.text = utf8(_("Multi-languages: disabled."))
3249         end
3250     }
3251     if $config['default-multi-languages']
3252         multilanguages_value = $config['default-multi-languages']
3253         ml_label.text = utf8(_("Multi-languages: enabled."))
3254     else
3255         ml_label.text = utf8(_("Multi-languages: disabled."))
3256     end
3257
3258     vb.add(Gtk::HBox.new(false, 3).pack_start(Gtk::Label.new(utf8(_("'Return to your website' link on pages bottom: "))), false, false, 0).
3259                                    pack_start(indexlinkentry = Gtk::Entry.new, true, true, 0))
3260     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)
3261     vb.add(Gtk::HBox.new(false, 3).pack_start(Gtk::Label.new(utf8(_("'Made with' markup on pages bottom: "))), false, false, 0).
3262                                    pack_start(madewithentry = Gtk::Entry.new.set_text(utf8(_("made with <a href=%booh>booh</a>!"))), true, true, 0))
3263     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)
3264
3265     src_nb_calculated_for = ''
3266     src_nb_thread = nil
3267     process_src_nb = proc {
3268         if src.text != src_nb_calculated_for
3269             src_nb_calculated_for = src.text
3270             if src_nb_thread
3271                 Thread.kill(src_nb_thread)
3272                 src_nb_thread = nil
3273             end
3274             if src_nb_calculated_for != '' && from_utf8_safe(src_nb_calculated_for) == ''
3275                 src_nb.set_markup(utf8(_("<span size='small'><i>invalid source directory</i></span>")))
3276             else
3277                 if File.directory?(from_utf8_safe(src_nb_calculated_for)) && src_nb_calculated_for != '/'
3278                     if File.readable?(from_utf8_safe(src_nb_calculated_for))
3279                         src_nb_thread = Thread.new {
3280                             gtk_thread_protect { src_nb.set_markup(utf8(_("<span size='small'><i>processing...</i></span>"))) }
3281                             total = { 'image' => 0, 'video' => 0, nil => 0 }
3282                             `find '#{from_utf8_safe(src_nb_calculated_for)}' -type d -follow`.each { |dir|
3283                                 if File.basename(dir) =~ /^\./
3284                                     next
3285                                 else
3286                                     begin
3287                                         Dir.entries(dir.chomp).each { |file|
3288                                             total[entry2type(file)] += 1
3289                                         }
3290                                     rescue Errno::EACCES, Errno::ENOENT
3291                                     end
3292                                 end
3293                             }
3294                             gtk_thread_protect { src_nb.set_markup(utf8(_("<span size='small'><i>%s photos and %s videos</i></span>") % [ total['image'], total['video'] ])) }
3295                             src_nb_thread = nil
3296                         }
3297                     else
3298                         src_nb.set_markup(utf8(_("<span size='small'><i>permission denied</i></span>")))
3299                     end
3300                 else
3301                     src_nb.set_markup(utf8(_("<span size='small'><i>N/A</i></span>")))
3302                 end
3303             end
3304         end
3305         true
3306     }
3307     timeout_src_nb = Gtk.timeout_add(100) {
3308         process_src_nb.call
3309     }
3310
3311     src_browse.signal_connect('clicked') {
3312         fc = Gtk::FileChooserDialog.new(utf8(_("Select the directory of photos/videos")),
3313                                         nil,
3314                                         Gtk::FileChooser::ACTION_SELECT_FOLDER,
3315                                         nil,
3316                                         [Gtk::Stock::OPEN, Gtk::Dialog::RESPONSE_ACCEPT], [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
3317         fc.transient_for = $main_window
3318         if fc.run == Gtk::Dialog::RESPONSE_ACCEPT
3319             src.text = utf8(fc.filename)
3320             process_src_nb.call
3321             conf.text = File.expand_path("~/.booh/#{File.basename(src.text)}")
3322         end
3323         fc.destroy
3324     }
3325
3326     dest_browse.signal_connect('clicked') {
3327         fc = Gtk::FileChooserDialog.new(utf8(_("Select a new directory where to put the web-album")),
3328                                         nil,
3329                                         Gtk::FileChooser::ACTION_CREATE_FOLDER,
3330                                         nil,
3331                                         [Gtk::Stock::OPEN, Gtk::Dialog::RESPONSE_ACCEPT], [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
3332         fc.transient_for = $main_window
3333         if fc.run == Gtk::Dialog::RESPONSE_ACCEPT
3334             dest.text = utf8(fc.filename)
3335         end
3336         fc.destroy
3337     }
3338
3339     conf_browse.signal_connect('clicked') {
3340         fc = Gtk::FileChooserDialog.new(utf8(_("Select a new file to store this album's properties")),
3341                                         nil,
3342                                         Gtk::FileChooser::ACTION_SAVE,
3343                                         nil,
3344                                         [Gtk::Stock::SAVE, Gtk::Dialog::RESPONSE_ACCEPT], [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
3345         fc.transient_for = $main_window
3346         fc.add_shortcut_folder(File.expand_path("~/.booh"))
3347         fc.set_current_folder(File.expand_path("~/.booh"))
3348         if fc.run == Gtk::Dialog::RESPONSE_ACCEPT
3349             conf.text = utf8(fc.filename)
3350         end
3351         fc.destroy
3352     }
3353
3354     theme_sizes = []
3355     nperrows = []
3356     recreate_theme_config = proc {
3357         theme_sizes.each { |e| sizes.remove(e[:widget]) }
3358         theme_sizes = []
3359         select_theme(theme_button.label, 'all', optimize432.active?, nil)
3360         $images_size.each { |s|
3361             sizes.add(cb = Gtk::CheckButton.new(sizename(s['name'], true)))
3362             if !s['optional']
3363                 cb.active = true
3364             end
3365             tooltips.set_tip(cb, utf8(s['description']), nil)
3366             theme_sizes << { :widget => cb, :value => s['name'] }
3367         }
3368         sizes.add(cb = Gtk::CheckButton.new(utf8(_('original'))))
3369         tooltips = Gtk::Tooltips.new
3370         tooltips.set_tip(cb, utf8(_("Include original photo in web-album")), nil)
3371         theme_sizes << { :widget => cb, :value => 'original' }
3372         sizes.show_all
3373
3374         nperrows.each { |e| nperrowradios.remove(e[:widget]) }
3375         nperrow_group = nil
3376         nperrows = []
3377         $allowed_N_values.each { |n|
3378             if nperrow_group
3379                 nperrowradios.add(rb = Gtk::RadioButton.new(nperrow_group, n.to_s, false))
3380             else
3381                 nperrowradios.add(nperrow_group = rb = Gtk::RadioButton.new(n.to_s))
3382             end
3383             tooltips.set_tip(rb, utf8(_("Set the number of thumbnails per row of the 'thumbnails' pages (if chosen theme uses a row arrangement)")), nil)
3384             if $default_N == n
3385                 rb.active = true
3386             end
3387             nperrows << { :widget => rb, :value => n }
3388         }
3389         nperrowradios.show_all
3390     }
3391     recreate_theme_config.call
3392
3393     theme_button.signal_connect('clicked') {
3394         if newtheme = theme_choose(theme_button.label)
3395             theme_button.label = newtheme
3396             recreate_theme_config.call
3397         end
3398     }
3399
3400     dialog.vbox.add(frame1)
3401     dialog.vbox.add(frame2)
3402     dialog.show_all
3403
3404     keepon = true
3405     ok = true
3406     while keepon
3407         dialog.run { |response|
3408             if response == Gtk::Dialog::RESPONSE_OK
3409                 srcdir = from_utf8_safe(src.text)
3410                 destdir = from_utf8_safe(dest.text)
3411                 confpath = from_utf8_safe(conf.text)
3412                 if src.text != '' && srcdir == ''
3413                     show_popup(dialog, utf8(_("The directory of photos/videos is invalid. Please check your input.")))
3414                     src.grab_focus
3415                 elsif !File.directory?(srcdir)
3416                     show_popup(dialog, utf8(_("The directory of photos/videos doesn't exist. Please check your input.")))
3417                     src.grab_focus
3418                 elsif dest.text != '' && destdir == ''
3419                     show_popup(dialog, utf8(_("The destination directory is invalid. Please check your input.")))
3420                     dest.grab_focus
3421                 elsif destdir != make_dest_filename(destdir)
3422                     show_popup(dialog, utf8(_("Sorry, destination directory can't contain non simple alphanumeric characters.")))
3423                     dest.grab_focus
3424                 elsif File.directory?(destdir) && Dir.entries(destdir).size > 2
3425                     keepon = !show_popup(dialog, utf8(_("The destination directory already exists. All existing files and directories
3426 inside it will be permanently removed before creating the web-album!
3427 Are you sure you want to continue?")), { :okcancel => true })
3428                     dest.grab_focus
3429                 elsif File.exists?(destdir) && !File.directory?(destdir)
3430                     show_popup(dialog, utf8(_("There is already a file by the name of the destination directory. Please choose another one.")))
3431                     dest.grab_focus
3432                 elsif conf.text == ''
3433                     show_popup(dialog, utf8(_("Please specify a filename to store the album's properties.")))
3434                     conf.grab_focus
3435                 elsif conf.text != '' && confpath == ''
3436                     show_popup(dialog, utf8(_("The filename to store the album's properties is invalid. Please check your input.")))
3437                     conf.grab_focus
3438                 elsif File.directory?(confpath)
3439                     show_popup(dialog, utf8(_("Sorry, the filename specified to store the album's properties is an existing directory. Please choose another one.")))
3440                     conf.grab_focus
3441                 elsif !theme_sizes.detect { |e| e[:value] != 'original' && e[:widget].active? }
3442                     show_popup(dialog, utf8(_("You need to select at least one size (not counting original).")))
3443                 else
3444                     system("mkdir '#{destdir}'")
3445                     if !File.directory?(destdir)
3446                         show_popup(dialog, utf8(_("Could not create destination directory. Permission denied?")))
3447                         dest.grab_focus
3448                     else
3449                         keepon = false
3450                     end
3451                 end
3452             else
3453                 keepon = ok = false
3454             end
3455         }
3456     end
3457     if ok
3458         srcdir = from_utf8(src.text)
3459         destdir = from_utf8(dest.text)
3460         configskel = File.expand_path(from_utf8(conf.text))
3461         theme = theme_button.label
3462         #- some sort of automatic theme preference
3463         $config['default-theme'] = theme
3464         $config['default-multi-languages'] = multilanguages_value
3465         $config['default-optimize32'] = optimize432.active?.to_s
3466         sizes = theme_sizes.find_all { |e| e[:widget].active? }.collect { |e| e[:value] }.join(',')
3467         nperrow = nperrows.find { |e| e[:widget].active? }[:value]
3468         nperpage = nperpage_model.get_value(nperpagecombo.active_iter, 1)
3469         opt432 = optimize432.active?
3470         madewith = madewithentry.text.gsub('\'', '&#39;')  #- because the parameters to booh-backend are between apostrophes
3471         indexlink = indexlinkentry.text.gsub('\'', '&#39;')
3472     end
3473     if src_nb_thread
3474         Thread.kill(src_nb_thread)
3475         gtk_thread_flush  #- needed because we're about to destroy widgets in dialog, for which they may be some pending gtk calls
3476     end
3477     dialog.destroy
3478     Gtk.timeout_remove(timeout_src_nb)
3479
3480     if ok
3481         call_backend("booh-backend --source '#{srcdir}' --destination '#{destdir}' --config-skel '#{configskel}' --for-gui " +
3482                      "--verbose-level #{$verbose_level} --theme #{theme} --sizes #{sizes} --thumbnails-per-row #{nperrow} " +
3483                      (nperpage ? "--thumbnails-per-page #{nperpage} " : '') +
3484                      (multilanguages_value ? "--multi-languages #{multilanguages_value} " : '') +
3485                      "#{opt432 ? '--optimize-for-32' : ''} --made-with '#{madewith}' --index-link '#{indexlink}' #{additional_booh_options}",
3486                      utf8(_("Please wait while scanning source directory...")),
3487                      'full scan',
3488                      { :closure_after => proc {
3489                              open_file_user(configskel)
3490                              $main_window.urgency_hint = true
3491                          } })
3492     end
3493 end
3494
3495 def properties
3496     dialog = Gtk::Dialog.new(utf8(_("Properties of your album")),
3497                              $main_window,
3498                              Gtk::Dialog::MODAL | Gtk::Dialog::DESTROY_WITH_PARENT,
3499                              [Gtk::Stock::OK, Gtk::Dialog::RESPONSE_OK],
3500                              [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
3501     
3502     source = $xmldoc.root.attributes['source']
3503     dest = $xmldoc.root.attributes['destination']
3504     theme = $xmldoc.root.attributes['theme']
3505     opt432 = !$xmldoc.root.attributes['optimize-for-32'].nil?
3506     nperrow = $xmldoc.root.attributes['thumbnails-per-row']
3507     nperpage = $xmldoc.root.attributes['thumbnails-per-page']
3508     limit_sizes = $xmldoc.root.attributes['limit-sizes']
3509     if limit_sizes
3510         limit_sizes = limit_sizes.split(/,/)
3511     end
3512     madewith = ($xmldoc.root.attributes['made-with'] || '').gsub('&#39;', '\'')
3513     indexlink = ($xmldoc.root.attributes['index-link'] || '').gsub('&#39;', '\'')
3514     save_multilanguages_value = multilanguages_value = $xmldoc.root.attributes['multi-languages']
3515
3516     tooltips = Gtk::Tooltips.new
3517     frame1 = Gtk::Frame.new(utf8(_("Locations"))).add(tbl = Gtk::Table.new(0, 0, false))
3518     tbl.attach(Gtk::Label.new(utf8(_("Directory of source photos/videos: "))),
3519                0, 1, 0, 1, Gtk::SHRINK, Gtk::SHRINK, 2, 2)
3520     tbl.attach(Gtk::Alignment.new(0, 0.5, 0, 0).add(Gtk::Label.new.set_markup('<i>' + source + '</i>')),
3521                1, 2, 0, 1, Gtk::FILL, Gtk::SHRINK, 2, 2)
3522     tbl.attach(Gtk::Label.new(utf8(_("Directory where the web-album is created: "))),
3523                0, 1, 2, 3,&n