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